ISC DHCPD - разделяем клиентов на классы (классы, группировки, опции, разбор ошибок)

Полезные советы и программы от пользователей нашего форума.

Модератор: Модераторы разделов

Аватара пользователя
kasak
Сообщения: 886
ОС: OpenBSD

ISC DHCPD - разделяем клиентов на классы

Сообщение kasak »

Здравствуй, дорогой читатель!

Вступление

В этой статье я хотел бы, раскрыть некоторые аспекты использования isc dhcpd, поскольку, даже вполне тривиальные задачи, в иных статьях описаны либо неправильно, либо не до конца.
Этим грешат не только рандомные страницы в интернете, но и даже вполне себе официальная документация. Взять хотя бы документацию от восьмой центоси: https://docs.centos.org/en-US/8-docs/advanced-install/assembly_preparing-for-a-network-install/#configuring-a-tftp-server-for-bios-based-clients_preparing-for-a-network-install

Работать то это конечно будет, но в их примере есть лишний код.

И да, наверное в 2020 году уже нужно бы потихоньку двигаться в сторону kea. Однако, это у нас с вами ещё впереди. Сейчас разберёмся над ошибками в dhcpd.

Фабула

Начнём с весьма тривиальной задачи — разделим клиентов по разным адресным пространствам при помощи классов.
Я не знаю почему, но многие не понимают как это делается.
Вот типичный пример https://voxlink.ru/kb/voip-devices-configuration/dhcp-vendor-class/
на который можно наткнуться, если искать информацию об этом в интернетах.

Давайте сначала немного разберёмся в механизме деления на классы, а потом посмотрим что не так сделал автор.

Итак, в dhcpd есть такая вещь как «опции».
Опции могут иметь различный формат, и от этого, принимать различные значения (например опция может быть булева, численная, строковая, или ip адрес)
Некоторые опции задаются сервером, и передаются в последствии клиентам. Некоторые опции передаются от клиента к серверу.
С такими (от клиента к серверу) опциями, очень просто выделить клиента в класс.
Рассмотрим простой пример:

Код: Выделить всё

class "yealink" {
        match if substring (option vendor-class-identifier, 0, 7) = "yealink";
}

class "panasonic" {
        match if substring (option vendor-class-identifier, 0, 9) = "Panasonic";
}
Тут мы создали два класса, с именами yealink и panasonic. Как несложно догадаться, эти классы могут пригодится для выделения ip адресов voip телефонам.

Тут для выделения клиента в класс, мы используем сравнение по выражению:
match if substring. И мы сравниваем vendor class identifier со строкой yealink.
Что же означают цифры? Substring это оператор, который вырезает часть строки:
substring ( строка, отступ, длина ) где строка — то что мы будем вырезать.
Отступ — сколько бит пропустить от начала строки. Длина — сколько бит отрезать.
Стало понятнее?
Клиент посылает нам опцию vendor-class-identifier (она будет разная от разных клиентов)
мы вырезаем из неё часть нужной длины, в первом случае 7 байт, во втором случае 9 байт. Таким образом, можем выделить слово yealink или panasonic, которые и имеют длину 7 байт и 9 байт.
Где же можно посмотреть присылает ли клиент опцию vendor-class-identifier?
В файле dhcpd.leases. Вот типичный пример записи клиента:

Код: Выделить всё

lease 172.16.7.5 {
  starts 5 2020/08/07 08:15:02;
  ends 5 2020/08/07 20:15:02;
  tstp 5 2020/08/07 20:15:02;
  cltt 5 2020/08/07 08:15:02;
  binding state free;
  hardware ethernet bc:c3:42:f8:6e:a0;
  set vendor-class-identifier = "Panasonic";
}
Обратите внимание, сравнение чувствительно к регистру. Panasonic тут с заглавной буквы.
Так же обратите внимание что не все клиенты посылают эту опцию.

Давайте теперь посмотрим на пример посложнее:

Код: Выделить всё

