Я люблю автоматизировать процесс и писать собственные велосипеды для изучения того или иного материала. Моей новой целью стал DHCP-сервер, который будет выдавать адрес в маленьких сетях, чтобы можно было производить первоначальную настройку оборудования.

В данной статье я расскажу немного про протокол DHCP и некоторые тонкости из bash'а.



Конечный результат


Начнём с конца, чтобы было понятно, за что сражаемся.

Демонстрация работы:



Репозиторий со скриптом: firemoon777/bash-dhcp-server

Изначальная проблема


Необходимая мне настройка производится так: подключаемся напрямую по витой паре к оборудованию, выдаём временный адрес по DHCP, производим настройку уже созданным скриптом. И так десять-двадцать раз подряд.

Многим известный isc-dhcp-server прекрасно справляется со своими обязанностями, но, увы, никак не уведомляет мой скрипт о том, что адрес выдан, поэтому нужно как-то блокировать выполение, пока адрес не выдан.

Решение, вроде бы, на поверхности: пинговать до посинения, пока оборудование не отзовётся:

while ! ping -c1 -W1 "$DHCP" | grep -q "time="
do
    echo "Waiting for $DHCP..."
done

Но в этом решении, определённо, не хватает авантюризма.

Теоретическая часть


Получение адреса при одном DHCP-сервере



Протокол DHCP работает поверх UDP на портах 67 и 68. Сервер работает всегда только на 67, а клиент только на 68. Так как клиент не имеет адреса (имеет адрес 0.0.0.0), рассылка DHCP-пакетов производится шировещательным образом. Т.е. клиент всегда отправляет пакеты на адрес 255.255.255.255:67 с адреса 0.0.0.0:68, а сервер отправляет со своего адреса :67 на адрес 255.255.255.255:68.

Получение адреса клиентом происходит в четыре пакета (DORA):

  1. Клиент выясняет где здесь DHCP-сервер (Discover)
  2. Сервер отзывается и предлагает свой адрес (Offer)
  3. Клиент запрашивает предложенный адрес у конкретного сервера (Request)
  4. Сервер соглашается и выдаёт адрес (Ack)

Визуально схему можно представить так:



Получение адреса при нескольких DHCP-серверах



Когда клиент отправляет Discover, все сервера, которые могут слышать, присылают свой Offer клиенту. Но клиент должен выбрать кого-то одного. Выбор клиента оглашается в сообщении Request опцией 54 (DHCP-сервер), которая содержит IP-адрес предпочтённого DHCP-сервера. Хотя Request отправляется так же всем в сети, реагирует только тот DHCP-сервер, чей IP указан в опции 54.



Содержимое DHCP-пакета


DHCP-пакет состоит из двух частей: постоянной, размером в 236 байт и переменной, которая несёт в себе опции (DHCP Option).

Таблица со всеми полями пакета DHCP из Википедии
Поле Описание Длина (в байтах)
op
Тип сообщения. Например может принимать значения: BOOTREQUEST (0x01, запрос от клиента к серверу) и BOOTREPLY (0x02, ответ от сервера к клиенту).
1
htype
Тип аппаратного адреса. Допустимые значения этого поля определены в RFC 1700 «Assigned Numbers». Например, для MAC-адреса Ethernet это поле принимает значение 0x01.
1
hlen
Длина аппаратного адреса в байтах. Для MAC-адреса Ethernet —0x06.
1
hops
Количество промежуточных маршрутизаторов (так называемых агентов ретрансляции DHCP), через которые прошло сообщение. Клиент устанавливает это поле в 0x00.
1
xid
Уникальный идентификатор транзакции в 4 байта, генерируемый клиентом в начале процесса получения адреса.
4
secs
Время в секундах с момента начала процесса получения адреса. Может не использоваться (в этом случае оно устанавливается в 0x0000).
2
flags
Поле для флагов — специальных параметров протокола DHCP.
2
ciaddr
IP-адрес клиента. Заполняется только в том случае, если клиент уже имеет собственный IP-адрес и способен отвечать на запросы ARP (это возможно, если клиент выполняет процедуру обновления адреса по истечении срока аренды).
4
yiaddr
Новый IP-адрес клиента, предложенный сервером.
4
siaddr
IP-адрес сервера. Возвращается в предложении DHCP (см. ниже).
4
giaddr
IP-адрес агента ретрансляции, если таковой участвовал в процессе доставки сообщения DHCP до сервера.
4
chaddr
Аппаратный адрес (обычно MAC-адрес) клиента.
16
sname
Необязательное имя сервера в виде нуль-терминированной строки.
64
file
Необязательное имя файла на сервере, используемое бездисковыми рабочими станциями при удалённой загрузке. Как и sname, представлено в виде нуль-терминированной строки.
128
options
Поле опций DHCP. Здесь указываются различные дополнительные параметры конфигурации. В начале этого поля указываются четыре особых байта со значениями 99, 130, 83, 99 («волшебные числа»), позволяющие серверу определить наличие этого поля. Поле имеет переменную длину, однако DHCP-клиент должен быть готов принять DHCP-сообщение длиной в 576 байт (в этом сообщении поле options имеет длину 340 байт).
переменная


