Pull to refresh

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

Reading time 11 min
Views 23K
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

На этом всё. Спасибо за внимание.
Tags:
Hubs:
+8
Comments 0
Comments Leave a comment

Articles