class "pxeclients" {
        match if substring (option vendor-class-identifier, 0, 9) = "PXEClient";
}
        if substring (option vendor-class-identifier, 15, 5) = "00000" {
                filename "bios/lpxelinux.0";
        } elsif substring (option vendor-class-identifier, 15, 5) = "00007" { 
                filename "EFI/grubx64.efi";
        } else {
                filename "pxelinux.0";
        }
Здесь мы анализируем такой же идентификатор, который посылают все pxe клиенты. Чтобы, исходя из этих данных, определить какой загрузчик передать клиентам. Посмотрите, типичная строка, которую передают такие клиенты:

Код: Выделить всё

set vendor-class-identifier = "PXEClient:Arch:00007:UNDI:003016";
Обратите внимание, как я отделил класс от конструкций if.
Почему я так сделал? Я хотел показать вам, что для передачи разных опций клиенту, вовсе не обязательно создавать отдельный класс. Если выделять клиенту отдельный диапазон адресов не нужно, не нужно и объявлять для этого отдельный класс.
Тут у нас выделен класс pxeclients, куда попадут без исключения все клиенты pxe, и их можно выделить в свою подсеть.
А потом выполнена конструкция из операторов if, которая никак не связана с классами клиентов, а уже служит просто для того, чтобы каждому клиенту был передан его загрузчик.

Теперь, когда мы познакомились с базовыми знаниями по поводу классов, давайте рассмотрим как же их применить на практике.
Тут нам помогут директивы allow и deny.
Запомните простую логику:
  • Если для части адресов указана хотя бы одна директива allow, то всё что не разрешено — будет запрещено
  • Если для части адреса указана хотя бы одна директива deny — то всё что не запрещено — будет разрешено.

Код: Выделить всё

subnet 172.16.0.0 netmask 255.240.0.0 {
        pool { range 172.16.7.0 172.16.7.20; allow members of "panasonic"; }
        pool { range 172.16.6.0 172.16.6.150; allow members of "yealink"; }
        pool { range 172.16.10.0 172.16.15.255; deny members of "yealink"; deny members of "panasonic"; }
}
Теперь рассмотрим всё по логике:
Первый промежуток адресов. Разрешено панасоникам. По логике, всем остальным запрещено.
Второй промежуток — разрешено еалинкам. По логике, всем остальным, запрещено.
Третий промежуток — запрещено панасоникам и еалинкам. По логике — всем остальным клиентам использование этого пула будет разрешено!

То есть, все кто не относится к панасоникам и еалинкам — попадёт в третий пул адресов.

Ну вроде же не сложно, и всё по логике, верно?
Давайте теперь рассмотрим какие же ошибки совершают некоторые авторы, и почему нельзя так делать.

Вот по той же ссылочке, куда я уже указывал https://voxlink.ru/kb/voip-devices-configuration/dhcp-vendor-class/

Автор пытается сделать такое сравнение:

Код: Выделить всё

        class «yealink» {
            match if
                substring (option vendor-class-identifier, 0, 4) = «A580» or
                substring (option vendor-class-identifier, 0, 5) = «udhcp» or
                substring (option vendor-class-identifier, 0, 10) = «yealink» or
                substring (option vendor-class-identifier, 0, 4) = «00006=»;
       }
Тут автор допустил ошибку в 3 и 4 сравнении, пытаясь сравнивать строку в 10 байт с 7 байтной. И в четвёртой строке, пытаясь сравнивать 4 байта с 6. Однако, тут всё не так страшно.
Что мы видим дальше:

Код: Выделить всё

#Yealink-Phones
pool {
    range 192.168.180.31 192.168.180.250;
    allow members of «yealink»;
    }

#Not-grouped-clients
pool {
    range 192.168.180.205 192.168.180.215;
    allow unknown-clients;
   }
Если в первом косяке всё не так страшно, то тут уже совсем ложь.
С этим конфигом у наc еалинки свободно попадут в сеть «not-grouped-clients».
Почему? Потому что неправильно используется определение unknown-clients.