Список всех опций DHCP в RFC 2132

Опции DHCP кодируются следующим образом:
Номер Длина Данные

Например, параметр 3 (предлагаемый шлюз) со значением 10.0.0.1:
3 4 10 0 0 1

В случае, если нужно передать несколько параметров, длина параметра увеличивается.
Например, в параметре 6 (DNS-сервер) передадим два адреса (1.1.1.1 и 8.8.4.4):
6 8 1 1 1 1 8 8 4 4

Признаком конца поля опций является параметр с номером 255 (0xFF) и длиной 0.

Чаще всего клиент вкладывает параметр 55 (список параметров, которые он хочет получить в ответ) в DHCP Discover, однако, мы имеем право выдать ему не всё, что он запросил.

Практическая часть


Изначально планировалось написать сервер на каком-то более подоходящем для этого языке (Си), однако, это было бы буднично и просто. То ли дело написать скрипт, который будет брать на себя функции dhcp-сервера.

Упрощения


Так как разрабатываемый сервер предполагался к использованию в сетях из двух узлов, соединенных патчом, были приняты следующие упрощения:

  • гарантируется, что один клиент в сети;
  • гарантируется, что больше нет dhcp-серверов в сети
  • запускающий сам решает, какой адрес выдать
  • DHCP Release и DHCP Decline игнорируются

Слушатель


В первую очередь нужно научиться принимать пакеты. Для этого необходим дипломированный сочувственный слушатель, например, nc. Но не всякий nc подойдёт для этих целей. OpenBSD netcat 1.130 с дебиана подходит, а вот 1.105 с Ubuntu уже нет. Запустим nc слушать все UDP-пакеты, прилетающшие на порт 67.

nc -l 0.0.0.0 -up 67 -w0

OpenBSD netcat нужен в том числе из-за ключа -w со значением 0. После получения одного пакета (UDP Broadcast) традиционный nc не принимает больше пакетов, но и не завершается.

Работа с сырыми байтами


В командном интерпретаторе весьма сложно работать с непечатными символами, например с нуль-символом: он его просто игнорирует. А DHCP-пакет содержит множество байт 0x00 (например, поле file). Решение проблемы приходит в виде hex-dump'а:

 nc -l 0.0.0.0 -up 67 -w0 | stdbuf -o0 od -v -w1 -t x1 -An

По одному байту на строку, без вывода адреса, не пропуская повторяющиеся байты. Можно так же приправить stdbuf -o0, чтобы вывод не буферизировался.

Получение, хранение и обработка пакетов


Из stdout команды od байты забираются командой read и складываются в массив.

msg=()
for i in {0..235}; do
	read -r tmp
	msg[$i]=$tmp
done

Хотя все значения передаются в шестнадцатеричной системе счисления, номер DHCP Option и длину опции лучше всего выводить на экран/в логи в привычном десятичном виде. Для этого можно воспользоваться краткой записью bash'a:

