Insidious router or why ports should be opened


The article has a short story about how the desire to simplify the application for the end user turned out to be a very time-consuming process.

We are talking about an "automatic" port forwarding, through UPnP technology , without using the "standard" NATUPnPLib library .

About why such a difficult path was chosen and why it is still not easy - read below.

Prologue


While working on my project (game project), I clearly realized that sooner or later I would have to approach the issue of server and connection with it. It is worth noting that he was in the plans. But I remembered my experience working with a dedicated server, for the same minecraft, I was ready for a certain “pain” awaiting me, especially if I try to promote my product to the masses.

Minecraft dedicated server in brief
Many people who tried to start their own dedicated server had one common problem: having a router, they themselves 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 showed (as well as a bunch of videos on YouTube) that it was difficult for people.

From all these things, the fundamental requirements for my server flowed out.
  1. The user must make a minimum of movements to start the server and start the game.
  2. The solution should work out of the box on any machine running 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).

The illusion of decision


Having decided on the language, the first thing I went to google, to find out if there are already ready-made solutions. 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");

Rejoicing at this, I hastened to try out the resulting "animal". In the end, on one machine I managed to get the desired result. However, when I was already completely happy, I decided to "test a little" (in general, as practice shows, this is a useful action), and ran the compiled code on a neighboring machine (in one LAN, with one router "headed") and here I was waiting for the first disappointment.

Alas, upnpnat.StaticPortMappingCollection - returned null, whatever I would do. Along with this, a “report” came from another person whom I also asked to test, his answer was just as sad, his library was not allowed at all (it was probably not registered in the system, for some reason it was either prohibited, or else like, but the point is that she did not pick up).

“Lack of result, also result” - so says one cunning wisdom. The sad result gave me the understanding that if I leave this library, the end user will have the same errors, which means that I will have to get ready to receive the stream of "good." What I, for some reason, did not want at all.

Search for a way


In frustrated feelings, I wentogle the 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 NATUPnPLib is the basis, everything ended as before.

This greatly upset. However, looking at products such as uTorrent, for example, and seeing that it is successfully forwarding the port, I decided to go the scientific tricky way. The idea is as simple as a cat. I know that a certain command or part is transmitted from the machine to the router, in which it should somehow say what the router should do. The only thing left was to “face it” with this team or a set of teams.

The first link in Google to a request for a network sniffer was Wireshark. Then he started to google that he knows how to get this articlefrom comrade sinist3r for which many thanks to him. The article adequately describes how and what to do, so I will not dwell on this in detail.

Network traffic analysis


Launching Wireshark and making a port forwarding with the NATUPnPLib library (from the machine on which everything worked), we get something like this:
Overall result


So what we have:
  • Pile of network information
  • We know the ip address of the machine with which we are sending
  • We know the ip address of other machines

Let's configure the filter so that there is only a request from the machine with which we are conducting the test (Source ip column 150th), and also that there would be no other machines in the destination column (ip 200).

We look at the 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 multicast group it is, and ... a dramatic pause ... with the first request we kill two small fluffy and eared creatures, Google gives such a link .
Note
Why did I decide that the address to 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 a 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, find out that the UDP protocol is used, a detection message is sent, which was shown in the screenshot above. And everything says that I'm going the right way.

From theory to practice


Having the request at hand, the protocol by which we are contacting and the address, I decided to try to make a small console program in order to test how it will work and whether it will work at all.

So, such a request must be sent to the multicast group, on port 1900. Upon hearing the request, the router will respond to 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

More details, what is what, you can see here .

We write something like 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 is the result
Query result


We are interested in the underlined line, it is on it in the future that communication with the router will occur. (How did I understand this? In the same Wireshark, I pointed out the source of the ip machine from which the request was coming, and as the destination - the ip router, and saw a bunch of http requests)
Note
First of all, I would like to draw attention to the fact that the message is sent three times, this is due to the fact that on some machines (I have one of three), the first packet is “lost”, but in fact it is not sent at all (when observed in Wireshark ' e there is not even a hint that the packet is being sent). I read about such things on the Internet, but I didn’t find solutions besides “go to TCP” or “make some requests”.

Note
Why I use this design:
IPEndPoint LocalEndPoint = new IPEndPoint(GetLocalAdress(), 0);

And not IPAddress.Any, as a result of this answer

Now a little about the GetLocalAdress function. Usually, they suggest using this 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 similar that puts its adapter, then in this case, the above code will return the address of this adapter itself. That "not good", and therefore it is necessary either somehow by name to try to trim the "left" addresses, 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, try to forward the port in practice and close it.

I’ll omit the points as I was in Wireshark, I watched which teams go and where, previously I wrote in sufficient detail, the further search is quite simple, given that all communication with the router is already going through HTTP.

Having received at the previous stage, the path for requesting information about the router, do it. Immediately make a reservation, for HTTP requests, be sure to specify UserAgent = "Microsoft-Windows / 6.1 UpnP / 1.0"; (naturally considering the real version of Windows).

In my case, a GET request should be sent to this address:
 http://192.168.0.1:46382/rootDesc.xml


In the huge answer we received (yes, in this huge text sheet), we are interested in the controlURL tag with serviceType equal to urn: schemas-upnp-org: service: WANIPConnection: 1.

Note
WANPPPConnection (ADSL modems) and WANIPConnection (IP routers)

More details can be found here , including commands for adding or removing 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 let's 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 // life expectancy, 0 - forever

Having assembled, I got such a body (Formatted to improve reading):
Request body
25565TCP25565192.168.0.1501Test Open Port and say hi, habrahabr0


Having fulfilled the request, and if everything is correctly indicated, then we will get a similar result
Result



Similarly, but it’s easier to remove the port, there are only two important parameters - the protocol and the port, I emphasize the external port.

Conclusion


So, the simple desire to make the user “simpler” turned into a whole epic and article. I hope this article helps someone avoid the specific difficulties that I have encountered.

Thank you all who read, if there are any additions, write, I will supplement the article.

Also popular now: