Привет!
Так получилось, что я пишу на C# микросервис, отвечающий за рассылку писем.
В процессе работы моего сервиса может случится что-то плохое, и нужно сделать рассылку. А для того, чтобы она была более информативная и господа из DevOps были менее раздражены, то в письмо следует добавить IP-адрес, где именно что-то упало.
Но вот проблема - В .NET нет нормального, простого метода или свойства в библиотеке System.Net для простого получения IP-адреса текущего хоста. И тут пришлось начать писать свои «костыли»...
По ходу прочтения поста у Вас возникнут вопросы, почему так, а не иначе. На них я постарался ответить в конце.
Попытка 1:
Окей. Пойдем решать задачу самым очевидным путем, через Google.
Вписываем : Получить ip локального хоста C#.
Получаем список ответов, среди результатов есть документация Microsoft на метод Dns.GetHostEntry, которая намекает по названию, что за нас все давно придумали.
Смотрим документацию и в ходе недолгих поисков находим нужные методы получения IP-адреса указанного хоста:
- GetHostAddresses(string) - для получения списка IP с указанного хоста.
- GetHostName() - для возвращает имя узла локального хоста, где будет запущен микросервис.
Супер! Запускам тест в консоли и получаем:
Почти идеально, но нам не нужны IPv6 адреса.
В документации для этого есть перегрузка метода GetHostAddresses(String, AddressFamily), которая поможет отсечь ненужные IPv6 адреса интерфейсов.
Проваливаемся в AddressFamily и видим enum, благодаря которой мы можем сделать фильтр по схеме адресации.
Отлично! Оно нам и нужно.
Добавляем и запускаем консоль еще раз.
Идем в сетевые настройки на своем устройстве и сверяем c полученным IP в списке консоли.
Вот решение, вот наш IP в списке. Осталось сделать в массиве testResult сделать выбор второго значения (testResult[1]) и получить наш IP хоста.
Но увы нет...
Использование "магических" чисел очень плохое решение.
С большой вероятностью наш метод покажет не верные данные на другом компьютере или сервере, где будет запущен наш будущий метод.
А разработчик, который будет смотреть код, не поймет откуда вязалась вообще эта единица.
Вернемся к полученному списку IP-адресов.
Как мы видим первый вообще IP 192.168.56.1. Что это за IP? Почему он первый? И вообще, что за другие IP адреса у меня?
Другие указанные IP - это иные сетевые интерфейсы на моем компьютере, включая виртуальные.
Для того, чтобы убедиться в этом и посмотреть, что к чему - открываем Powershell вводим командлет и получаем результат:
Get-NetIPAddress | Format-Table
Тут мы и видим все IP из списка.
А теперь глядим на префикс PrefixOrigin. Он показывает, как назначается IP в сетевом интерфейсе.
Получается Wi-Fi интерфейсу IP назначается по DHCP, а остальным - в ручную (Manual).
Давайте ради любопытства глянем, что это за интерфейс назначен на IP 192.168.56.1.
ipconfig /all
Ага, понятно - это интерфейс VirtualBox. Менять его, удалять и т.д. не в ходит в наш план и задачу.
Делаем промежуточные выводы:
1. Хост, на котором будет установлен микросервис и с которого будет браться информация, может содержать тучу виртуальных сетевых интерфейсов (адаптеров), либо вообще один единственный.
2. Судя по всему список формируется как-то рандомно и не понятно мне. Если обратить внимание на полученную таблицу IP-адресов в Powershell, формирование списка игнорирует возрастание по столбцу ifIndex.
Мы получаем рандом почти что.
3. А как такой список формируется на сервере и на других компах например? Было бы классно это выяснить.
Обратившись к коллегам, я получил результаты как это работает на другом компьютере и на Windows сервере, где вероятно будет запущен мой микросервис.
На другом компьютере результат получился почти как у меня:
Windows Server показал вот такие результаты:
Резюмируем:
Наш код вероятно не покажет актуальный IP.
Как делать выборку - не понятно. Каждый хост - это уникальное кол-во сетевых интерфейсов.
Список вообще формируется не по индексу.
Попытка 2:
Из прошлой попытки я понял, что нужно самому филировать IP по полю PrefixOrigin, которое мы получили в Powershell.
Но вот нюанс в том, что из класса Dns и его методов\свойств, не дотянуться до нужного значения.
Идея пришла следующая: если Powershell может провернуть эту операцию используя тоже платформу .NET, то нам просто нужно найти как дотянуться до необходимой информации через C#.
В ходе часового поиска я нашел нужный класс NetworkInterface.
В классе нам нужно будет использовать метод GetIPProperties(), свойство UnicastAddresses, Address, а так же enum другого класса NetworkInformation -> System.Net.NetworkInformation.PrefixOrigin
Теперь код без подробностей:
// формируем массив с интерфейсами
NetworkInterface[] nics = NetworkInterface.GetAllNetworkInterfaces();
// Создаем список с нашими Ip адресами
List<string> list = new List<string>();
// Перебираем сетевые интерфейсы
foreach (NetworkInterface nic in nics)
{
// фильтруем сетевые интерфейсы по префиксу DHCP который нам и нужен
if (nic.GetIPProperties().UnicastAddresses.LastOrDefault()?.PrefixOrigin == System.Net.NetworkInformation.PrefixOrigin.Dhcp)
{
// Узнаем IP в нужного интерфейса и записываем в List.
IPAddress? res = nic.GetIPProperties().UnicastAddresses.LastOrDefault()?.Address;
list.Add(res.ToString());
}
}
// Второй волной заполняем IP с префиксом Manual
foreach (NetworkInterface nic in nics)
{
if (nic.GetIPProperties().UnicastAddresses.LastOrDefault()?.PrefixOrigin == System.Net.NetworkInformation.PrefixOrigin.Manual)
{
IPAddress? res = nic.GetIPProperties().UnicastAddresses.LastOrDefault()?.Address;
list.Add(res.ToString());
}
}
Подробности кода:
Получение списка сетевых интерфейсов:
NetworkInterface[] nics = NetworkInterface.GetAllNetworkInterfaces();
Когда мы используем свойство UnicastAddresses, то получаем list из двух значений:
1- Ipv6 адрес интерфейса.
2- Ipv4 адрес интерфейса.
Назначение таких IP-адресов идет именно по Unicast. В Broadcast и AnyCast Вы не найдете нужного вам значения.
Вкратце об этом тут.
Нас интересует второй из них. Но что бы избавиться от "магических" чисел мы прикрутили .LastOrDefault(), который выберет последнее значение.
Даже если отключить в сетевом интерфейсе IPv6 и получаемое значение останется одно в List, то мы все равно получим нужный IP благодаря .LastOrDefault()
Вот пример почему в данном случае использовать .UnicastAddresses[1].PrefixOrigin было бы плохой идеей.
PrefixOrigin поможет нам в сравнении с NetworkInformation.PrefixOrigin.Dhcp, отфильтровать нужные IP.
nic.GetIPProperties().UnicastAddresses.LastOrDefault()?.PrefixOrigin
Если префикс подошел, то меняем дальше по коду свойство PrefixOrigin на Address для получения IP-адреса интерфейса и записываем результат.
IPAddress? res = nic.GetIPProperties().UnicastAddresses.LastOrDefault()?.Address;
Результат:
Мы получаем, благодаря написанному методу и использованию класса NetworkInterface, нужный результат, который еще и сам сортируется по возрастанию индекса интерфейса (ifIndex), который мы наблюдали в командлете Powershell.
Теперь мы можем с большой вероятностью достать нужный ip первым в списке и использовать его.
Конец.
P.S:
- Двойной foreach сделан мной для удобства понимания кода, тут нет колоссальной работы ресурсов хоста при такой работе.
- Список в начале наполняет DHCP адресами, но если таких нет, как на примере скрина с Window Server, то список наполнится адресами с PrefixOrigin = Manual и вероятно первый же из них будет верным.
- Данная реализация сядет в лужу, если на сервере две и более сетевые платы.
- Проблема произойдет, если на сервере накручены хитрые сетевые настройки, где IP получается по DHCP одним их сетевых интерфейсов, а нужный интерфейс, вообще в Manual почему-то.
Всякие сетевые извращения - это ваши трудности =) - Код не покажет актуальный IP, если устройство находится за роутером. Да, прилетит локальный IP, а не белый IP с границы вашей сети. Обратите на это внимание.
- Данный код рассчитан под выделенные сервера на Azure или YandexCloud и т.д. имеющие белый IP-адрес на интерфейсе. Но не под пункт 3,4,5.
- Почему не интегрировать просто zabbix к работе сервиса? У нас не стоит такой задачи в рамках реализации MVP.
Скажу более: этот проект вероятно и не уйдет далее. - А почему бы не достать IP хоста с какой-то файла в системе?
Отличное решение. Если бы я знал, где и как это провернуть, то обязательно сделал. Добро пожаловать в комментарии с решением) - В проекте используется C# 10 и .NET 6.