Запомните!!! unknown-clients — это клиенты, которые не имеют явного объявления хоста (host declaration!)
Занести клиента в класс — это не значит объявить его! Он всё ещё останется unknown!
В данном случае, все телефоны в классе еалинк, точно так же являются и членами класса unknown-clients!

Объявить клиента — значит создать запись host для него. Вы могли видеть такие записи когда нужно статично присвоить ip адрес клиенту. Давайте возьмём пример, как должны выглядеть записи, чтобы этот конфиг работал:

Код: Выделить всё

host ncd1 { hardware ethernet 0:c0:c3:49:2b:57; }
host ncd4 { hardware ethernet 0:c0:c3:80:fc:32; }
host ncd8 { hardware ethernet 0:c0:c3:22:46:81; }
pool {
    range 192.168.180.205 192.168.180.215;
    deny unknown-clients;
   }
Вот теперь эти три хоста СМОГУТ попасть в адресное пространство 205-215.

Именно так это работает.
Кстати, если вы вдруг задавались вопросом, зачем вообще нужны группы в dhcp, у меня есть ответ. Именно для таких случаев.

Код: Выделить всё

group {
filename "loader1";
host ncd1 { hardware ethernet 0:c0:c3:49:2b:57; }
host ncd4 { hardware ethernet 0:c0:c3:80:fc:32; }
}
group {
filename "loader2";
host ncd2 { hardware ethernet 0:c0:c3:88:2d:81; }
host ncd3 { hardware ethernet 0:c0:c3:00:14:11; }
}
Тут члены группы 1 получат при загрузке один файл, и члены группы два — другой файл.
Я не знаю чем это может быть полезно в реальной жизни, но работает это именно так.

Теперь, когда мы уже получше разобрались в классификации клиентов, и уже умеем выделять для них разный пул адресов, давайте усложним задачу, и попытаемся соединить клиентов, которые НЕ посылают vendor class но при этом обладают общими свойствами (три первых части мак адреса).

Для этого нам потребуется вот такое очень хитрое сравнение:

Код: Выделить всё

match if binary-to-ascii(16, 8, ":", substring(hardware, 1, 3)) = «0:c1:a0»;
Вот тут уже всё сложнее.
Для начала давайте разбираться с binary-to-ascii:

binary-to-ascii ( число1, число2, строка1, строка2 )
этот оператор, берёт данные из «строка2», после чего, откусывает от него по кусочку бит, указанных в «число2», преобразует это в систему счисления, указанную в «число1», после чего записывает результат, разделённый между собой через «строка1».
Да, я знаю, звучит дико, но сейчас всё станет понятно.

