The article is a little story about how the desire to simplify the application for the end user, came out very time-consuming process.
Speech about the "automatic" port forwarding through
UPnP technology, without using the "standard" library
NATUPnPLib .
About that, by virtue of what such a difficult way was chosen and why it is still not an easy one - read below.
Prologue
While working on my project (game project), I clearly realized that sooner or later I would have to come to the server and the link to this question. It is worth noting that he was in the plans. But I remembered my experience with a dedicated server, for the same minecraft, I was ready for a certain “pain”, especially if I tried to promote my product to the masses.
')
Briefly about the dedicated server Minecraft'aMany people who tried to start their dedicated server had one common problem, having a router, they could connect to themselves, but any person from outside did not. It is clear that such issues were solved by forwarding the port in the settings of the router, but as practice has shown (as well as a bunch of videos on YouTube), it was difficult for people.
From all these things, the underlying requirements for my server came out.
- The user must do a minimum of movements to start the server and start the game.
- The solution should work out of the box on any machine under Windows (support for Unix systems is not yet in the plans).
In essence, this meant the following goals:
- Port forwarding should occur without human intervention.
- The solution will be written in C # in two x64 and x32 architectures (priority on x64, due to the fact that it will probably require “a lot” of memory).
Illusion of solution
Having decided on the language, the first thing I did was go to Google to find out if there are already ready-made solutions. And indeed, a “solution” was immediately found, it was proposed to use the NATUPnPLib library, and immediately an example of its use:
NATUPNPLib.UPnPNATClass upnpnat = new NATUPNPLib.UPnPNATClass(); NATUPNPLib.IStaticPortMappingCollection mappings = upnpnat.StaticPortMappingCollection; // mappings.Add(12345, "UDP", 12345, "localIp", true, "Some name"); // mappings.Remove(12345, "UDP");
Having rejoiced to this, I hurried to test the received "little animal". In the end, on one machine I managed to get the desired result. However, when I was completely rejoiced, I decided to “test a bit” (in general, as practice shows, this is a useful action), and I ran the compiled code on the next machine (in one local computer, with one router “at the head”) and then the first disappointment.
Alas, upnpnat.StaticPortMappingCollection - returned null, whatever I did. At the same time, a “report” came from another person, whom I also asked to test, his answer was just as sad, this library was not allowed for him at all (probably it was not registered in the system, for some reason or forbidden, or as, but the point is that she did not pick up).
“Lack of results, also results” - as one cunning wisdom says. The sad result, gave me an understanding that if I leave this library, the end user will have similar errors, which means I will have to prepare to accept the flow of "good". What I, for some reason did not want.
Finding the way
In the frustrated feelings went google replacement NATUPnPLib. But as it turned out, almost all ready-made solutions were essentially wrappers over this library. For the sake of experiment, they were tried, but since it is based on NATUPnPLib, everything ended as before.
This greatly grieved. However, looking at such products as uTorrent, for example, and seeing that it is successfully forwarding the port, I decided to take the tricky
scientific path. The idea is simple as a cat. I know that a certain command or part is transmitted from the machine to the router, which should somehow say what the router has to do. It remained the case for small, “face” this team or a set of teams.
The first link in Google to the request for a network sniffer was Wireshark. Then he started to google, that he knows how this
article came across from comrade
sinist3r for which he thanks a lot. The article describes to a sufficient degree how and what to do, therefore I will not dwell on this in detail.
Network traffic analysis
Having started Wireshark and having made port forwarding with NATUPnPLib library (from that machine on which everything worked), we receive something like this:
So, what we have:
- A bunch of network information
- We know the ip address of the machine from which we send
- We know the ip address of other machines
We will configure the filter so that there is only a request from the machine with which we carry out the test (column Source ip 150th), and also that there were no other machines in the destination column (ip 200).
We look at the resulting list and see some interesting thing, namely, the
multicast group and the
SSDP protocol, and the following message is sent to it:
This is already interesting. We go to Google and see what kind of a multicast group it is, and ... a
dramatic pause ... with the first request we kill two small fluffy and eared creatures, Google gives this
link .
NoteWhy I decided that the address for which the request is being sent is multicast, I remind you that addresses starting with 224.0.0.0 and ending with 239.255.255.255 are class D, which was reserved for multicast groups. More details can be found
here .
I joyfully rub my paws, like a fly, from a literally small Wikipedia article, I find out that the UDP protocol is being used, a detection message is sent, which was shown in the screenshot above. And everything says that I am on the right path.
From theory to practice
Having a request at hand, the protocol for which we are addressing and the address, I decided to try to make a small console program to test, how it will, and whether it will work at all.
So, such a request should be sent to a multicast group, on port 1900. The router, having heard the request, will respond, the
machine with which the request came , with a certain answer.
M-SEARCH * HTTP / 1.1 \ r \ n
HOST: 239.255.255.250: 1900 \ r \ n
MAN: \ "ssdp: discover \" \ r \ n
ST: upnp: rootdevice \ r \ n
MX: 3 \ r \ n \ r \ n
You can find out more about what is
here .
We write about this code:
Sample code IPEndPoint MulticastEndPoint = new IPEndPoint(IPAddress.Parse("239.255.255.250"), 1900);// 1900 IPEndPoint LocalEndPoint = new IPEndPoint(GetLocalAdress(), 0);// IP-a // , Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); // socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); socket.Bind(LocalEndPoint);// socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(MulticastEndPoint.Address, IPAddress.Any)); socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, 2); socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastLoopback, true); string searchString = "M-SEARCH * HTTP/1.1\r\nHOST:239.255.255.250:1900\r\nMAN:\"ssdp:discover\"\r\nST:upnp:rootdevice\r\nMX:3\r\n\r\n"; byte[] data = Encoding.UTF8.GetBytes(searchString); socket.SendTo(data, data.Length, SocketFlags.None, MulticastEndPoint); socket.SendTo(data, data.Length, SocketFlags.None, MulticastEndPoint); socket.SendTo(data, data.Length, SocketFlags.None, MulticastEndPoint); // , ! byte[] ReceiveBuffer = new byte[64000]; int ReceivedBytes = 0; int repeatCount = 10; while (repeatCount>0) { if (socket.Available > 0) { ReceivedBytes = socket.Receive(ReceiveBuffer, SocketFlags.None); if (ReceivedBytes > 0) { Console.WriteLine(Encoding.UTF8.GetString(ReceiveBuffer, 0, ReceivedBytes)); } } else { repeatCount
Here's what happens in the end
We are interested in the underlined line, it is for her in the future and will be communicating with the router. (How did I understand this? In the same Wireshark, I pointed out the source of the ip-machine from which the request was sent, and as the destination, an ip-router, and saw a bunch of http requests)
NoteFirst of all, I would like to draw your attention to the fact that the message is carried out three times, due to the fact that on some machines (I have one of the three), the first packet is “lost” and in fact is not sent at all (when observed in Wireshark ' e there is not even a hint that the packet is sent). Read about such things on the Internet, but okromya solutions "go to TCP" or "make several requests" did not find.
NoteWhy use this design:
IPEndPoint LocalEndPoint = new IPEndPoint(GetLocalAdress(), 0)
Not IPAddress.Any, as a result of this
answer
Now not much about the function GetLocalAdress. Usually, it is suggested to use such or similar code.
Dns.GetHostEntry(Dns.GetHostName()).AddressList.FirstOrDefault(ip => ip.AddressFamily == AddressFamily.InterNetwork);
However, if you have VirtualBox on your machine or, say, Tunngle, or something like that, which puts your adapter, then in this case, the above code will return the address of that adapter itself. That “it is not good”, and therefore it is necessary either to try to cut off the “left” addresses by names, or as I suggest:
Code example private static IPAddress GetLocalAdress() { NetworkInterface[] networkInterfaces = NetworkInterface.GetAllNetworkInterfaces(); foreach (NetworkInterface network in networkInterfaces) { IPInterfaceProperties properties = network.GetIPProperties(); if (properties.GatewayAddresses.Count == 0)// continue; foreach (IPAddressInformation address in properties.UnicastAddresses) { if (address.Address.AddressFamily != AddressFamily.InterNetwork) continue; if (IPAddress.IsLoopback(address.Address)) continue; return address.Address; } } return default(IPAddress); }
The end is near
So, almost everything we have. You can go to the final stage, namely, to try in practice to forward the port and close it.
I’ll omit moments like I’m at Wireshark, I’ve watched which commands go and where, I wrote in sufficient detail earlier, the further search is quite simple, considering that all communication with the router is already going through HTTP.
Having received at the previous stage, the path to request information about the router, let's do it. Immediately make a reservation when HTTP requests,
be sure to specify UserAgent = "Microsoft-Windows / 6.1 UpnP / 1.0"; (naturally given the real version of Windows).
In my case, the GET request must be sent to this address:
http://192.168.0.1:46382/rootDesc.xml
In the received, huge answer, (yes, here in this huge sheet of text), we are interested in the controlURL tag, in which the serviceType is equal to the urn: schemas-upnp-org: service: WANIPConnection: 1.
NoteWANPPPConnection (ADSL modems) and WANIPConnection (IP routers)
Read more, you can read
here , including the commands to add or remove ports
In my case, the value "/ ctl / IPConn" is received. We add it to the address of the router, as a result we get this:
http://192.168.0.1:46382/ctl/IPConn
Now we will collect the request body, it should contain:
- NewRemoteHost // leave empty
- NewExternalPort // external port
- NewProtocol // protocol (TCP / UDP)
- NewInternalPort // internal port
- NewInternalClient // ip "on which" we open
- NewEnabled // on or off
- NewPortMappingDescription // description
- NewLeaseDuration // lifespan, 0 - forever
Having collected, I received such a body (Formatted to improve reading):
Request body <? xml version = \ "1.0 \"?>
<SOAP-ENV: Envelope xmlns: SOAP-ENV = \ "http: //schemas.xmlsoap.org/soap/envelope/ \" SOAP-ENV: encodingStyle = \ "http://schemas.xmlsoap.org/soap/ encoding / \ ">
<SOAP-ENV: Body> <m: AddPortMapping xmlns: m = \ "urn: schemas-upnp-org: service: WANIPConnection: 1 \">
<NewRemoteHost xmlns: dt = \ "urn: schemas-microsoft-com: datatypes \" dt: dt = \ "string \"> </ NewRemoteHost>
<NewExternalPort xmlns: dt = \ "urn: schemas-microsoft-com: datatypes \" dt: dt = \ "ui2 \"> 25565 </ NewExternalPort>
<NewProtocol xmlns: dt = \ "urn: schemas-microsoft-com: datatypes \" dt: dt = \ "string \"> TCP </ NewProtocol>
<NewInternalPort xmlns: dt = \ "urn: schemas-microsoft-com: datatypes \" dt: dt = \ "ui2 \"> 25565 </ NewInternalPort>
<NewInternalClient xmlns: dt = \ "urn: schemas-microsoft-com: datatypes \" dt: dt = \ "string \"> 192.168.0.150 </ NewInternalClient>
<NewEnabled xmlns: dt = \ "urn: schemas-microsoft-com: datatypes \" dt: dt = \ "boolean \"> 1 </ NewEnabled>
<NewPortMappingDescription xmlns: dt = \ "urn: schemas-microsoft-com: datatypes \" dt: dt = \ "string \"> Test your port </ NewPortMappingDescription>
<NewLeaseDuration xmlns: dt = \ "urn: schemas-microsoft-com: datatypes \" dt: dt = \ "ui4 \"> 0 </ NewLeaseDuration>
</ m: AddPortMapping>
</ SOAP-ENV: Body> </ SOAP-ENV: Envelope>
After completing the query, and if everything is correctly stated, we will get a similar result.
Similarly, it is easier to remove the port, there are only two important parameters - the protocol and the port, I stress, the external port.
Conclusion
In this way, the simple desire to make the user “simpler” resulted in a whole epic and article. I hope the article will help someone to avoid the certain difficulties that I encountered.
Thank you all, who read, if there are any additions, write, supplement the article.