DHCP+Mysql сервер на Python (в работе)
Содержание
Целью данного проекта было:
- Изучение протокола DHCP при работе в сети IPv4
- Изучение Python (немножко более чем с нуля 😉 )
- замена серверу DB2DHCP (мой форк), оригинал здесь, который собирать под новую ОС всё труднее и труднее. Да и не нравится, что бинарник, который нет возможности «поменять прям счас»
- получение работоспособного сервера DHCP с возможностью выборки IP адреса абонента по mac абонента или связке mac свича+порт (Option 82)
- написание очередного велосипеда (О! это моё любимое занятие)
- получение инвайта на Хабрахабр 😉
Результат: работает 😉 Опробовано на ОС FreeBSD и Ubuntu. Теоретически код можно попросить работать под любой ОС, т.к. специфических привязоккоде как будто нет.
Ссылка на репозитарий: https://github.com/donpadlo/dhcp2dbpy , для любителей «потрогать живьем».
Процесс установки, настройки и использования результата «изучения матчасти» много ниже, а далее немножко теории по протоколу 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 сервер о том, что «клиент жив» и пользуется выданными сетевыми настройками. В реализации данного сервера эти пакеты игнорируются.
Формат пакетов
Теперь поподробнее остановимся на каждом формате пакета DHCP
DHCPDISCOVER
Итак, процесс получения IP адреса для устройства начинается с того, что клиент DHCP рассылает широковещательный запрос с порта 68 на 255.255.255.255:67. В этом пакете клиент включает свой MAC адрес, а так-же что именно он хочет получить от DHCP сервера. Структура пакета описана в виде таблицы ниже. Также, приведен коротенький код на Python, при помощи которого я обрабатываю ту или иную опцию.
Позиция | Название значения (общепринятое) | Пример | Представление | Байт | Пояснение | Python код для разбора пакета |
1 | Boot Request | 1 | Hex | 1 | Тип сообщения. 1 — запрос от клиента к серверу, 2 — ответ от сервера клиенту | if data[0]==1: res[«op»]=»DHCPDISCOVER/DHCPREQUEST»; If data[0]==2: res[«op»]=»DHCPOFFER/DHCPACK»; |
2 | Hardware type | 1 | Hex | 1 | Тип аппаратного адреса,в данном протоколе 1 — MAC | if data[1]==1: res[«htype»]=»MAC» |
3 | Hardware adrees length | 6 | Hex | 1 | Длина MAC адреса устройства | res[«hlen»]=data[2] |
4 | Hops | 1 | Hex | 1 | Количество промежуточных маршрутов | res[«hops»]=data[3] |
5 | Transaction ID | 23:cf:de:1d | Hex | 4 | Уникальный идентификатор транзакции. Генерирует клиент в начале операции запроса | res[«xidhex»]=data[4:8].hex() res[«xidbyte»]=data[4:8] |
7 | Second elapsed | 0 | Hex | 4 | Время в секундах с начала процесса получения адреса | res[«secs»]=data[8]*256+data[9] |
9 | Bootp flags | 0 | Hex | 2 | Некие флаги, которые могут устанавливаться, в качестве указания параметров протокола. В данном случае 0 — это тип запроса Unicast | res[«flags»]=pack(‘BB’,data[10],data[11]) |
11 | Client IP address | 0.0.0.0 | Строка | 4 | IP адрес клиента (если есть) | res[«ciaddr»]=socket.inet_ntoa(pack(‘BBBB’,data[12],data[13],data[14],data[15])); |
15 | Your client IP address | 0.0.0.0 | Строка | 4 | IP адрес предложенный сервером (если есть) | res[«yiaddr»]=socket.inet_ntoa(pack(‘BBBB’,data[16],data[17],data[18],data[19])); |
19 | Next server IP address | 0.0.0.0 | Строка | 4 | IP адрес сервера (если известен) | res[«siaddr»]=socket.inet_ntoa(pack(‘BBBB’,data[20],data[21],data[22],data[23])); |
23 | Relay agent IP address | 172.16.114.41 | Строка | 4 | IP адрес агента ретрансляции (например свича) | res[«giaddr»]=socket.inet_ntoa(pack(‘BBBB’,data[24],data[25],data[26],data[27])); |
27 | Client MAC address | 14:d6:4d:a7:c9:55 | Hex | 6 | MAC адрес отправителя пакета (клиента) | res[«chaddr»]=data[28:34].hex() |
31 | Client hardware address padding | Hex | 10 | Зарезервированное место. Обычно забито нулями | ||
41 | Server host name | Строка | 64 | Имя сервера DHCP. Обычно не передается | ||
105 | Boot file name | Строка | 128 | Имя файла на сервере , используемое бездисковыми станциями при загрузке | ||
235 | Magic cookie | 63:82:53:63 | Hex | 4 | «Магическое» число, по которому в т.ч. можно определить, что этот пакет — принадлежит протоколу DHCP | res[«magic_cookie»]=data[236:240] |
Опции DHCP. Могут идти в любом порядке | ||||||
236 | Номер опции | 53 | Dec | 1 | Опция 53, определяющая тип пакета DHCP 1 — DHCPDISCOVER 3 — DHCPREQUEST 2 — DHCPOFFER 5 — DHCPACK 8 — DHCPINFORM |
if data[res[«gpoz»]]==53: res[«option53»]=data[res[«gpoz»]]; ln=data[res[«gpoz»]+1] if data[res[«gpoz»]+2]==1: res[«op»]=»DHCPDISCOVER» if data[res[«gpoz»]+2]==3: res[«op»]=»DHCPREQUEST» if data[res[«gpoz»]+2]==2: res[«op»]=»DHCPOFFER» if data[res[«gpoz»]+2]==5: res[«op»]=»DHCPACK» if data[res[«gpoz»]+2]==8: res[«op»]=»DHCPINFORM» res[«gpoz»]=res[«gpoz»]+ln+2; return res |
Длина опции | 1 | Dec | 1 | |||
Значение опции | 1 | Dec | 1 | |||
Номер опции | 50 | Dec | 1 | Какой IP адрес хочет получить клиент | if data[res[«gpoz»]]==50: res[«option50»]=data[res[«gpoz»]]; ln=data[res[«gpoz»]+1] res[«RequestedIpAddress»]=socket.inet_ntoa(pack(‘BBBB’,data[res[«gpoz»]+2],data[res[«gpoz»]+3],data[res[«gpoz»]+4],data[res[«gpoz»]+5])); res[«gpoz»]=res[«gpoz»]+ln+2; return res |
|
Длина опции | 4 | Dec | 1 | |||
Значение опции | 172.16.134.61 | Строка | 4 | |||
Номер опции | 55 | 1 | Запрашиваемые клиентом сетевые параметры. Состав может быть различным
01 — Маска сети |
if data[res[«gpoz»]]==55: res[«option55»]=data[res[«gpoz»]]; ln=data[res[«gpoz»]+1] preq=0; while preq<ln: if data[res[«gpoz»]+2+preq]==1:res[«ReqListSubnetMask»]=True; if data[res[«gpoz»]+2+preq]==15:res[«ReqListDomainName»]=True; if data[res[«gpoz»]+2+preq]==3:res[«ReqListRouter»]=True; if data[res[«gpoz»]+2+preq]==6:res[«ReqListDNS»]=True; if data[res[«gpoz»]+2+preq]==31:res[«ReqListPerfowmRouterDiscover»]=True; if data[res[«gpoz»]+2+preq]==33:res[«ReqListStaticRoute»]=True; if data[res[«gpoz»]+2+preq]==43:res[«ReqListVendorSpecInfo»]=43; preq=preq+1 res[«gpoz»]=res[«gpoz»]+ln+2; return res |
||
Длина опции | 8 | 1 | ||||
Значение опции | 01:03:06:0c:0f:1c:42:79 | 8 | ||||
Номер опции | 82 | Dec | 1 | Опция 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 |
if data[res[«gpoz»]]==82: res[«option82»]=data[res[«gpoz»]]; ln=data[res[«gpoz»]+1] res[«option_82_AgentCircuitId_len»]=data[res[«gpoz»]+3]; res[«option_82_AgentCircuitId_hex»]=data[res[«gpoz»]+4:res[«gpoz»]+4+res[«option_82_AgentCircuitId_len»]].hex(); res[«option_82_AgentCircuitId_port_hex»]=data[res[«gpoz»]+3+res[«option_82_AgentCircuitId_len»]:res[«gpoz»]+4+res[«option_82_AgentCircuitId_len»]].hex(); res[«option_82_AgentRemoteId_len»]=data[res[«gpoz»]+5+res[«option_82_AgentCircuitId_len»]]; res[«option_82_AgentRemoteId_hex»]=data[res[«gpoz»]+6+res[«option_82_AgentCircuitId_len»]:res[«gpoz»]+6+res[«option_82_AgentCircuitId_len»]+res[«option_82_AgentRemoteId_len»]].hex(); res[«option_82_len»]=ln res[«option_82_byte»]=data[res[«gpoz»]+1:res[«gpoz»]+2+ln]; res[«option_82_hex»]=data[res[«gpoz»]+1:res[«gpoz»]+2+ln].hex() res[«option_82_str»]=str(data[res[«gpoz»]+1:res[«gpoz»]+2+ln]) res[«gpoz»]=res[«gpoz»]+ln+2; return res |
|
Длина опции | 18 | Dec | 1 | |||
Значение опции | 01:06 00:04:00:01:00:04 02:08 00:06:c8:be:19:93:11:48 |
Hex | 12 | |||
Окончание пакета | 255 | Dec | 1 | 255 символизирует окончание пакета |
DHCPOFFER
Как только сервер получает пакет DHCPDISCOVER и если он видит, что может клиенту что-то предложить из запрошенного, то он формирует для него ответ — DHCPDISCOVER. Ответ высылается на порт «откуда пришел», бродкастом, т.к. в этот момент, у клиента еще нет IP адреса, следовательно пакет он может принять, только если он отослан широковещательно. Клиент распознает что это пакет для него по MAC своему адресу внутри пакета, а так-же номеру транзакции, который он генерирует в момент создания первого пакета.
Позиция в пакете | Название значения (общепринятое) | Пример | Представление | Байт | Пояснение |
1 | Boot Request | 1 | Hex | 1 | Тип сообщения. 1 — запрос от клиента к серверу, 2 — ответ от сервера клиенту |
2 | Hardware type | 1 | Hex | 1 | Тип аппаратного адреса,в данном протоколе 1 — MAC |
3 | Hardware adrees length | 6 | Hex | 1 | Длина MAC адреса устройства |
4 | Hops | 1 | Hex | 1 | Количество промежуточных маршрутов |
5 | Transaction ID | 23:cf:de:1d | Hex | 4 | Уникальный идентификатор транзакции. Генерирует клиент в начале операции запроса |
7 | Second elapsed | 0 | Hex | 4 | Время в секундах с начала процесса получения адреса |
9 | Bootp flags | 0 | Hex | 2 | Некие флаги, которые могут устанавливаться, в качестве указания параметров протокола. В данном случае, 0 — означает тип запроса Unicast |
11 | Client IP address | 0.0.0.0 | Строка | 4 | IP адрес клиента (если есть) |
15 | Your client IP address | 172.16.134.61 | Строка | 4 | IP адрес предложенный сервером (если есть) |
19 | Next server IP address | 0.0.0.0 | Строка | 4 | IP адрес сервера (если известен) |
23 | Relay agent IP address | 172.16.114.41 | Строка | 4 | IP адрес агента ретрансляции (например свича) |
27 | Client MAC address | 14:d6:4d:a7:c9:55 | Hex | 6 | MAC адрес отправителя пакета (клиента) |
31 | Client hardware address padding | 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 | Dec | 1 | Опция 53, определяющая тип пакета DHCP 2 — DHCPOFFER |
Длина опции | 1 | Dec | 1 | ||
Значение опции | 2 | Dec | 1 | ||
Номер опции | 1 | Dec | 1 | Опция предлагающая DHCP клиенту маску сети | |
Длина опции | 4 | Dec | 1 | ||
Значение опции | 255.255.224.0 | Строка | 4 | ||
Номер опции | 3 | Dec | 1 | Опция предлагающая DHCP клиенту шлюз по умолчанию | |
Длина опции | 4 | Dec | 1 | ||
Значение опции | 172.16.12.1 | Строка | 4 | ||
Номер опции | 6 | Dec | 1 | Опция предлагающая DHCP клиенту DNS | |
Длина опции | 4 | Dec | 1 | ||
Значение опции | 8.8.8.8 | Строка | 4 | ||
Номер опции | 51 | Dec | 1 | Время жизни выданных сетевых параметров в секундах, через которое DHCP клиент должен запросить их снова | |
Длина опции | 4 | Dec | 1 | ||
Значение опции | 86400 | Dec | 4 | ||
Номер опции | 82 | Dec | 1 | Опция 82, повторяет то что пришло в DHCPDISCOVER | |
Длина опции | 18 | Dec | 1 | ||
Значение опции | 01:08:00:06:00 01:01:00:00:01 02:06:00:03:0f 26:4d:ec |
Dec | 18 | ||
Окончание пакета | 255 | Dec | 1 | 255 символизирует окончание пакета |
DHCPREQUEST
После того, как клиент получит DHCPOFFER, он формирует пакет с запросом сетевых параметров уже не ко всем серверам DHCP в сети, а только к одному конкретному, предложение DHCPOFFER которого, ему наиболее «понравилось». Критерии «понравилось» могут быть различные и зависят от реализации DHCP клиента. Получатель запроса указывается при помощи MAC адреса сервера DHCP. Так-же пакет DHCPREQUEST может быть выслан клиентом и без формирования ранее DHCPDISCOVER, если IP адрес у сервера уже ранее когда-то был получен.
Позиция в пакете | Название значения (общепринятое) | Пример | Представление | Байт | Пояснение |
1 | Boot Request | 1 | Hex | 1 | Тип сообщения. 1 — запрос от клиента к серверу, 2 — ответ от сервера клиенту |
2 | Hardware type | 1 | Hex | 1 | Тип аппаратного адреса,в данном протоколе 1 — MAC |
3 | Hardware adrees length | 6 | Hex | 1 | Длина MAC адреса устройства |
4 | Hops | 1 | Hex | 1 | Количество промежуточных маршрутов |
5 | Transaction ID | 23:cf:de:1d | Hex | 4 | Уникальный идентификатор транзакции. Генерирует клиент в начале операции запроса |
7 | Second elapsed | 0 | Hex | 4 | Время в секундах с начала процесса получения адреса |
9 | Bootp flags | 8000 | Hex | 2 | Некие флаги, которые могут устанавливаться, в качестве указания параметров протокол. В данном случае выставлено «бродкаст» |
11 | Client IP address | 0.0.0.0 | Строка | 4 | IP адрес клиента (если есть) |
15 | Your client IP address | 172.16.134.61 | Строка | 4 | IP адрес предложенный сервером (если есть) |
19 | Next server IP address | 0.0.0.0 | Строка | 4 | IP адрес сервера (если известен) |
23 | Relay agent IP address | 172.16.114.41 | Строка | 4 | IP адрес агента ретрансляции (например свича) |
27 | Client MAC address | 14:d6:4d:a7:c9:55 | Hex | 6 | MAC адрес отправителя пакета (клиента) |
31 | Client hardware address padding | 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 | Dec | 3 | Опция 53, определяющая тип пакета DHCP 3 — DHCPREQUEST |
Длина опции | 1 | Dec | 1 | ||
Значение опции | 3 | Dec | 1 | ||
Номер опции | 61 | Dec | 1 | Идентификатор клиента: 01 (для Ehernet) + MAC адрес клиента | |
Длина опции | 7 | Dec | 1 | ||
Значение опции | 01:2c:ab:25:ff:72:a6 | Hex | 7 | ||
Номер опции | 60 | Dec | «Vendor class identifier». В моем случае сообает версию DHCP клиента. Возможно другие устройства, возвращают что-то другое. Windows например сообщает MSFT 5.0 | ||
Длина опции | 11 | Dec | |||
Значение опции | udhcp 0.9.8 | Строка | |||
Номер опции | 55 | 1 | Запрашиваемые клиентом сетевые параметры. Состав может быть различным
01 — Маска сети |
||
Длина опции | 8 | 1 | |||
Значение опции | 01:03:06:0c:0f:1c:42:79 | 8 | |||
Номер опции | 82 | Dec | 1 | Опция 82, повторяет то что пришло в DHCPDISCOVER | |
Длина опции | 18 | Dec | 1 | ||
Значение опции | 01:08:00:06:00 01:01:00:00:01 02:06:00:03:0f 26:4d:ec |
Dec | 18 | ||
Окончание пакета | 255 | Dec | 1 | 255 символизирует окончание пакета |
DHCPACK
В качестве подтверждения того, что «да точно, это твой IP адрес, и больше я его никому не выдам» от DHCP сервера, служит пакет в формате DHCPACK от сервера клиенту. Он так-же как и остальные пакеты высылается широковещательно. Хотя, в ниже приведенном коде DHCP сервера реализованного на Python, я на всякий случай дублирую любой широковещательный запрос, отправкой пакета на конкретный IP клиента, если он уже известен. Причем DHCP сервер совершенно не волнует, дошел ли до клиента пакет DHCPACK. Если клиент не получает DHCPACK, то через некоторое время он просто повторяет DHCPREQUEST
Позиция в пакете | Название значения (общепринятое) | Пример | Представление | Байт | Пояснение |
1 | Boot Request | 2 | Hex | 1 | Тип сообщения. 1 — запрос от клиента к серверу, 2 — ответ от сервера клиенту |
2 | Hardware type | 1 | Hex | 1 | Тип аппаратного адреса,в данном протоколе 1 — MAC |
3 | Hardware adrees length | 6 | Hex | 1 | Длина MAC адреса устройства |
4 | Hops | 1 | Hex | 1 | Количество промежуточных маршрутов |
5 | Transaction ID | 23:cf:de:1d | Hex | 4 | Уникальный идентификатор транзакции. Генерирует клиент в начале операции запроса |
7 | Second elapsed | 0 | Hex | 4 | Время в секундах с начала процесса получения адреса |
9 | Bootp flags | 8000 | Hex | 2 | Некие флаги, которые могут устанавливаться, в качестве указания параметров протокол. В данном случае выставлено «бродкаст» |
11 | Client IP address | 0.0.0.0 | Строка | 4 | IP адрес клиента (если есть) |
15 | Your client IP address | 172.16.134.61 | Строка | 4 | IP адрес предложенный сервером (если есть) |
19 | Next server IP address | 0.0.0.0 | Строка | 4 | IP адрес сервера (если известен) |
23 | Relay agent IP address | 172.16.114.41 | Строка | 4 | IP адрес агента ретрансляции (например свича) |
27 | Client MAC address | 14:d6:4d:a7:c9:55 | Hex | 6 | MAC адрес отправителя пакета (клиента) |
31 | Client hardware address padding | 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 | Dec | 3 | Опция 53, определяющая тип пакета DHCP 5 — DHCPACK |
Длина опции | 1 | Dec | 1 | ||
Значение опции | 5 | Dec | 1 | ||
Номер опции | 1 | Dec | 1 | Опция предлагающая DHCP клиенту маску сети | |
Длина опции | 4 | Dec | 1 | ||
Значение опции | 255.255.224.0 | Строка | 4 | ||
Номер опции | 3 | Dec | 1 | Опция предлагающая DHCP клиенту шлюз по умолчанию | |
Длина опции | 4 | Dec | 1 | ||
Значение опции | 172.16.12.1 | Строка | 4 | ||
Номер опции | 6 | Dec | 1 | Опция предлагающая DHCP клиенту DNS | |
Длина опции | 4 | Dec | 1 | ||
Значение опции | 8.8.8.8 | Строка | 4 | ||
Номер опции | 51 | Dec | 1 | Время жизни выданных сетевых параметров в секундах, через которое DHCP клиент должен запросить их снова | |
Длина опции | 4 | Dec | 1 | ||
Значение опции | 86400 | Dec | 4 | ||
Номер опции | 82 | Dec | 1 | Опция 82, повторяет то что пришло в DHCPDISCOVER | |
Длина опции | 18 | Dec | 1 | ||
Значение опции | 01:08:00:06:00 01:01:00:00:01 02:06:00:03:0f 26:4d:ec |
Dec | 18 | ||
Окончание пакета | 255 | Dec | 1 | 255 символизирует окончание пакета |
Установка
Установка фактически заключается в установке модулей python необходимых для работы. Предполагется что MySQL уже установлена и настроена.
FreeBSD
1 2 3 |
pkg install python3 python3 -m ensurepip pip3 install mysql-connector |
Ubuntu
1 2 3 |
sudo apt-get install python3 sudo apt-get install pip3 sudo pip3 install mysql-connector |
Создаем БД MySQL, заливаем в неё дамп pydhcp.sql, настраиваем файл конфигурации.
Конфигурация
Все настройки сервера лежат в файле формата xml. Эталонный файл:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
<?xml version="1.0" ?> <config> <dhcpserver> <host>0.0.0.0</host> <broadcast>255.255.255.255</broadcast> <DHCPServer>192.168.0.71</DHCPServer> <LeaseTime>8600</LeaseTime> <ThreadLimit>1</ThreadLimit> <defaultMask>255.255.255.0</defaultMask> <defaultRouter>192.168.0.1</defaultRouter> <defaultDNS>8.8.8.8</defaultDNS> </dhcpserver> <mysql> <host>localhost</host> <username>test</username> <password>test</password> <basename>pydhcp</basename> </mysql> <options> <option>option_82_hex:sw_port1:20:22</option> <option>option_82_hex:sw_port2:16:18</option> <option>option_82_hex:sw_mac:26:40</option> </options> <query> <offer_count>3</offer_count> <offer_1>select ip,mask,router,dns from users where upper(mac)=upper('{option_82_AgentRemoteId_hex}') and upper(port)=upper('{option_82_AgentCircuitId_port_hex}')</offer_1> <offer_2>select ip,mask,router,dns from users where upper(mac)=upper('{sw_mac}') and upper(port)=upper('{sw_port2}')</offer_2> <offer_3>select ip,mask,router,dns from users where upper(mac)=upper('{ClientMacAddress}')</offer_3> <history_sql>insert into history (id,dt,mac,ip,comment) values (null,now(),'{ClientMacAddress}','{RequestedIpAddress}','DHCPACK/INFORM')</history_sql> </query> </config> |
Теперь поподробнее по тегам:
Секция 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. Увидим примерно такой лог:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
--пришел пакет DHCPINFORM на 67 порт,от 0025224ad764 , b'\x91\xa5\xe0\xa3\xa5\xa9-\x8f\x8a' , ('172.30.114.25', 68) {'ClientMacAddress': '0025224ad764', 'ClientMacAddressByte': b'\x00%"J\xd7d', 'HType': 'Ethernet', 'HostName': b'\x91\xa5\xe0\xa3\xa5\xa9-\x8f\x8a', 'ReqListDNS': True, 'ReqListDomainName': True, 'ReqListPerfowmRouterDiscover': True, 'ReqListRouter': True, 'ReqListStaticRoute': True, 'ReqListSubnetMask': True, 'ReqListVendorSpecInfo': 43, 'RequestedIpAddress': '0.0.0.0', 'Vendor': b'MSFT 5.0', 'chaddr': '0025224ad764', 'ciaddr': '172.30.128.13', 'flags': b'\x00\x00', 'giaddr': '172.30.114.25', 'gpoz': 308, 'hlen': 6, 'hops': 1, 'htype': 'MAC', 'magic_cookie': b'c\x82Sc', 'op': 'DHCPINFORM', 'option12': 12, 'option53': 53, 'option55': 55, 'option60': 60, 'option61': 61, 'option82': 82, 'option_82_byte': b'\x12\x01\x06\x00\x04\x00\x01\x00\x06\x02\x08\x00' b'\x06\x00\x1eX\x9e\xb2\xad', 'option_82_hex': '12010600040001000602080006001e589eb2ad', 'option_82_len': 18, 'option_82_str': "b'\\x12\\x01\\x06\\x00\\x04\\x00\\x01\\x00\\x06\\x02\\x08\\x00\\x06\\x00\\x1eX\\x9e\\xb2\\xad'", 'result': False, 'secs': 768, 'siaddr': '0.0.0.0', 'sw_mac': '001e589eb2ad', 'sw_port1': '06', 'xidbyte': b'<\x89}\x8c', 'xidhex': '3c897d8c', 'yiaddr': '0.0.0.0'} |
Соответственно мы можем любую переменную обернуть в {} и она будет использована в SQL запросе.
Запечатлим для истории, что IP адрес клиент получил:
Запуск сервера
./pydhcpdb.py -d -c config.xml
— d режим вывода в консоль DEBUG
— c <имя_файла> конфигурационный файл
Разбор полетов
А теперь подробнее по реализации сервера на Python. Это боль. Python изучался «на лету». Многие моменты сделаны в стиле: «ухты, как-то сделал что работает». Совсем не оптимизированны, и оставлены в таком виде в основном из-за малого опыта разработки на python. Остановлюсь на наиболее интересных моментах реализации сервера в «коде».
Парсер файла конфигурации XML
Используется стандартный модуль Python xml.dom. Вроде бы и просто, но при реализации ощутимо не хватало толковой документации и примеров в сети с использованием данного модуля.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
tree = minidom.parse(gconfig["config_file"]) mconfig=tree.getElementsByTagName("mysql") for 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") for 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.data gconfig["dhcp_ThreadLimit"]=int(elem.getElementsByTagName("ThreadLimit")[0].firstChild.data) gconfig["dhcp_Server"]=elem.getElementsByTagName("DHCPServer")[0].firstChild.data gconfig["dhcp_defaultMask"]=elem.getElementsByTagName("defaultMask")[0].firstChild.data gconfig["dhcp_defaultRouter"]=elem.getElementsByTagName("defaultRouter")[0].firstChild.data gconfig["dhcp_defaultDNS"]=elem.getElementsByTagName("defaultDNS")[0].firstChild.data qconfig=tree.getElementsByTagName("query") for elem in qconfig: gconfig["offer_count"]=elem.getElementsByTagName("offer_count")[0].firstChild.data for num in 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") for elem in options: node=elem.getElementsByTagName("option") for options in node: optionsMod.append(options.firstChild.data) |
Многопоточность
Как ни странно, многопоточность в Python реализована очень понятно и просто.
1 2 3 4 5 6 7 8 9 10 11 |
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() >gconfig["dhcp_ThreadLimit"]: time.sleep(1) # если число уже запущеных потоков больше чем в настройках, ждем пока их станет меньше |
Прием/отправка пакета DHCP
Для того чтобы перехватить пакеты UDP идущие через сетевую карту, нужно «поднять» сокет:
1 2 |
udp_socket = socket.socket(socket.AF_INET,socket.SOCK_DGRAM,socket.IPPROTO_UDP) udp_socket.bind((gconfig["dhcp_host"],67)) |
,где флаги:
- AF_INET — означет что формат адреса будет IP:порт. Может быть еще AF_UNIX — где адрес задается именем файла.
- SOCK_DGRAM — означает, что принимаем не «сырой пакет», а уже прошедший через файревол, и с обрезанным частично пакетом. Т.е. получаем только пакет UDP без «физической» составляющей обертки пакета UDP. Если использовать флаг SOCK_RAW, то необходимо будет еще парсить и это «обертку».
Отправка пакета может быть как бродкастом:
1 2 |
udp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) #переключаем сокет в режим отправки бродкаста rz=udp_socket.sendto(packetack, (gconfig["broadcast"],68)) |
, так и на адрес, «откуда пришел пакет»:
1 2 |
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 и просто байтах:
1 2 |
res["xidhex"]=data[4:8].hex() res["xidbyte"]=data[4:8] |
, упаковать байты в структуру:
1 |
res["flags"]=pack('BB',data[10],data[11]) |
Получить IP из структуры:
1 |
res["ciaddr"]=socket.inet_ntoa(pack('BBBB',data[12],data[13],data[14],data[15])); |
И наоборот:
1 |
res=res+socket.inet_pton(socket.AF_INET, gconfig["dhcp_Server"]) |
На этом всё 😉