Приватный динамический IP – прийти, увидеть, утаить

от автора

image
Не задалось у меня общение с DynDNS сервисами буквально с первого дня знакомства. Грабли попадались на каждом шагу: регистрация, скачивание и запуск клиента, настройка клиента или роутера – везде были какие-то мелкие нюансы, недоговорки, недоделки или просто баги, что приводило к неработоспособности сервиса. В довесок ко всему, через время «эти ребята» вдруг перестают быть белыми, пушистыми и бесплатными — начинают слать спам, раз в месяц требовать разгадать капчу или заставляют проделывать еще какие-либо телодвижения, чтобы доказать что ты еще жив. Всё это привело к общей неприязни ко всем сервисам подобного рода. Так и возникла идея создать что-то своё, и чтоб обязательно «белое и пушистое».

От идеи до реализации прошло довольно много времени. В основном из-за непонимания «а что собственно мне надо?». Читал статьи на досуге, кумекал, и постепенно появился в голове список основных требований к велосипеду.

Основные положения.

Назначение: узнать IP адрес удаленного компьютера (например домашний компьютер).
Уровень паранойи: выше среднего! (то есть IP адрес должны знать только доверенные лица). Вот тут как раз и основное отличие, от подобных сервисов – я не хочу чтобы любой желающий мог получить адрес моего компьютера просто вбив в командной строке что-то типа «ping supercomp.dyndns.org».
Обязательные условия «пушистости»:

  1. Бесплатность (не забываем что и время тоже деньги).
  2. Стабильность.
  3. Простота готового решения для конечного пользователя.

Исходя из уточнения к первому условию, технологии решено было использовать только те, что лично мне более-менее известны — Windows, c#, ASP.NET.
Под влиянием статьи «Свой простой DynDNS сервер» была предпринята попытка написания небольшого сайта-посредника. Но, посмотрев на поразительно стабильную не стабильность бесплатных ASP.NET хостингов, от этой идеи было решено отказаться и в качестве посредников использовать бесплатные почтовые сервисы и облачные хранилища. Кстати, именно из упомянутой статьи была взята, показавшаяся здравой, идея с «возможностью хранения IP адресов всех интерфейсов клиента».
Вот как-то так и получилось, что это должно быть обычное виндовое си-шарповое приложение.

Выбор «хранилища»

Под хранилищем подразумевается некое место, где будет лежать наша информация. Место это должно быть защищено от посторонних взглядов, быть легкодоступно из любой точки и обязательно соответствовать трем «пушистым» требованиям.
Чтобы сильно не напрягаться, было решено остановиться на таких вариантах:

  • Файловая система компьютера (например папка синхронизируемая каким-нибудь облачным клиентом) – сохранение или чтение проблем не вызывает абсолютно, вся работа с сетью лежит на клиенте облака.
  • Почта – отправляются письма без проблем, а вот читать приходится через стороннюю бесплатную библиотеку.
  • Облачное хранилище (имеется ввиду взаимодействие с облаком без установки клиента) – вполне осуществимо.

На третьем пункте остановимся, и рассмотрим возможные варианты.
Предварительный опрос друзей и знакомых показывал, что большинство ничего не имеют против Яндекс-Диска и Скай-Драйва. Поэтому они изначально рассматривались как основные претенденты. Но проведя пол дня в «активном поиске», оказалось, что далеко не каждый облачный сервис предоставляет вменяемое средство взаимодействия. Например, Скай-Драйв API с некоторых пор невозможно использовать в настольных приложениях, Гугл-Драйв API – без бутылки не разобраться, а у ДропБокс – я как-то вообще не нашел SDK для Windows. Использование не официальных или устаревших “API” даже не рассматривались, так как нет никакой гарантии что они завтра будут работать. Возможно я плохо искал, или не там и не то искал – не знаю, если у кого-то есть примеры, буду рад помощи. Последним гвоздём в проблему выбора облачного сервиса, стал тот факт, что для работы с Яндекс-диском из c# не нужны вообще никакие сторонние библиотеки.
На каком-то одном из этих трех типов хранения/передачи останавливаться не стал. Было решено сделать поддержку всех трёх, а что конкретно использовать – оставить на выбор пользователя. Ибо ситуации бывают разные – у кого-то порты закрыты и почта не работает, кому-то нельзя ставить программы облачных клиентов и т.д.

Общий алгоритм работы приложения.

Общий алгоритм работы прост как две копейки:

  1. Периодически сохраняем текстовые сообщения со всей нужной информацией в «хранилище»
  2. Периодически читаем сообщения, и показываем в удобном виде.

Перейдем к реализации идей в программном коде.

Получение внешнего адреса.

Тут все просто. В «интернетах» полно всяких сервисов, которые показывают ваш внешний адрес. Если мало уже существующих, то создать еще пару десятков не составит особого труда. Примерный код такой страницы на ASP.NET:

protected void Page_Load(object sender, EventArgs e) { 	LabelIp.Text = HttpContext.Current.Request.UserHostAddress; } 

Вернемся к нашему приложению. Используя класс System.Net.WebClient скачиваем страничку с таким адресом в строку, разбираем её регулярным выражением и получаем нужную нам информацию:

WebClient webClient = new WebClient(); string strExternalIp = webClient.DownloadString("http://checkip.dyndns.org/"); strExternalIp = (new Regex(@"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}")).Matches(strExternalIp)[0].ToString(); 

Получение свойств сетевых интерфесов.

В этом нам поможет класс System.Net.NetworkInformation.NetworkInterface, и его статический метод GetAllNetworkInterfaces(), который возвращает массив элементов своего-же типа NetworkInterface[]. Перебрав этот массив мы можем получить из объекта IPInterfaceProperties всю нужную нам информацию – IP адреса, маски, шлюзы, dns-сервера и т.д.:

NetworkInterface[] adapters = NetworkInterface.GetAllNetworkInterfaces(); // перебираем все сетевые интерфейсы foreach (NetworkInterface nic in adapters) { 	string strInterfaceName = nic.Name;	// наименование интерфейса 	string strPhysicalAddress = nic.GetPhysicalAddress().ToString(); //МАС - адрес 	 	string strAddr = string.Empty;  	// перебираем IP адреса 	IPInterfaceProperties properties = nic.GetIPProperties(); 	foreach (UnicastIPAddressInformation unicast in properties.UnicastAddresses) 	{ 		strAddr = unicast.Address.ToString() + " / " + unicast.IPv4Mask; 	} 	// перебираем днс-сервера 	foreach (IPAddress dnsAddress in properties.DnsAddresses) 	{ 		strAddr = dnsAddress.ToString(); 	} 	// перебираем шлюзы 	foreach (GatewayIPAddressInformation gatewayIpAddressInformation in properties.GatewayAddresses) 	{ 		strAddr = gatewayIpAddressInformation.Address.ToString(); 	} } 

Передача текстового сообщения в «хранилище».

Собрав всю необходимую информацию, отправляем её в «хранилище» в виде обычного текстового файла (в случае с почтой – просто сообщение).
С обычными файлами всё просто:

System.IO.File.WriteAllText("MyInterfaces.txt", strInterfaces); 

С почтой тоже всё решается парой строк кода (метод запросто находится в интернете). Одна из возможных вариаций:

