⬆️ ⬇️

Treacherous router or why ports need to open



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'a
Many 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.

  1. The user must do a minimum of movements to start the server and start the game.
  2. 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:

  1. Port forwarding should occur without human intervention.
  2. 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:

Overall result




So, what we have:



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:

Multicast group




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 .

Note
Why 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--; Thread.Sleep(100);//  ,   ,      ,     } } socket.Close(); 




Here's what happens in the end

Query result




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)

Note
First 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.



Note
Why 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.



Note
WANPPPConnection (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:



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.

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.

Source: https://habr.com/ru/post/264521/



All Articles