Целью данного проекта было:

  • Изучение протокола DHCP при работе в сети IPv4
  • Изучение Python (немножко более чем с нуля ;) )
  • замена серверу DB2DHCP (мой форк), оригинал здесь, который собирать под новую ОС всё труднее и труднее. Да и не нравится, что бинарник, который нет возможности «поменять прям щас»
  • получение работоспособного сервера DHCP с возможностью выборки IP адреса абонента по mac абонента или связке mac свича+порт (Option 82)
  • написание очередного велосипеда (О! это моё любимое занятие)
  • получение люлей про свою косорукость на Хабрахабр (а лучше инвайта) ;)

Результат:  работает ;) Опробовано на ОС FreeBSD и Ubuntu. Теоретически код можно попросить работать под любой ОС, т.к. специфических привязок в коде как будто нет.
Осторожно! Дальше много.

Ссылка на репозиторий для любителей «потрогать живьем».

Процесс установки, настройки и использования результата «изучения матчасти» много ниже, а  далее немножко теории по протоколу 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 сервер о том, что «клиент жив» и пользуется выданными сетевыми настройками. В реализации данного сервера эти пакеты игнорируются.

Формат пакетов



В целом фрейм пакета 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 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 Некие флаги, которые могут устанавливаться, в качестве указания параметров протокола
11 Client IP address 0.0.0.0 Строка 4 IP адрес клиента (если есть)
15 Your client IP address 0.0.0.0 Строка 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

1 — DHCPDISCOVER
3 — DHCPREQUEST
2 — DHCPOFFER
5 — DHCPACK
8 — DHCPINFORM
  Длина опции 1 Dec 1
  Значение опции 1 Dec 1
  Номер опции 50 Dec 1 Какой IP адрес хочет получить клиент
  Длина опции 4 Dec 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 Dec   Опция 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 Dec  
  Значение опции 01:06
00:04:00:01:00:04
02:08
00:06:c8:be:19:93:11:48
Hex  
  Окончание пакета 255 Dec 1 255 символизирует окончание пакета


DHCPOFFER


Как только сервер получает пакет DHCPDISCOVER и если он видит, что может клиенту что-то предложить из запрошенного, то он формирует для него ответ — DHCPOFFER. Ответ высылается на порт «откуда пришел», бродкастом, т.к. в этот момент, у клиента еще нет IP адреса, следовательно пакет он может принять, только если он отослан широковещательно. Клиент распознает что это пакет для него по MAC своему адресу внутри пакета, а так-же номеру транзакции, который он генерирует в момент создания первого пакета.

Таблица структуры пакета DHCPOFFER
Позиция в пакете Название значения (общепринятое) Пример Представление Байт Пояснение
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 адрес у сервера уже ранее когда-то был получен.

Таблица структуры пакета DHCPREQUEST
Позиция в пакете Название значения (общепринятое) Пример Представление Байт Пояснение
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

Таблица структуры пакета DHCPACK
Позиция в пакете Название значения (общепринятое) Пример Представление Байт Пояснение
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"])

На этом всё ;)

Комментарии (9)


  1. frostspb
    26.03.2019 11:35
    +1

    Не надо коммитить pyc файлы и прочий мусор. Советую донастроить файл .gitignore
    Гитигнор под питон можно посмотреть вот тут github.com/github/gitignore/blob/master/Python.gitignore
    А еще я бы посоветовал — форматировать код по PEP8. Можно использовать автоформаттер, но первые пару раз я бы посоветовал это сделать руками, чтобы в памяти отложилось


    1. donpadlo Автор
      27.03.2019 09:02

      Доброе!
      .gitignore подправил. А по PEP8… Попробовал автоформаттеры, ну «не согласен я с ними». Не привычно на глаз. Оставил так, подправив лишь чуть ;)


  1. eugals
    26.03.2019 14:25

    Вместо ручного разбора содержимого пакетов, удобнее воспользоваться ctypes или готовой библиотекой вроде dpkt или Scapy.
    Ну а для файла конфигураций в Питоне удобнее и естественее использовать JSON или просто модуль config.py, вместо XML.


    1. Deathik
      26.03.2019 19:58

      Последнее время часто вижу ещё YAML-файлы к качестве конфигов — довольно привычно работать и читать.


  1. ArTefT
    26.03.2019 15:44
    +1

    слишком сложно читаются кофниги, в xml


    1. eri
      26.03.2019 23:56

      а в json тяжко пишутся


      1. hardex
        27.03.2019 08:27

        а в yaml все хорошо


        1. dmig
          27.03.2019 09:08

          YAML потребует установки парсера. Для проекта такого размера — overkill.


  1. dmig
    27.03.2019 09:05

    Используйте модуль configparser и конфиг в формате ini — не надо усложнять себе жизнь с XML.