MailMessage mail = new MailMessage { 	From = new MailAddress(strMailAddress), // от кого 	Subject = strSubject,	// тема письма 	Body = strBody,			// тело письма 	IsBodyHtml = false }; mail.To.Add(new MailAddress(Settings.Default.strMailTo)); // кому  SmtpClient client = new SmtpClient { 	Host = strSmtpServer, // адрес SMTP сервера 	Port = nSmtpServerPort,	// порт SMTP сервера 	EnableSsl = isSmtpSsl,	// нужно ли испльзовать SSL 	Credentials = new NetworkCredential(strEmailUserName, strMailPassword), // логин пароль 	DeliveryMethod = SmtpDeliveryMethod.Network }; client.Send(mail); // отправляем mail.Dispose(); 

А вот с облаками немного сложнее, общий смысл – создать правильный веб запрос в который впихнуть передаваемый текст:

// strFilePath - имя и путь к файлу на сервере HttpWebRequest web = (HttpWebRequest)WebRequest.Create("https://webdav.yandex.ru/" + strFilePath); // указываем логин и пароль (дважды!!! в разных местах) web.Credentials = new NetworkCredential("mail@yandex.ru", "password"); web.Headers.Add("Authorization: Basic " + Convert.ToBase64String(Encoding.Unicode.GetBytes("mail@yandex.ru" + ":" + "password"))); web.Accept = "*/*"; web.Method = "PUT"; web.ContentType = "application/binary"; web.ContentLength = buffer.Length; using (Stream myReqStream = web.GetRequestStream()) { 	// strContent - текст передаваемого файла 	byte[] buffer = Encoding.UTF8.GetBytes(strContent);  	myReqStream.Write(buffer, 0, buffer.Length); 	myReqStream.Flush(); } HttpWebResponse resp = (HttpWebResponse)web.GetResponse(); 

Здесь немного пришлось поплясать с кодировками, но методом «научного тыка» было установлено, что с UTF8 всё отлично работает.

Чтение сообщений из «хранилища»

Обычные файлы из обычной файловой системы читаются одной строкой. Но нам ведь не нужен просто один файл, да и имя его заранее может быть не известно, поэтому просматриваем всё содержимое папки, ищем файлы по указанной маске и обрабатываем их по очереди:

// просмотр всех файлов из указанной директории по указанной маске var files = Directory.EnumerateFiles("путь к папке", "*.txt"); strFileNames = files as string[] ?? files.ToArray(); foreach (string strFileName in strFileNames) { 	string message = File.ReadAllText(strFileName); // читаем содержимое файла 	// что-то делаем с прочитанным }  

С чтением почты пришлось повозиться. Код затачивался под гугло-почту, поэтому возможно некорректная работа на других почтовиках. Именно гугло-почта и привела к использованию IMAP сервера (на данный момент хотмэйл этот протокол не поддерживает). Многие советовали использовать псевдо-бесплатную библиотеку (название не буду приводить), которая периодически вместо тела письма возвращала свою рекламу. Но это прямо нарушает «второе пушистое требование» — стабильность, а если платить, то «первое пушистое» – бесплатность. Поэтому я выбрал полностью бесплатную и вполне рабочую библиотеку в которой есть работа с IMAP серверами — «MailSystem.NET». Примеры использования можно найти на странице проекта, здесь же я приведу небольшой кусочек кода для получения письма:

Imap4Client imap = new Imap4Client(); imap.ConnectSsl("imap.gmail.com", 993);	// подключаемся imap.Login("mail@google.com", "password");// авторизуемся Mailbox inbox = imap.SelectMailbox("inbox");// получаем папку входящих int[] nIdsUnread = inbox.Search("UNSEEN");	// получаем только непрочитанные int nUnreadCount = nIdsUnread.Length;	// узнаем количество непрочитанных for (int i = 0; i < nUnreadCount; i++) { 	int idx = nIdsUnread[i]; // получаем индекс письма в папке входящих 	// получаем текст сообщения     Message message = inbox.Fetch.MessageObject(idx); 	// message.Subject - содержит тему письма 	// message.BodyText.Text - содержит текст письма 	// обрабатываем полученную информацию } 

Вот так можно прочитать письмо — всего десять строк кода, но они тянут за собой пять библиотек (DLL) в папку программы, и потом придется тягать их везде с собой.

Читать файлы из облачного хранилища даже проще чем их туда отсылать:

// strFilePath - имя и путь к файлу на сервере HttpWebRequest web = (HttpWebRequest)WebRequest.Create("https://webdav.yandex.ru/" + strFilePath); // указываем логин и пароль web.Credentials = new NetworkCredential("mail@yandex.ru", "password"); web.Headers.Add("Authorization: Basic " + Convert.ToBase64String(Encoding.Unicode.GetBytes("mail@yandex.ru" + ":" + "password"))); web.Accept = "*/*"; web.Method = "GET"; HttpWebResponse resp = (HttpWebResponse)web.GetResponse(); using (StreamReader sr = new StreamReader(resp.GetResponseStream())) { 	string text = sr.ReadToEnd(); 	// text - теперь содержит в себе текстовое содержимое файла } 

Но этот пример прочитает только один файл, а нам нужно читать все файлы из указанной директории. Решается эта задача предварительным запросом списка файлов. Сервер вернет нам XML файл, и пройдясь по содержимому тегов <d:displayname> мы получим список файлов:

// strPath - путь к папке на сервере HttpWebRequest web = (HttpWebRequest)WebRequest.Create("https://webdav.yandex.ru/" + strPath); // указываем логин и пароль web.Credentials = new NetworkCredential("mail@yandex.ru", "password"); web.Headers.Add("Authorization: Basic " + Convert.ToBase64String(Encoding.Unicode.GetBytes("mail@yandex.ru" + ":" + "password"))); web.Accept = "*/*"; web.Headers.Add("Depth: 1"); web.Method = "PROPFIND"; List<string> retValue = new List<string>(); // в этот список попадут все файлы из указанной паки HttpWebResponse resp = (HttpWebResponse)web.GetResponse(); using (StreamReader sr = new StreamReader(resp.GetResponseStream())) { 	// сервер возвращает XML файл. Разбираем его содержимое: 	XmlDocument xmlDoc = new XmlDocument(); 	xmlDoc.LoadXml(sr.ReadToEnd());  	XmlNodeList displaynames = xmlDoc.GetElementsByTagName("d:displayname"); 	int nCount = displaynames.Count; 	for (int i = 1; i < nCount; i++) 	{ 		retValue.Add(displaynames[i].InnerText); 	} } 

DNS

После получения всей информации из хранилища, встает вопрос – а что делать дальше? И вариантов не много:

  1. Показать пользователю всё полученное в удобном виде
  2. Организовать возможность доступа к удаленным компьютерам по имени

С первым всё просто – обычный список и пару колонок. А вот со вторым сложнее. Реализовать можно двумя путями:

  1. Редактирование файла %windir%\system32\drivers\etc\hosts
  2. Создать свой локальный DNS сервер

Первое реализуется довольно просто, файл hosts – это обычный текстовый файл, который без труда читается/изменяется/сохраняется, главное только иметь на это права. А их то у обычного пользователя нет, поэтому повышаем у нашего приложения «уровень управления учетными записями Windows» поставив в файле app.manifest значение для requestedExecutionLevel = requireAdministrator. Подробнее об этом можно прочитать здесь. Работаем с файлом хостов приблизительно так:

//открываем файл хостов string strHosts = File.ReadAllText(Environment.SystemDirectory + "\\drivers\\etc\\hosts"); string[] linesHostsOld = Regex.Split(strHosts, "\r\n|\r|\n"); // разбиваем на строки StringBuilder sbHostsNew = new StringBuilder(); // обрабатываем все строки foreach (string lineHosts in linesHostsOld) {     sbHostsNew.AppendLine(lineHosts); } // добавляем в конец текущие значения хостов sbHostsNew.AppendLine("127.0.0.1  hello.world.com"); // сохраняем файл хостов File.WriteAllText(Environment.SystemDirectory + "\\drivers\\etc\\hosts", sbHostsNew.ToString()); 

Второй вариант у меня не удалось хорошо оттестировать, так-как всех пока-что полностью устраивает работоспособность первого метода. DNS сервер реализован помощи сторонней библиотеки «ARSoft.Tools.Net». Сильно не мудрим, и по этим примерам делаем свои функции, приблизительно так:

DnsServer _server = new DnsServer(IPAddress.Any, 10, 10, ProcessQuery); _server.Start(); // запуск сервера  // запрос адреса у DNS сервера private static DnsMessageBase ProcessQuery( 	DnsMessageBase message,  	IPAddress clientAddress,  	ProtocolType protocol) { 	message.IsQuery = false; 	DnsMessage query = message as DnsMessage;  	if (query != null) 	{ 		if (query.Questions.Count == 1) 		{ 			if (query.Questions[0].RecordType == RecordType.A) 			{ 				if (query.Questions[0].Name.Equals("hello.world.com", StringComparison.InvariantCultureIgnoreCase)) 				{ 					IPAddress ip; 					if (IPAddress.TryParse("127.0.0.1", out ip)) 					{ 						query.ReturnCode = ReturnCode.NoError; 						DnsRecordBase rec = new ARecord(strHostName, 0, ip); 						query.AnswerRecords.Add(rec); 						return message; 					} 				} 			} 		} 	} 	message.ReturnCode = ReturnCode.ServerFailure; 	return message; }  

Готовое приложение.

Собрав вместе всё выше описанное, и добавив вызов нужных процедур по таймеру, получится некое подобие задуманной программы. Остается только доработать всё напильником, привести в божеский вид и можно показывать людям.
Все настройки (а их получилось не мало) приложение хранит в файле %PROGRAM_NAME%.exe.config а сам файл лежит где то в этом районе: %USERPROFILE%\AppData\Local\%PROGRAM_NAME%\***. Реализовано это при помощи стандартных возможностей Properties.Settings.Default. Пароли хранятся там-же, но в зашифрованном виде. Шифрование сделано используя DPAPI (на харбе по этой теме есть статья и вопрос).
Работу с настройками формы, с шифрованием, с таймерами, с параллельными процессами и всего прочего, что не касается изначальной темы я подробно описывать не буду. Кому очень интересно – всегда можно посмотреть исходный код.

Внешний вид получившегося велосипеда:
Четыре скриншота программы




При первом запуске понадобится внимательно настроить все нужные параметры.
В минимальном варианте: на первом компьютере (с динамическим адресом) нужно будет настроить «интерфейсы», а на втором компьютере (на котором нам нужно знать динамический адрес) нужно будет внимательно настроить «хосты».

Планы по развитию.

  • Увеличение поддерживаемых облачных хранилищ.
  • Увеличение поддерживаемых почтовых протоколов
  • Шифрование передаваемой информации.

Исходный код проекта и саму программу пока выложил на Яндекс.Диск.
Исходники можно скачать здесь: http://yadi.sk/d/iZNy9wA28E0-E
Бинарные файлы лежат тут: http://yadi.sk/d/kYpZIqdn8E-ui

На этом всё. Спасибо за внимание.

ссылка на оригинал статьи http://habrahabr.ru/post/191172/