В статье небольшая история о том как желание упростить приложение для конечного пользователя, вышло весьма трудоемким процессом.
Речь об «автоматической» пробросе порта, через технологию UPnP, без использования «стандартной» библиотеки NATUPnPLib.
О том, в силу чего был выбран такой непростой путь и почему он все-таки непростой — читайте ниже.
Пролог
Работая над своим проектом (игровой проект), я четко осознавал, что рано или поздно придется подойти в плотную к вопросу сервера и связки с оным. Стоит отметить, что он был в планах. Но я помнил свой опыт работы с выделенным сервером, для того же minecraft'a, был готов к тому что меня ждет определенная «боль», в особенности, если я попытаюсь продвинуть свой продукт в массы.
У многих людей, которые пытались запустить свой выделенный сервер, была одна общая проблема, они имея роутер, сами могли к себе подключиться, а любой человек из вне — нет. Понятно дело, что такие вопросы решались пробросом порта в настройках роутера, но как показывала практика (а так же куча видео на ютубе), что это было сложно для людей.
Из всех этих вещей вытекло основополагающие требования для моего сервера.
- Пользователь должен делать минимум движений для запуска сервера и начала игры.
- Решение должно работать «из коробки» на любой машине под Windows (поддержка Unix систем пока что не в планах).
По сути это означало следующие цели:
- Проброс порта должен происходить без вмешательства человека.
- Решение будет написано на C# в двух архитектрурах x64 и x32 (приоритет на x64, в связи с тем, что вероятно потребуется «много» памяти).
Иллюзия решения
Определившись с языком, первым же делом отправился в гугл, узнать если ли уже готовые решения. И действительно, было сразу же найдено «решение», предлагалось использовать библиотеку NATUPnPLib, и сразу же пример ее использования:
NATUPNPLib.UPnPNATClass upnpnat = new NATUPNPLib.UPnPNATClass();
NATUPNPLib.IStaticPortMappingCollection mappings = upnpnat.StaticPortMappingCollection;
//Добавление порта
mappings.Add(12345, "UDP", 12345, "localIp", true, "Some name");
//Удаление порта
mappings.Remove(12345, "UDP");
Возрадовавшись сему, я поспешил опробовать полученную «зверюшку». В конечном итоге на одной машине мне удалось, получить желаемый результат. Однако, когда я уже совсем возрадовался, я решил «чуток потестировать» (вообще как показывает практика, это полезное действие), и запустил скомпиленый код на соседней машине (в одной локалке, с одним роутером «во главе») и тут меня ждало первое разочарование.
Увы, upnpnat.StaticPortMappingCollection — возвращал null, что бы я не делал. Вместе с этим пришел «отчет» от другого человека, которого я так же попросил протестировать, его ответ был так же грустен, у него данная библиотека вообще не разрешалась (вероятно была не зарегистрирована в системе, по какой-то причине или запрещена, или еще как, но суть в том что она не подхватывалась).
«Отсутствие результата, тоже результат» — так гласит одна хитрая мудрость. Печальный результат, дал мне понимание, что если я оставлю эту библиотеку, то подобные же ошибки будут у конечного потребителя, а значит мне придется готовится принимать поток «добра». Что мне, почему-то совсем не хотелось.
Поиск пути
В расстроенных чувствах пошел гуглить замену NATUPnPLib. Но как оказалось, почти все готовые решения были по сути обертками над этой библиотекой. Ради эксперимента они были попробованы, но поскольку в основе NATUPnPLib, то все заканчивалось как и ранее.
Это сильно огорчало. Однако глядя на такие продукты как например uTorrent, и видя что он успешно пробрасывает порт, я решил пойти научным хитрым путем. Идея проста как котик. Я знаю, что от машины до роутера передается некая команда или часть, в которой должно быть как-то сказано, что роутер должен сделать. Оставалось дело за малым, «посмотреть в лицо» этой команде или набору команд.
Первой же ссылкой в гугле, на запрос о сетевом сниффере был Wireshark. Дальше начав гуглить, что он умеет попалась вот эта статья от товарища sinist3r за что ему большое спасибо. В статье в достаточной степени описано, как и что делать, потому подробно на этом останавливаться не стану.
Анализ сетевого трафика
Запустив Wireshark и сделав проброс порта с библиотекой NATUPnPLib (с той машины на которой все работало), получаем что-то в таком духе:
Итак, что мы имеем:
- Кучу сетевой информации
- Знаем ip адрес машины, с которой отправляем
- Знаем ip адрес других машин
Настроим фильтр так, что остался запрос только от машины с которой проводим тест (столбец Source ip 150-й), а так же что бы в столбце назначения не было других машин (ip 200).
Смотрим на полученный список и видим какую-то интересную вещь, а именно мультикаст группа и протокол SSDP, и на нее посылается такое сообщение:
Это уже интересно. Идем в гугл, и смотрим, что это за мультикаст группа, и… драматическая пауза… первым же запросом убиваем двух маленьких пушистых и ушастых существ, гугл выдает такую ссылку.
Радостно потираю лапки, как муха, из буквально маленькой статьи на Википедии, узнаю, что используется протокол UDP, посылается сообщение обнаружение, которое было приведено в скрине выше. И все говорит, о том что я иду правильным путем.
От теории к практике
Имея под рукой запрос, протокол по которому обращаемся и адрес, я решил попробовать сделать небольшую консольную программку, для того что бы протестеровать, как оно будет, и будет ли вообще, работать.
Итак, такой запрос надо отправить в мультикаст группу, на порт 1900. Роутер услышав запрос, ответит, машине с которой пришел запрос, с неким ответом.
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
Подробнее, что есть что, можно посмотреть тут.
Пишем примерно такой код:
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();
Вот что в итоге получиться
Нас интересует подчеркнутая строка, именно по ней в дальнейшем и будет происходить общение с роутером. (Как я это понял? В том же Wireshark'e указал в источнике ip-машины с которого шел запрос, и в качестве назначения — ip-роутера, и увидел кучу http запросов)
Прежде всего хотел бы обратить внимание на то, что посыл осуществляется трижды, связано это с тем, что на некоторых машинах (у меня это одна из трех), первый пакет «теряется», а по сути вообще не отправляется (при наблюдении в Wireshark'e нету даже намека, на то что пакет отсылается). О подобных вещах читал на просторах интернета, но решения окромя «перейдите на TCP» или «делайте несколько запросов» не обнаружил.
IPEndPoint LocalEndPoint = new IPEndPoint(GetLocalAdress(), 0);
А не IPAddress.Any, в следствии вот этого ответа
Теперь не много о функции GetLocalAdress. Обычно, предлагают использовать такой или подобный код
Dns.GetHostEntry(Dns.GetHostName()).AddressList.FirstOrDefault(ip => ip.AddressFamily == AddressFamily.InterNetwork);
Однако, если у вас на машине стоит VirtualBox или скажем Tunngle, или что-то подобное, что ставит свой адаптер, то в таком случае, указанный выше код, вернет адрес этого самого адаптера. Что «не есть хорошо», и потому надо либо как-то по названиям пытаться обрезать «левые» адреса, либо как предлагаю я:
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);
}
Конец уж близок
Итак, почти все у нас есть. Можно перейти к финальной стадии, а именно, попробовать на практике пробросить порт и его закрыть.
Опущу моменты как я в Wireshark'e, наблюдал какие команды ходят и куда, ранее достаточно подробно писал, дальнейший поиск достаточно прост, учитывая, что все общение с роутером уже идет через HTTP.
Получив на предыдущей стадии, путь для запроса информации о роутере, сделаем это. Сразу оговорюсь, при HTTP запросах, обязательно указывать UserAgent = «Microsoft-Windows/6.1 UpnP/1.0»; (естесвенно учитывая реальную версию Windows).
В моем случае GET-запрос, надо послать по этому адресу:
http://ift.tt/1J8GESx
В полученном, огромном ответе, (да-да, вот в этой огромной простыне текста), нас интересует тег controlURL, у которого serviceType равен urn:schemas-upnp-org:service:WANIPConnection:1.
WANPPPConnection (ADSL modems) and WANIPConnection (IP routers)
Подробнее, можно почитать тут, в том числе и о командах для добавления или удаления портов
В моем случае получено значение «/ctl/IPConn». Дописываем его к адресу роутера, в итоге получаем такое:
http://ift.tt/1J8GDhx
Теперь соберем тело запроса, в нем должно быть:
- NewRemoteHost //оставляем пустым
- NewExternalPort //внешний порт
- NewProtocol //протокол (TCP/UDP)
- NewInternalPort //внутренний порт
- NewInternalClient //ip «на который» открываем
- NewEnabled //включен или выключен
- NewPortMappingDescription //описание
- NewLeaseDuration //продолжительность жизни, 0 — навсегда
Собрав, я получил такое тело (Форматировано для улучшения чтения):
<?xml version=\"1.0\"?> <SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://ift.tt/sVJIaE\" SOAP-ENV:encodingStyle=\"http://ift.tt/wEYywg\"> <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 Open Port and say hi, habrahabr</NewPortMappingDescription> <NewLeaseDuration xmlns:dt=\"urn:schemas-microsoft-com:datatypes\" dt:dt=\"ui4\">0</NewLeaseDuration> </m:AddPortMapping> </SOAP-ENV:Body></SOAP-ENV:Envelope>
Выполнив запрос, и если все правильно указано, то получим подобный результат
Аналогично, но проще делается для удаления порта, там всего два важных параметра — это протокол и порт, подчеркну, внешний порт.
Заключение
Вот так вот, простое желание сделать для пользователя «проще», вылилось в целую эпопею и статью. Надеюсь, статья кому-либо поможет избежать тех определенных трудностей, с которыми я столкнулся.
Всем спасибо, кто прочитал, если имеются какие-то дополнения, пишите, дополню статью.
This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.
Комментариев нет:
Отправить комментарий