...

пятница, 23 августа 2013 г.

[Из песочницы] Приватный динамический 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 файл, и пройдясь по содержимому тегов мы получим список файлов:

// 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


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


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 fivefilters.org/content-only/faq.php#publishers. Five Filters recommends: 'You Say What You Like, Because They Like What You Say' - http://www.medialens.org/index.php/alerts/alert-archive/alerts-2013/731-you-say-what-you-like-because-they-like-what-you-say.html


Комментариев нет:

Отправить комментарий