$ op=AC
$ echo $((16#$op))
172

Принятый пакет редактируется в соответствии с типом запроса (Discover или Request) и отправляется обратно.

Отправка ответа


Впрочем, отправка пакета не такая уж и простая задача. Сначала нужно конвертировать байты из дампа в сырые байты и отправить сразу всё одним пакетом.

Конвертацию можно сделать утилитой printf с помощью escape-последовательностей. А чтобы ничего не потерялось, записывать байты сразу в файл.

# Зачищаем файл
>/tmp/dhcp.payload
# Записываем начало фиксированной длины
for i in ${msg[*]}; do
	printf "\x$i" >> /tmp/dhcp.payload 	
done

Для отправки так же используется OpenBSD netcat. Однако, если в качестве слушателя версия 1.105 с Ubuntu подходит, то вот для рассылки широковещательных UDP-сообщений не годится: получаем ошибку protocol not available.

cat /tmp/dhcp.payload | nc -ub 255.255.255.255 68 -s $SERVER -p 67 -w0

Ключ -b разрешает отправку широковещательных сообщений и это вторая причина, по которой сервер необходимо запускать из-под суперпользователя.

Что там с ограничениями?


Данный DHCP-сервер разрабатывался при упрощениях типа одного клиента в сети. Однако, он будет работать и при нескольких клиентах. Просто адрес получит быстрейший.



Вывод


Хотя bash-скрипты сложно назвать полноценным языком программирования, тем не менее, при должном желании можно решить даже такие задачи, как выдача IP-адреса в сети без использования специально предназначенного для этого ПО. А решение специфичных задач не только приносит радость, но и новые знания, которые открылись в момент решения.

Источники


  1. DHCP — Википедия
  2. DHCP and BOOTP Parameters — IANA

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


  1. Tortortor
    05.01.2019 17:03

    троллейбус из хлеба, конечно.
    isc-dhcp может писать лог, реагировать на записи в котором как-то более удобно/логично чем это.


  1. mcsimm
    05.01.2019 17:20

    Здравствуйте!
    Подскажите, пожалуйста, а что будет, если есть сетка, и вдруг приходят несколько негодяев, которые втыкают в сетку пару-тройку штук роутеров, или чего-либо ещё, которые сами умеют в DHCP-сервер, и адреса раздают из диапазона 192.168.0.xxx -192.168.xxx.xxx соответствено.
    Как доверчивой сетевухе понять, чьё предложение принять?
    Просто был такой Wi-Fi роутер, NetGear, и были из-за него много проблем.


    1. Firemoon Автор
      05.01.2019 17:25

      Ну… Например, линуксовая утилита для получения адреса, dhclient, имеет ключ (-s ), которым можно явно указать адрес доверенного dhcp-сервера. Вариант.
      Или изменить дефолтный порт (68) на другой.
      Сложно сказать конкретнее.


      1. lioncub
        07.01.2019 19:16

        del, не сюда…


    1. rzerda
      05.01.2019 17:40
      +1

      Буква «S» в названии протокола DHCP означает «безопасность».

      Никак. RFC 2131 вообще не определяет порядок выбора одного предложения из нескольких, это дело клиента. Но после выбора он шлёт широковещательный DHCPREQUEST с указанием своего решения, и его можно увидеть зондом в том же L2 сегменте. Например, в iptables есть модуль u32, которым можно легко и приятно заглядывать внутрь пакетов (мне был нужен DHCPREQUEST + REBIND, но посыл понятен):

      iptables -I INPUT -p udp --sport 68 --dport 67 -d 255.255.255.255 \! -f -m u32 --u32 "0>>22&0x3C@20=0x01:0xFFFFFFFF && 0>>22&0x3C@8&0xFF000000>>24=0x01" -j LOG --log-prefix "DHCP REBINDING detected "

      А из лога уже можно подбирать чем угодно и радировать на базу «у нас завёлся злодей».


      1. kvaps
        06.01.2019 02:14

        Ещё есть специальный dhcpdump


    1. Henry7
      06.01.2019 17:05

      В сети, определенно, появится проблема.
      Инструмент решения проблемы: en.wikipedia.org/wiki/DHCP_snooping


      1. lioncub
        07.01.2019 19:27

        Ещё можно с помощью ACL, если его нет.


    1. q2digger
      06.01.2019 22:23

      В управляемых коммутаторах есть опция «DHCP snooping», позволяет определить на каком порту коммутатора находится «доверенный» DHCP сервер и все ответы на других портах отправлять в блок.


    1. Merkat0r
      07.01.2019 20:38

      роутер это должен присекать. ну и option 82 в помощь ему :)


  1. rzerda
    05.01.2019 17:21

    Хоккей на траве и балет на льду одновременно, вот это я понимаю!

    Смотрели ли в сторону events в ISC DHCPD? Если да, чем не понравилось? Я их существование только что нагуглил, так что интересуюсь на будущее, вдруг доведётся в тех же дисциплинах выступать.


  1. loginsin
    05.01.2019 17:21

    Необходимая мне настройка производится так: подключаемся напрямую по витой паре к оборудованию, выдаём временный адрес по DHCP, производим настройку уже созданным скриптом. И так десять-двадцать раз подряд.
    Многим известный isc-dhcp-server прекрасно справляется со своими обязанностями, но, увы, никак не уведомляет мой скрипт о том, что адрес выдан, поэтому нужно как-то блокировать выполение, пока адрес не выдан.


    Да ну? Пример для сервера.

    Для клиента dhclient:
    -sf script-file
    Path to the network configuration script invoked by dhclient when it gets
    a lease. If unspecified, the default CLIENTBINDIR/dhclient-script is
    used. See dhclient-script(8) for a description of this file.



    1. Firemoon Автор
      05.01.2019 17:27

      Хм. Возможно, я плохо гуглил.
      Но разве данный способ позволяет передавать какие-то ещё динамические параметры в скрипт (например, новый пароль)?


      1. loginsin
        05.01.2019 18:04

        Это инструмент, а как им пользоваться — решать вам.


      1. vurd
        05.01.2019 22:11

        Вот кусок из моего dhcpd.conf. Решал похожую задачу 6-7 лет назад, работает до сих пор. Единственный нюанс, скрипт после запуска лучше отфоркнуть.

        on commit {
        set ClientIP = binary-to-ascii(10, 8, ".", leased-address);
        set ClientMac = binary-to-ascii(16, 8, ":", substring(hardware, 1, 6));
        execute("/usr/bin/perl","/opt/rew/event", ClientIP, ClientMac);
        }


  1. lexore
    05.01.2019 22:36

    Вы не поверите, но уже есть такой на perl.
    http://netlab.dhis.org/wiki/ru:software:perl:dhcp_server


    1. blind_oracle
      06.01.2019 00:34

      Ну перл это полноценный язык программирования, в отличии от.
      Поэтому поверим.


      1. tchspprt
        06.01.2019 01:09
        +1

        Офтопчег: миллиард раз поднималось, пора в миллиард первый.
        Баш, конечно, это оболочка, но по какому критерию это не язык программирования? :) Хоть один аргумент.

        Базовый барьер — полноту по Тьюрингу — баш вполне себе проходит.


        1. blind_oracle
          06.01.2019 01:19

          В практическом контексте данной статьи — нет поддержки системных вызовов.
          Соответственно — нет работы с сетью и подобным.
          Что сильно ограничивает его применение.

          Все остальное — религиозные споры.


          1. tchspprt
            06.01.2019 01:29
            +1

            Таким же макаром какой-нибудь MATLAB — не язык программирования, что для кого-то потенциально звучит как оскорбление.
            С учётом того, что в руках есть GCC + Make — можно всё замечательно «забиндить» в баш.


            1. blind_oracle
              06.01.2019 01:37

              что для кого-то потенциально звучит как оскорбление.
              Честно говоря, мне плевать на чьи-то розовые сопли :) Если кто-то хочет обидеться — он найдет повод.

              Я рассматриваю языки программирования как инструменты для решения задач. Какие-то из них подходят в определенных условиях хуже, какие-то лучше — у каждого своя ниша есть, наверное.

              Шелл очень хорош для простых вещей, либо там куда тащить перл\питон неразумно и\или невозможно. Например — в OpenWRT роутеры с 4Мб флеша, половину из которых займет ядро.

              Но начинать считать его полноценным языком программирования и пытаться написать что-либо серьезное — путь в никуда.

              С учётом того, что в руках есть GCC + Make — можно всё замечательно «забиндить» в баш.
              Это уже будет не баш, а Си.


              1. tchspprt
                06.01.2019 14:11

                Утрированно, по Вашей логике всё что угодно на баше может быть «не баш, а Си». По-крайней мере интерпретаторы баша, написанные не на Си, мне не встречались.


                1. blind_oracle
                  06.01.2019 16:30

                  Утрированно, по Вашей логике всё что угодно на баше может быть «не баш, а Си».
                  Нет. Если ты доработаешь баш и добавишь туда требуемый функционал (полноценная работа с сокетами, опять таки) — это одно, это будет частью языка\интерпретатора.

                  Если же ты просто наколбасишь сбоку какую-то логику на Си для своей программы и будешь ее использовать, это — уже не часть языка. Это как просто вызвать netcat, как у автора данной заметки, и парсить его вывод.

                  Это все религиозные споры — есть инструмент, есть адекватность его использования в тех или иных условиях. А как его классифицировать — как язык программирования или нет, дело третье.

                  Есть еще такое понятие как General-purpose programming language и баша\шелла среди них нет. Его относят к Domain-specific language


                  1. tchspprt
                    06.01.2019 17:02

                    Всё так — Вы разделили понятия UNIX Tools, UNIX shell и bash. И по DSL всё так, споров 0. Хотя есть один — фор экземпл, если учитывать, что DSL — это не ЯП, то Erlang — это не ЯП. Однако та же вики, на которую Вы ссылаетесь, говорит о том, что Erlang — это ЯП. Взаимоисключающие параграфы? Технически, можно ли использовать Erlang абсолютно без OTP (повторюсь — абсолютно, а не в минимальной мало-мальской его сборке), тем самым отвязав его от термина DSL? Что-то мне подсказывает, что нет. В таком случае Erlang — это ЯП или не ЯП?

                    Суть в том, что на чистом баше (без использования UNIX Tools) можно написать машину Тьюринга. На нём же можно «распарсить», интерпретировать и выполнить код любого другого языка (конечно, в рамках совместимости с набором команд процессора, на котором это запущено). Это будет сложно, долго, мучительно, бессмысленно из-за скорости работы конечной поделки… но это будет. Возможно, имея что-то минимально-крошечно-юниксовое, способное только запускать баш, читать из /dev/stdin и писать в /dev/stdout и /dev/stderr, можно написать и запустить UNIX/Linux внутри этого на чистом баше. А может даже и Windows, если совсем сойти с ума.


                    1. tchspprt
                      06.01.2019 17:10

                      И я учёл тот факт, что «полноценной работы с сокетами», например, у самого баша нет. Но при написании описанной мной OS полноценную работу с сокетами можно реализовать, скорее всего, внутри этой OS, как и полноценную поддержку оборудования. Или неполноценную поддержку виртуального оборудования, написанного на том же баше. А может и нельзя и я ничего не смыслю в информатике и вычислительной технике. :)


                  1. tchspprt
                    06.01.2019 19:23

                    Ладно, я понял Вас. Отсутствие возможности в сисколы (и не только) из коробки превращает баш во всего-лишь админоориентированный UI.

                    Вы правы, умываю руки.


          1. slonopotamus
            06.01.2019 02:09

            В bash есть /dev/tcp и /dev/udp. Это не считается за "работу с сетью"?


            1. blind_oracle
              06.01.2019 02:14

              Слушать порты через них нельзя. Так что нет, не считается :)


  1. gatoazul
    05.01.2019 22:47

    Проще было взять Freeradius. Он отлично поддерживает DHCP, а в процессе обработки можно вообще делать все, что угодно, включая вызовы bash и perl.


  1. iig
    05.01.2019 23:26

    Легко гуглятся реализации dhcp на perl и python.


  1. blind_oracle
    06.01.2019 00:39

    За решение проблемы через одно место — плюс :)
    Но можно было решить гораздо проще:

    -6 --dhcp-script=<path>
    
    Whenever a new DHCP lease is created, or an old one destroyed, or a TFTP file transfer completes, the executable specified by this option is run.
    ...
    

    © man dnsmasq


  1. Nalivai
    06.01.2019 02:01
    +1

    Вот шутки про троллейбус шутками, а мне однажды что-то подобное, на ash вообще, для одной эмбеддед железки написанное, спасло серьезное количество времени.


  1. sn00p
    06.01.2019 09:23

    kea-dhcp умеет хуки и еще много чего.