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. Общий принцип работы устройства при запросе параметров сети следующий:

  1. Устройство (клиент) рассылает широковещательный UDP запрос (DHCPDISCOVER) по всей сети с запросом «ну кто-нибудь, дайте мне IP адрес». Причем обычно (но не всегда) запрос происходит с 68 порта (источник), а назначение — 67 порт (назначение). Некоторые устройства отправляют пакеты и с 67 порта. Внутри пакета DHCPDISCOVER включен MAC адрес устройства клиента.
  2. Все сервера DHCP находящийеся в сети (а их может быть несколько), формируют для для устройства отправившего DHCPDISCOVER, предложение DHCPOFFER с сетевыми настройками, и так-же широковещательно его отсылает его по сети. Идентификация кому предназначен этот пакет идет по MAC адресу клиента, предоставленного ранее в запросе DHCPDISCOVER.
  3. Клиент принимает пакеты с предложениями сетевых настроек, выбирает наиболее привлекательный (критерии могут быть различными, например в т.ч. и по времени доставки пакета, количестве промежуточных маршрутов), и делает у понравившегося сервера DHCP «официальный запрос» DHCPREQUEST с сетевыми настройками . В этом случае пакет идет уже к конкретному серверу DHCP.
  4. Сервер, получивший 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 — Маска сети
03 — Шлюз
06 — DNS
oc — Имя хоста
0f — имя домена сети
1c — адрес широковещательного запроса (бродкаста)
42 — имя сервера TFTP
79 — Classless Static Route

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 — Маска сети
03 — Шлюз
06 — DNS
oc — Имя хоста
0f — имя домена сети
1c — адрес широковещательного запроса (бродкаста)
42 — имя сервера TFTP
79 — Classless Static Route

  Длина опции 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

pkg install python3
python3 -m ensurepip
pip3 install mysql-connector

Ubuntu

sudo apt-get install python3
sudo apt-get install pip3
sudo pip3 install mysql-connector

Создаем БД MySQL, заливаем в неё дамп pydhcp.sql, настраиваем файл конфигурации.

Конфигурация

Все настройки сервера лежат в файле формата xml. Эталонный файл:

<?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. Увидим примерно такой лог:

--пришел пакет  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. Вроде бы и просто, но при реализации ощутимо не хватало толковой документации и примеров в сети с использованием данного модуля.

    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 реализована очень понятно и просто.

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 идущие через сетевую карту, нужно «поднять» сокет:

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, то необходимо будет еще парсить и это «обертку».

Отправка пакета может быть как бродкастом:

                    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 и просто байтах:

    res["xidhex"]=data[4:8].hex()
    res["xidbyte"]=data[4:8]

, упаковать байты в структуру:

res["flags"]=pack('BB',data[10],data[11])

Получить IP из структуры:

res["ciaddr"]=socket.inet_ntoa(pack('BBBB',data[12],data[13],data[14],data[15]));

И наоборот:

res=res+socket.inet_pton(socket.AF_INET, gconfig["dhcp_Server"])

На этом всё 😉


Жизнь замечательных грибов