Что такое substring (hardware, 1, 3) — это сетевой идентификатор клиента (hardware) от которого откушен первый байт (для ethernet этот байт всегда 1 (единица) а всё остальное — сетевой адрес клиента (link-layer address). Мы откусили ещё три байта, и теперь преобразуем это в удобоваримый формат.

Посмотрите, binary-to-ascii берёт вот эти три байта от substring, берёт первые 8 бит (число 2) преобразует их в 16ричный формат (число1), ставит после всего этого «:» (строка1) и переходит к следующим 8 битам. На выходе мы получим три первых части mac адреса.
Обратите внимание, ноль, это 0 а не 00. И 1 это 1 а не 01. Поэтому если часть адреса содержит числа, начинающиеся с нуля, их нужно обрезать. Например: "0:c1:a0" вместо "00:c1:a0" или "0:fe:a" вместо "00:fe:0a".

Вроде и не сложно, правда?

Последнее, о чём бы я хотел поговорить, это опции.
Как только не издеваются разнообразные авторы над этими опциями.

Я уже говорил, что некоторые опции сервер отправляет клиенту. А некоторые опции клиент отправляет серверу. В любом случае, у опций dhcp есть код, числовой. Людям удобнее работать с символьными именами, нежели с числами. Поэтому, опции dhcp можно объявить. Давайте вернёмся к моему примеру с документацией к centos. Вот пример оттуда:

Код: Выделить всё

option space pxelinux;
option pxelinux.magic code 208 = string;
option pxelinux.configfile code 209 = text;
option pxelinux.pathprefix code 210 = text;
option pxelinux.reboottime code 211 = unsigned integer 32;
option architecture-type code 93 = unsigned integer 16;

subnet 10.0.0.0 netmask 255.255.255.0 {
	option routers 10.0.0.254;
	range 10.0.0.2 10.0.0.253;

	class "pxeclients" {
	  match if substring (option vendor-class-identifier, 0, 9) = "PXEClient";
	  next-server 10.0.0.1;

	  if option architecture-type = 00:07 {
	    filename "uefi/shim.efi";
	    } else {
	    filename "pxelinux/pxelinux.0";
	  }
	}
}
В самом начале примера, это:

Код: Выделить всё

option space pxelinux;
option pxelinux.magic code 208 = string;
option pxelinux.configfile code 209 = text;
option pxelinux.pathprefix code 210 = text;
option pxelinux.reboottime code 211 = unsigned integer 32;
option architecture-type code 93 = unsigned integer 16;
И есть объявление опций.
Тут авторы объявляют целое пространство опций pxelinux, после чего указывают код опции, и через знак «=» объявляют тип данных.

Например architecture-type имеет код 93 и тип — целое число без знака, размером 16 бит.

Теперь хотелось бы указать что меня смущает в этом примере.
Первое — опции эти добры молодцы объявили. А используют из них только одну — architecture-type. Зачем объявлять опции и не использовать их? Ещё и включать это в документацию.
Второе — это сама опция.
Вот тут уже нужно сделать отступление и кинуть камень в огород к тем, кто писал документацию на сам dhcpd. К сожалению, они забыли указать в своей документации список опций, которые они сами же включили в dhcpd! Но!!! Они включили этот список в документацию к kea!

Ура! https://kea.readthedocs.io/en/latest/arm/dhcp4-srv.html#id2

Для dhcpd есть разве что опции без привязки к кодам:

https://kb.isc.org/docs/en/isc-dhcp-44-manual-pages-dhcp-options#STANDARD%20DHCPV4%20OPTIONS

По большей части они совпадают, но кое-что, к сожалению может не совпадать.
Вот например та же опция 93 согласно документации называется pxe-system-type
Так что вышеуказанный пример от centos, можно сократить до этого:

Код: Выделить всё

subnet 10.0.0.0 netmask 255.255.255.0 {
	option routers 10.0.0.254;
	range 10.0.0.2 10.0.0.253;
	next-server 10.0.0.1;

	  if option pxe-system-type = 00:07 {
	    filename "uefi/shim.efi";
	    } else {
	    filename "pxelinux/pxelinux.0";
	  }

}
Согласитесь, конфиг ведь чем короче, тем приятнее читать, не так ли?
Меня кроме этого весьма забавляет попытка сравнивать целочисленную опцию с "00:07" но злые языки говорят что якобы это работает.

Кроме того, внимательный читатель мог заметить, что в начале статьи, я выполнил всю туже самую процедуру распределения, не используя переменную с кодом 93 вообще. Ну, у кого получилось красивше, это конечно вопрос с дополнительным обсуждением. Моя задача была показать вам, как не совершать ошибок.

У dhcpd ещё много возможностей в плане классификации клиентов, однако, в этом руководстве я хотел затронуть те случаи, которые чаще могут понадобится в реальной жизни, и выполнение таких задач тривиально, но почему-то не для всех.

Если где-то нашли ошибку, или информацию трудно читать, если что-то непонятно, пишите об этом в коментариях. Спасибо за внимание!
Linux kasakoff 5.7.15-200.fc32.x86_64 #1 SMP Tue Aug 11 16:36:14 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
Спасибо сказали: