Вивчення протоколу DHCP під час роботи в мережі IPv4
Вивчення Python (трохи більш ніж з нуля 😉)
заміна серверу DB2DHCP (мій форк), оригінал тут, Що збирати під нову ОС все важче і важче. Та й не подобається, що бінарник, який немає можливості «поміняти прямо зараз»
отримання працездатного сервера DHCP з можливістю вибірки IP адреси абонента по mac абонента або зв'язування mac свіча+порт (Option 82)
написання чергового велосипеда (О! це моє улюблене заняття)
отримання люлей про свою косорукість на Хабрахабр (а краще інвайту) 😉
Результат: працює 😉 Опробовано на ОС FreeBSD та Ubuntu. Теоретично код можна попросити працювати під будь-який ОС, т.к. специфічних прив'язок у коді начебто немає.
Обережно! Далі багато.
Посилання на репозиторій для аматорів «помацати живцем».
Процес встановлення, налаштування та використання результату «вивчення матчасті» набагато нижчий, а далі трішки теорії за протоколом DHCP. Для себе. І для історії 😉
Трохи теорії
Що таке DHCP
Це мережевий протокол, який дозволяє пристрою дізнатися свою IP адресу (ну й інші параметри на кшталт шлюзу, DNS та іншого) у сервера DHCP. Обмін пакетами відбувається за протоколом UDP. Загальний принцип роботи пристрою за запитом параметрів мережі наступний:
Пристрій (клієнт) розсилає широкомовний UDP запит (DHCPDISCOVER) по всій мережі із запитом "ну хто-небудь, дайте мені IP адресу". Причому зазвичай (але не завжди) запит походить із 68 порту (джерело), а призначення – 67 порт (призначення). Деякі пристрої відправляють пакети з 67 порту. Всередині пакету DHCPDISCOVER включено MAC адресу пристрою клієнта.
Всі сервери DHCP, що знаходяться в мережі (а їх може бути кілька), формують для пристрою DHCPDISCOVER, що відправив, пропозицію DHCPOFFER з мережевими налаштуваннями, і так само широкомовно його відсилає його по мережі. Ідентифікація кому призначений цей пакет йде за MAC адресою клієнта, наданого раніше у запиті DHCPDISCOVER.
Клієнт приймає пакети з пропозиціями мережевих налаштувань, вибирає найбільш привабливий (критерії можуть бути різними, наприклад у т.ч. і за часом доставки пакета, кількості проміжних маршрутів), і робить у сервера DHCP, що сподобався, «офіційний запит» DHCPREQUEST з мережевими налаштуваннями. У цьому випадку пакет вже йде до конкретного сервера DHCP.
Сервер, що отримав DHCPREQUEST, відправляє пакет формату DHCPACK, в якому в черговий раз перераховує мережеві налаштування, призначені для даного клієнта.
Крім того, є пакети DHCPINFORM, які ходять від клієнта, і мета яких поінформувати DHCP сервер про те, що клієнт живий і користується виданими мережними налаштуваннями. У цьому сервері ці пакети ігноруються.
Формат пакетів
Загалом кадр пакету Ethernet виглядає приблизно так:
У нашому випадку ми розглянемо лише дані безпосередньо вмісту UDP, без заголовків протоколів рівнів OSI, а саме структуру DHCP:
DHCPDISCOVER
Отже, процес отримання IP адреси пристрою починається з того, що клієнт DHCP розсилає широкомовний запит з порту 68 на 255.255.255.255:67. У цьому пакеті клієнт включає свою MAC адресу, а також що саме він хоче отримати від DHCP сервера. Структура пакета описана у таблиці нижче.
Таблиця структури пакету DHCPDISCOVER
Позиція у пакеті
Назва значення
Приклад
подання
байт
Пояснення
1
Boot Request
1
Hex
1
Тип повідомлення. 1 - запит від клієнта до сервера, 2 - відповідь від сервера клієнту
2
Тип апаратного забезпечення
1
Hex
1
Тип апаратної адреси, в даному протоколі 1 MAC
3
Hardware adrees length
6
Hex
1
Довжина MAC адреси пристрою
4
Хміль
1
Hex
1
Кількість проміжних маршрутів
5
Ідентифікатор транзакції
23:cf:de:1d
Hex
4
Унікальний ідентифікатор транзакції. Генерує клієнт на початку операції запиту
7
Second elapsed
0
Hex
4
Час у секундах з початку процесу отримання адреси
9
Bootp flags
0
Hex
2
Деякі прапори, які можуть встановлюватися, як зазначення параметрів протоколу
Номер опції
50
грудня
1
Яку IP адресу хоче отримати клієнт
Довжина опції
4
грудня
1
Значення опції
172.16.134.61
Рядок
4
Номер опції
55
1
Мережеві параметри, що запитуються клієнтом. Склад може бути різним
01 - Маска мережі
03 - Шлюз
06 - DNS
oc - Ім'я хоста
0f - ім'я домену мережі
1c - адреса широкомовного запиту (бродкасту)
42 - ім'я сервера TFTP
79 - Classless Static Route
Довжина опції
8
1
Значення опції
01:03:06:0c:0f:1c:42:79
8
Номер опції
82
грудня
Опція 82, в якій передається MAC адресу пристрою – ретранслятора та якісь додаткові значення.
Найчастіше - порт свіча на якому працює кінцевий клієнт DHCP У даній опції "вкладені" додаткові параметри. Перший байт - номер "підопції", другий її довжина, далі її значення.
В даному випадку в опції 82 вкладені підопції:
Agent Circuit ID = 00:04:00:01:00:04, де останні два байти - порт клієнта DHCP з якого надійшов запит
Agent Remote ID = 00:06:c8:be:19:93:11:48 — MAC адресу пристрою ретранслятора DHCP
Довжина опції
18
грудня
Значення опції
01:06
00:04:00:01:00:04
02:08
00:06:c8:be:19:93:11:48
Hex
Закінчення пакету
255
грудня
1
255 символізує закінчення пакету
DHCPOFFER
Як тільки сервер отримує пакет DHCPDISCOVER і якщо він бачить, що може клієнтові щось запропонувати із запитаного, він формує для нього відповідь — DHCPDISCOVER. Відповідь надсилається порт «звідки прийшов», бродкастом, т.к. в цей момент, у клієнта ще немає IP адреси, отже пакет він може прийняти тільки якщо він відісланий широкомовно. Клієнт розпізнає що це пакет для нього за MAC своєю адресою всередині пакета, а також номер транзакції, який він генерує в момент створення першого пакета.
Таблиця структури пакету DHCPOFFER
Позиція у пакеті
Назва значення (загальне)
Приклад
подання
байт
Пояснення
1
Boot Request
1
Hex
1
Тип повідомлення. 1 - запит від клієнта до сервера, 2 - відповідь від сервера клієнту
2
Тип апаратного забезпечення
1
Hex
1
Тип апаратної адреси, в даному протоколі 1 MAC
3
Hardware adrees length
6
Hex
1
Довжина MAC адреси пристрою
4
Хміль
1
Hex
1
Кількість проміжних маршрутів
5
Ідентифікатор транзакції
23:cf:de:1d
Hex
4
Унікальний ідентифікатор транзакції. Генерує клієнт на початку операції запиту
7
Second elapsed
0
Hex
4
Час у секундах з початку процесу отримання адреси
9
Bootp flags
0
Hex
2
Деякі прапори, які можуть встановлюватися, як зазначення параметрів протоколу. В даному випадку 0 означає тип запиту Unicast
Hex
10
Зарезервоване місце. Зазвичай забито нулями
41
Server host name
Рядок
64
Ім'я сервера DHCP. Зазвичай не передається
105
Boot file name
Рядок
128
Ім'я файлу на сервері, яке використовується бездисковими станціями під час завантаження
235
Magic cookie
63: 82: 53: 63
Hex
4
«Магічне» число, яким у т.ч. можна визначити, що цей пакет належить протоколу DHCP
Опції DHCP. Можуть йти у будь-якому порядку
236
Номер опції
53
грудня
1
Опція 53, яка визначає тип пакету DHCP 2 - DHCPOFFER
Довжина опції
1
грудня
1
Значення опції
2
грудня
1
Номер опції
1
грудня
1
Опція, що пропонує DHCP клієнту маску мережі
Довжина опції
4
грудня
1
Значення опції
255.255.224.0
Рядок
4
Номер опції
3
грудня
1
Опція, що пропонує DHCP клієнту шлюз за замовчуванням
Довжина опції
4
грудня
1
Значення опції
172.16.12.1
Рядок
4
Номер опції
6
грудня
1
Опція, що пропонує DHCP клієнту DNS
Довжина опції
4
грудня
1
Значення опції
8.8.8.8
Рядок
4
Номер опції
51
грудня
1
Час життя виданих мережевих параметрів за секунди, через який DHCP клієнт повинен запросити їх знову
Довжина опції
4
грудня
1
Значення опції
86400
грудня
4
Номер опції
82
грудня
1
Опція 82, повторює те, що прийшло в DHCPDISCOVER
Довжина опції
18
грудня
1
Значення опції
01:08:00:06:00
01:01:00:00:01
02:06:00:03:0f
26:4d:ec
грудня
18
Закінчення пакету
255
грудня
1
255 символізує закінчення пакету
DHCPREQUEST
Після того, як клієнт отримає DHCPOFFER, він формує пакет із запитом мережевих параметрів вже не до всіх серверів DHCP у мережі, а лише до одного конкретного, пропозиція DHCPOFFER якого йому найбільше «сподобалася». Критерії «сподобалося» може бути різні і залежить від реалізації DHCP клієнта. Отримувач запиту вказується за допомогою MAC-адреси сервера DHCP. Також пакет DHCPREQUEST може бути висланий клієнтом і без формування раніше DHCPDISCOVER, якщо IP адреса сервера вже раніше колись була отримана.
Таблиця структури пакету DHCPREQUEST
Позиція у пакеті
Назва значення (загальне)
Приклад
подання
байт
Пояснення
1
Boot Request
1
Hex
1
Тип повідомлення. 1 - запит від клієнта до сервера, 2 - відповідь від сервера клієнту
2
Тип апаратного забезпечення
1
Hex
1
Тип апаратної адреси, в даному протоколі 1 MAC
3
Hardware adrees length
6
Hex
1
Довжина MAC адреси пристрою
4
Хміль
1
Hex
1
Кількість проміжних маршрутів
5
Ідентифікатор транзакції
23:cf:de:1d
Hex
4
Унікальний ідентифікатор транзакції. Генерує клієнт на початку операції запиту
7
Second elapsed
0
Hex
4
Час у секундах з початку процесу отримання адреси
9
Bootp flags
8000
Hex
2
Деякі прапори, які можуть встановлюватися, як зазначення параметрів протокол. В даному випадку виставлено «бродкаст»
Hex
10
Зарезервоване місце. Зазвичай забито нулями
41
Server host name
Рядок
64
Ім'я сервера DHCP. Зазвичай не передається
105
Boot file name
Рядок
128
Ім'я файлу на сервері, яке використовується бездисковими станціями під час завантаження
235
Magic cookie
63: 82: 53: 63
Hex
4
«Магічне» число, яким у т.ч. можна визначити, що цей пакет належить протоколу DHCP
Опції DHCP. Можуть йти у будь-якому порядку
236
Номер опції
53
грудня
3
Опція 53, яка визначає тип пакету DHCP 3 - DHCPREQUEST
Довжина опції
1
грудня
1
Значення опції
3
грудня
1
Номер опції
61
грудня
1
Ідентифікатор клієнта: 01 (для Ehernet) + MAC адреса клієнта
Довжина опції
7
грудня
1
Значення опції
01:2c:ab:25:ff:72:a6
Hex
7
Номер опції
60
грудня
"Vendor class identifier". У моєму випадку відповідає версію DHCP клієнта. Можливо інші пристрої, які повертають щось інше. Windows наприклад повідомляє MSFT 5.0
Довжина опції
11
грудня
Значення опції
udhcp 0.9.8
Рядок
Номер опції
55
1
Мережеві параметри, що запитуються клієнтом. Склад може бути різним
01 - Маска мережі
03 - Шлюз
06 - DNS
oc - Ім'я хоста
0f - ім'я домену мережі
1c - адреса широкомовного запиту (бродкасту)
42 - ім'я сервера TFTP
79 - Classless Static Route
Довжина опції
8
1
Значення опції
01:03:06:0c:0f:1c:42:79
8
Номер опції
82
грудня
1
Опція 82, повторює те, що прийшло в DHCPDISCOVER
Довжина опції
18
грудня
1
Значення опції
01:08:00:06:00
01:01:00:00:01
02:06:00:03:0f
26:4d:ec
грудня
18
Закінчення пакету
255
грудня
1
255 символізує закінчення пакету
DHCPACK
Як підтвердження того, що «так, це твоя IP адреса, і більше я його нікому не видам» від DHCP сервера, служить пакет у форматі DHCPACK від сервера клієнту. Він так само як і інші пакети надсилається широкомовно. Хоча, в наведеному нижче коді DHCP сервера реалізованого на Python, я про всяк випадок дублюю будь-який широкомовний запит, відправкою пакета на конкретний IP клієнта, якщо він вже відомий. Причому DHCP сервер не хвилює, чи дійшов до клієнта пакет DHCPACK. Якщо клієнт не отримує DHCPACK, через деякий час він просто повторює DHCPREQUEST
Таблиця структури пакету DHCPACK
Позиція у пакеті
Назва значення (загальне)
Приклад
подання
байт
Пояснення
1
Boot Request
2
Hex
1
Тип повідомлення. 1 - запит від клієнта до сервера, 2 - відповідь від сервера клієнту
2
Тип апаратного забезпечення
1
Hex
1
Тип апаратної адреси, в даному протоколі 1 MAC
3
Hardware adrees length
6
Hex
1
Довжина MAC адреси пристрою
4
Хміль
1
Hex
1
Кількість проміжних маршрутів
5
Ідентифікатор транзакції
23:cf:de:1d
Hex
4
Унікальний ідентифікатор транзакції. Генерує клієнт на початку операції запиту
7
Second elapsed
0
Hex
4
Час у секундах з початку процесу отримання адреси
9
Bootp flags
8000
Hex
2
Деякі прапори, які можуть встановлюватися, як зазначення параметрів протокол. В даному випадку виставлено «бродкаст»
Створюємо БД MySQL, заливаємо в неї дамп pydhcp.sql, налаштовуємо конфігураційний файл.
Конфігурація
Усі налаштування сервера лежать у файлі формату xml. Еталонний файл:
1.0 0.0.0.0 255.255.255.255 192.168.0.71 8600 1 255.255.255.0 192.168.0.1 localhost test test pydhcp option_8.8.8.8_hex:sw_port82:1:20 option_22_hex:sw_port82:2:16 option_18_hex:sw_mac:82:26 40 select ip,mask,router,dns from users where upper(mac)=upper('{option_3_AgentRemoteId_hex}') and upper(port)=upper('{option_1_AgentCircuitId_port_hex}') select ip,mask,router,dns from users where upper(mac)=upper('{sw_mac}') and upper(port)=upper('{sw_port82}') select ip,mask,router,dns from users where upper(mac)=upper('{ClientMacAddress}') insert in historie (id,dt,mac,ip,comment) (null,now(),'{ClientMacAddress}','{RequestedIpAddress}','DHCPACK/INFORM')
Тепер докладніше за тегами:
Секція dhcpserver описує основні налаштування для запуску сервера, а саме:
host — яка IP-адреса слухає сервер на порту 67
broadcast - який ip є бродкастом для DHCPOFFER та DHCPACK
DHCPServer - який ip у DHCP сервера
LeaseTime час оренди виданої IP адреси
ThreadLimit — скільки одночасно потоків запущено по обробці пакетів UDP, що надійшли, на порту 67. Передбачається що допоможе на високонавантажених проектах 😉
defaultMask,defaultRouter,defaultDNS — те, що пропонується абоненту за умовчанням, якщо IP в базі знайдено, але додаткові параметри для нього не вказані
Секція mysql:
host, username, password, basename – все говорить саме за себе. Орієнтовна структура бази даних викладена на GitHub
Секція query: тут описуються запити для отримання OFFER/ACK:
offer_count — кількість рядків із запитами, які повертають результат виду ip,mask,router,dns
offer_n - рядок запиту. Якщо повернення - порожньо, то виконує наступний запит offer
history_sql — запит, який пише наприклад в «історію авторизації» за абонентом
У запитах можуть брати участь будь-які змінні із секції options або опції з протоколу DHCP.
Секція options. Ось тут уже цікавіше. Тут ми можемо створювати змінні, які можемо використовувати надалі в секції query.
Наприклад:
option_82_hex:sw_port1:20:22
, ця строчка-команда взяти весь рядок прийшла в DHCP запиті опції 82, у форматі hex, в діапазоні з 20 по 22 байт включно і покласти її в нову змінну sw_port1 (порт свіча звідки прийшов запит)
option_82_hex:sw_mac:26:40
, випереджаємо змінну sw_mac, взявши hex з діапазону 26:40
Побачити всі можливі опції, які можна використовувати в запитах, можна за допомогою запуску сервера з ключем -d. Побачимо приблизно такий лог:
Відповідно ми можемо будь-яку змінну обернути на {} і вона буде використана в SQL запиті.
Зазначимо для історії, що IP адреса клієнт отримав:
запуск сервера
./pydhcpdb.py -d -c config.xml
- d режим виведення в консоль DEBUG
- c <ім'я_файлу> конфігураційний файл
Розбір польотів
А тепер докладніше щодо реалізації сервера на Python. Це біль. Python вивчався "на льоту". Багато моментів зроблено в стилі: "ухти, якось зробив що працює". Зовсім не оптимізовані, і залишені в такому вигляді переважно через малий досвід розробки на python. Зупинюся на найцікавіших моментах реалізації сервера в коді.
Парсер конфігураційного файлу XML
Використовується стандартний модуль Python xml.dom. Начебто й просто, але при реалізації відчутно не вистачало тямущої документації та прикладів у мережі з використанням даного модуля.
tree = minidom.parse(gconfig["config_file"]) mconfig=tree.getElementsByTagName("mysql") для elem in mconfig: gconfig["mysql_host"]=elem.getElementsByTagName("host")[0].firstChild.data gconfig["mysql_username"]=elem.getElementsByTagName("username")[0].firstChild.data gconfig["mysql_password"]=elem.getElementsByTagName("password")[0].firstChild.data gconfig["mysql_basename"" =elem.getElementsByTagName("basename")[0].firstChild.data dconfig=tree.getElementsByTagName("dhcpserver") для elem in dconfig: gconfig["broadcast"]=elem.getElementsByTagName("broadcast")[0]. firstChild.data gconfig["dhcp_host"]=elem.getElementsByTagName("host")[0].firstChild.data gconfig["dhcp_LeaseTime"]=elem.getElementsByTagName("LeaseTime")[0].firstChild dhcp_ThreadLimit"]=int(elem.getElementsByTagName("ThreadLimit")[0].firstChild.data) gconfig["dhcp_Server"]=elem.getElementsByTagName("DHCPServer")[0].firstChild.data =elem.getElementsByTagName("defaultMask")[0].firstChild.data gconfig["dhcp_defaultRouter"]=elem.getElementsByTagName("defaultRouter")[0].firstChild.data gconfig["dhcp_def defaultDNS")[0].firstChild.data qconfig=tree.getElementsByTagName("query") для elem in qconfig: gconfig["offer_count"]=elem.getElementsByTagName("offer_count")[0].firstChild.data for range(int(gconfig["offer_count"])): gconfig["offer_"+str(num+1)]=elem.getElementsByTagName("offer_"+str(num+1))[0].firstChild.data gconfig ["history_sql"]=elem.getElementsByTagName("history_sql")[0].firstChild.data options=tree.getElementsByTagName("options") для elem in options: node=elem.getElementsByTagName("option") for options in node : optionsMod.append(options.firstChild.data)
Багатопоточність
Як не дивно, багатопоточність у Python реалізована дуже зрозуміло та просто.
def PacketWork(data,addr): ... # реалізація розбору пакета, що прийшов, і відповіді на нього ... while True: data, addr = udp_socket.recvfrom(1024) # чекаємо пакет UDP thread = threading.Thread(target=PacketWork , args=(data,addr,)).start() # як прийшов - запускаємо у фоні визначену раніше функцію PacketWork з параметрами while threading.active_count() вже запущених потоків більше ніж у налаштуваннях, чекаємо поки їх стане менше
Прийом/надсилання пакету DHCP
Для того, щоб перехопити пакети UDP, що йдуть через мережну карту, потрібно «підняти» сокет:
AF_INET - означає, що формат адреси буде IP: порт. Може бути ще AF_UNIX - де адреса задається ім'ям файлу.
SOCK_DGRAM - означає, що приймаємо не "сирий пакет", а вже пройшов через файревол, і з частково обрізаним пакетом. Тобто. отримуємо лише пакет UDP без «фізичної» складової обгортки пакета UDP. Якщо використовувати прапор SOCK_RAW, то необхідно буде ще парсить і це «обгортку».
Відправлення пакета може бути як бродкастом:
udp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) #перемикаємо сокет в режим відправлення бродкасту rz=udp_socket.sendto(packetack, (gconfig["broadcast"],68))
, і на адресу, «звідки прийшов пакет»:
udp_socket.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) # перемикаємо сокет в режим "багато слухачів" rz=udp_socket.sendto(packetack, addr)
, де SOL_SOCKET означає "рівень протоколу" для виставлення опцій,
, SO_BROADCAST опція що пакет шолом «бродкастом»
,SO_REUSEADDR опція перемикає сокет в режим "багато слухачів". По ідеї вона непотрібна в даному випадку, але на одному із серверів FreeBSD, на якому тестував, без цієї опції код не працював.
Розбір пакету DHCP
Ось тут мені справді сподобався Python. Виявляється з «коробки» він дозволяє досить вільно поводиться з байт-кодом. Дозволяючи його дуже просто переводити на десяткові значення, рядки і hex - тобто. те, що нам власне і потрібно, щоб зрозуміти структуру пакета. Так наприклад, можна отримати діапазон байт в HEX і просто байтах: