Устройство DLZ (Dynamically Loadable Zones) в BIND 9

Jan 28, 2013 01:34

Десять лет назад для BIND 9 появился DLZ, Dynamically Loadable Zones - механизм, позволяющий описывать данные зон не в обычных текстовых файлах (и конфигах), а брать их из внешних источников непосредственно во время обработки запроса. Например, из SQL-базы или LDAP. Таким образом, сервер заранее не знает, какие у него есть зоны, производительность чуть-чуть ниже (обычные файлы зон целиком есть в памяти, а здесь нужно делать запросы), но зато данные можно обновлять в реальном времени, не требуя долгих перезагрузок зон. Сначала существовал в виде отдельных патчей, затем был интегрирован в состав поставки BIND. С тех пор мало что менялось, на сайте автора осталась документация: http://bind-dlz.sourceforge.net/. Однако, сайт не обновлялся с 2004 года, и вообще сайт (и автор) очень своеобразный - так, оказалось, что на странице по модулю DLZ для MySQL (да и другим) документация больше для программиста - какие методы и функции вызываются.

Статья будет полезна системным администраторам и программистам, желающим расширить/изменить SDLZ-компоненты BIND 9 (предполагается, что начальное знакомство с темой уже имеется, по собственно сайту или какому-нибудь howto по конфигурации DLZ - поэтому здесь будут не азы, а объяснение "вглубь"). Админу нужно понимать некоторые особенности работы модулей DLZ, с точки зрения программирования, потому что конфигурирование, скажем, DLZ_MYSQL и DLZ_POSTGRESQL - довольно нетривиально, и чтобы разобраться с ним, нужно иметь представление об устройстве DLZ. При этом документация на сайте весьма ориентирована на программиста, и «сходу» может оказаться непонятной. Здесь в основном пересказываются доступным языком фрагменты документации с сайта DLZ (например, описание работы dlz_mysql), а также текстовые файлы с описанием API для программиста, распространявшиеся в предыдущих версиях DLZ (например, из архива DLZ-0.7.0.tar.gz - программисту, после чтения этой статьи, стоит прочитать и те документы). Кроме этого, здесь описываются некоторые другие особенности программирования внутренностей BIND 9 (с документацией разработчика в ISC вообще всё плохо), и наконец, в качестве примера описывается создание своего собственного SDLZ-модуля DLZ_WILDCARD (полный код доступен), который может использоваться и администраторами для отдачи статической зоны по шаблону FQDN (инструкция по применению ниже). Но обо всём по порядку.


Непосредственно DLZ для программиста довольно сложен/неудобен, поэтому поверх него сделан слой SDLZ (Simple DLZ) - на базе которого собственно и написаны драйверы (бэкенды) DLZ (т.е. для BDB, LDAP, MySQL, FS и др.). Программист драйвера пишет методы, которые будут вызываться слоем SDLZ, а SDLZ, в свою очередь, предоставляет программисту несколько функций (помимо обычных соглашений о программировании ISC/BIND), вот таких (см. sdlz.h):

dns_sdlzregister(drivername, methods, driverarg, flags, mctx, sdlzimp);
dns_sdlzunregister(sdlzimp);
dns_sdlz_putnamedrr(allnodes, name, type, ttl, data);
dns_sdlz_putrr(lookup, type, ttl, data);
dns_sdlz_putsoa(lookup, mname, rname, serial);
Программист заполняет структуру dns_sdlzmethods_t ссылками на определенные драйвером методы - требуются findzone() и lookup(), остальные необязательны (новые методы, т.е. "лишние" относительно здесь описываемых NULL-поля, появились для динамических апдейтов и т.п., см. http://jpmens.net/2011/01/21/bind-gets-a-new-updateable-dlz-driver-dlopen/):
  • create(dlzname, argc, argv[], driverarg, dbdata) - сюда передаются параметры из директивы конфига dlz, см. ниже
  • destroy(driverarg, dbdata) - самоочевиден
  • findzone(driverarg, dbdata, name) - есть или нет зона с именем name (аналог: если бы была определена zone в named.conf)
  • lookup(zone, name, driverarg, dbdata, lookup) - передается имя зоны и имя хоста (в т.ч. '@' и '*'), драйвер возвращает RR-запись, если найдена
  • allnodes(zone, driverarg, dbdata, allnodes) - все записи в зоне, предназначен для трансфера зоны
  • authority(zone, driverarg, dbdata, lookup) - используется только если lookup() не возвращает SOA и NS в ответ на запрос записи '@' (т.н. zone apex)
  • allowzonexfr(driverarg, dbdata, name, client) - определяет, разрешен ли трансфер зоны клиенту с переданным IP-адресом
Нужно заметить, что, например, в SQL-модулях работа того же, скажем, authority() будет зависеть от SQL-запросов в конфигурации драйвера - которые, в свою очередь зависят от схемы базы. То есть, в зависимости от конфигурации, метод authority() может и как бы отсутствовать - причем даже при той же схеме базы внешне одинаковое поведение сервера может быть достигнуто разными способами (SQL-запросами). Здесь нужно посидеть и вдумчиво разобраться, что будет удобнее в конкретном случае - гибкость DLZ предполагает, что BIND подстраивается под уже имеющуюся базу, а не наоборот.

Конфигурируются DLZ-модули, в общем виде, вот такими директивами в named.conf:

dlz "load_name" {
database "driver_name parameter_list {grouped list} more parameters { more grouped }";
};
Всё, что между двойными кавычками после слова database - это «командная строка» драйвера, абсолютно аналогичная по смыслу командной строке любой программы, и передается драйверу в таких же классических переменных argc и argv[]. Но есть отличия. В шелле для задания аргументов с пробелами и пр. символами (чтобы несколько слов попали в один aргумент, а не были разбиты на несколько) применяются одинарные и двойные кавычки. Поскольку в SQL-запросах они имеют специальное значение (а еще они к тому же уже заняты основным парсером конфига BIND), для удобства, чтобы не иметь головную боль с эскейпингом, DLZ использует для этого фигурные скобки (интуитивно понятные знакомым с Tcl). Таким образом, в примере выше драйверу будет передано 6 вот таких аргументов:

argc: 0 argv: "driver_name" (реальные значения всех argv, разумеется, без кавычек)
argc: 1 argv: "parameter_list"
argc: 2 argv: "grouped list"
argc: 3 argv: "more"
argc: 4 argv: "parameters"
argc: 5 argv: " more grouped " (обратите внимание на дополнительные пробелы).

После того, как SDLZ-драйвер был «создан», BIND может начинать вызывать его методы, главным образом findzone() - который может быть вызван более чем один раз на запрос. Хотя для некоторых запросов findzone() может вообще не вызываться, чаще всего - для большинства запросов - будет вызван именно он. А потому findzone() должен работать как можно быстрее.

По завершению работы драйвера будет, понятно, вызван метод destroy(), который может выполнять, например, закрытие соединения к базе и т.п. вещи. Об этом можно было бы и не говорить, если не принимать во внимание тот факт, что завершение работы драйвера происходит при любой перезагрузке или переконфигурации BIND (rndc reload / rndc reconfig), и в это время в памяти существуют две копии каждого view - старый view будет прибит после успешной загрузки нового view. И вот в каждом view может существовать только один экземпляр SDLZ-драйвера - а в такие моменты получается, что их на короткое время таки два. Это может быть важным при задании лимитов на количество одновременных соединений к базе, например. К сожалению, до вызова метода destroy() нет никакого способа выяснить, какой из работающих сейчас старый, а какой новый.

Порядок поиска записей

На большинство получаемых запросов BIND реагирует поиском в своих таблицах в памяти - а все описанные в текстовых файлах зоны целиком грузятся в память (для быстроты поиска используются RB-деревья). В общем случае, после этого BIND спросит метод findzone() - а нет ли у того более подходящего ответа. BIND начинает с наиболее длинного возможного имени зоны, и если findzone() отвечает, что зона не найдена, BIND будет последовательно спрашивать, укорачивая имя зоны, пока не кончится имя, пока findzone() не найдет что-нибудь, или пока имя не станет короче, чем то, что нашлось в таблицах в памяти у самого BIND. Если findzone() нашел совпадение, то BIND будет использовать (S)DLZ-драйвер и проигнорирует свои таблицы в памяти. Единственный случай, когда findzone() НЕ будет вызываться (совсем) - когда свои таблицы памяти BIND уже выдали так называемый «perfect match», то есть имя в DNS-запросе совпадает с именем зоны, и не остается ничего лишнего.

Проиллюстрируем примером. DNS-сервер получает запрос «my.long.name.com». Возможная последовательность событий:
  1. BIND проверяет таблицы в памяти и ничего не находит.
  2. BIND спрашивает findzone() насчет «my.long.name.com», findzone() ничего не находит.
  3. BIND спрашивает findzone() насчет «long.name.com», findzone() ничего не находит.
  4. BIND спрашивает findzone() насчет «name.com», findzone() ничего не находит.
  5. BIND спрашивает findzone() насчет «com», findzone() ничего не находит.
  6. BIND не имеет информации о «my.long.name.com» и отвечает на запрос соответственно.
Другая возможная последовательность событий:
  1. BIND проверяет таблицы в памяти и обнаруживает совпадение - есть зона «name.com».
  2. BIND спрашивает findzone() насчет «my.long.name.com», findzone() ничего не находит.
  3. BIND спрашивает findzone() насчет «long.name.com». На этот раз findzone() ответил положительно. Далее BIND использует (S)DLZ-драйвер для ответа на запрос по этой зоне.
Еще одна возможная последовательность событий:
  1. BIND проверяет таблицы в памяти и обнаруживает совпадение - есть зона «long.name.com».
  2. BIND спрашивает findzone() насчет «my.long.name.com», findzone() ничего не находит. Тогда BIND использует свои таблицы в памяти для ответа на запрос по этой зоне.
Возможна и вот такая последовательность событий:
  1. BIND проверяет таблицы в памяти и обнаруживает точное совпадение - есть зона «my.long.name.com». BIND сразу использует свои таблицы в памяти для ответа на запрос по этой
    зоне.

Далее используемые функции SDLZ

Для получения информации о конкретных записях BIND вызывает метод lookup(). Аргументами будет передана зона, имя записи, а также аргументы "driverarg", "dbdata" и "lookup". Последний требуется сугубо для передачи в API-функции dns_sdlz_putrr() и dns_sdlz_putsoa(), которые драйвер должен вызвать для передачи данных обратно в BIND. Нужно помнить, что передаваемое в lookup() имя будет абсолютным или относительным в зависимости от флагов драйвера при его регистрации (создании). Все 8 драйверов в поставке BIND 9.7 и 9.8 используют относительное.

Если BIND искал имя хоста и не нашел его, он повторно вызовет метод lookup(), передав ему «wild card»-имя - то есть звездочку «*». Для ряда драйверов это означает, что в запросах к базе подстановка %record% будет заменена на «*».

Документация на сайте рассказывает о подстановках с символами процента, типа %zone% - в действительности же используется не он, а символ доллара, т.е. получаемся $zone$. Кажется, оно и правда было изначально с процентами, но потом было исправлено по просьбе людей, которые обламывались с LIKE в SQL-запросах.
Метод authority() нужен тогда, и только тогда, когда lookup() не добавляет записи типов SOA и NS в информацию о самой зоне (zone apex). Если же lookup() их возвращает, тогда указатель на authority() лучше оставить NULL. Назначение метода - сугубо вернуть SOA и NS для указанной зоны. Остальные аргументы такие же, как и для lookup() (кроме имени хоста), возвращаемые данные будут абсолютными или относительными в зависимости от того же флага при регистрации драйвера.

Методы allnodes() и allowzonexfr() требуются для поддержки трансферов зоны - драйверам, генерирующим данные динамически, их определять не рекомендуется, потому что данные тут же устареют. BIND передает аргументами, как обычно, имя зоны, "driverarg", "dbdata" и "allnodes". Последний нужен сугубо для передачи обратно в API-функцию dns_sdlz_putnamedrr(), которую и нужно вызвать, чтобы передать сведения обо всех записях в зоне.

Перед тем, как делать трансфер зоны, BIND сначала проверит свои таблицы в памяти и использует традиционный способ для определения, разрешено ли этому клиенту делать трансфер зоны. Если совпадение нашлось, консультация DLZ-драйвера не требуется, и только если совпадения не было, будет вызван метод allowzonexfr(). Если метод вернул ISC_R_SUCCESS, тогда будет вызвана allnodes() для собственно трансфера.

Таковы методы, а теперь рассмотрим те API-функции SDLZ, которые вызываются в методах, чтобы отдать в BIND собственно данные.

Если представлять себе работу SDLZ-драйвера на примере обращения к SQL-базе, то проще всего начать рассмотрение с метода allnodes() (и соответствующего запроса). Метод просто проходит по всем строкам таблицы БД и вызывает для каждого функцию с 5 параметрами:

dns_sdlz_putnamedrr(allnodes, name, type, ttl, data)
Первый параметр "allnodes" предназначен для внутреннего использования BIND, его нужно просто передать обратно таким же, как он передан в вызов метода. Затем "name" - имя хоста записи. Затем "type" - DNS-тип записи. Параметр "ttl" имеет самоочевидное название, и наконец остается "data" - принимающий всю остальную информацию о записи в виде текстовой строки. Строка, разумеется, должна быть в правильном формате BIND - таком же, как в файлах зон, соответственно, допустимо форматировать этот текст любым количеством пробелов. Соответственно, SQL-драйверы, например, просто конкатенируют в эту строку все правые столбцы, разделяя их пробелами, неважно, есть они, NULL-ли они… Например, вызовы из-за NULL-значений могли получиться такими:

dns_sdlz_putnamedrr(allnodes, "@", "MX", 3600, "20 mail ")
dns_sdlz_putnamedrr(allnodes, "@", "NS", 3600, " NS1 ")
BIND прекрасно всё поймет, несмотря на пробелы слева и справа. Тут важнее напомнить скорее про относительность данных: допустим, была передана зона «example.com», тогда вызов

dns_sdlz_putnamedrr(allnodes, "@", "NS", 3600, " NS1 ")
сообщит BIND, что имя "NS1.example.com". Но если в аргумент data пошло значение NS1.domain.com:

dns_sdlz_putnamedrr(allnodes, "@", "NS", 3600, " NS1.domain.com ")
то имя, отдаваемое BIND, станет уже "NS1.domain.com.example.com". Это корректное поведение, хотя человек, скорее всего имел в виду другое, решаемое заданием абсолютного имени с точкой:

dns_sdlz_putnamedrr(allnodes, "@", "NS", 3600, " NS1.domain.com. ")
Здесь BIND отдаст имя DNS-сервера уже "NS1.domain.com" - в общем, всё как и в файлах зон, об этом просто надо помнить.

Другая API-функция может вызываться из метода lookup(), когда имя записи уже известно (в передаваемом обратно из метода параметре lookup). Использоваться она может так:

dns_sdlz_putrr(lookup, type, ttl, data)
dns_sdlz_putrr(lookup, "a", 86400, "your_data_field_here")
dns_sdlz_putrr(lookup, "dns_type_here", 86400, "data_field_here")
dns_sdlz_putrr(lookup, "dns_type_here", "ttl_field_here", "data_field_here")
dns_sdlz_putrr(lookup, "dns_type", "ttl_here", "concatenated_fields_here")
Всё, сказанное ранее про поле data, справедливо и здесь. Капитан Очевидность напоминает, что dns_sdlz_putrr() может вызываться не только из метода lookup(), но и из authority(), и что записей с одним именем может существовать несколько (dns_sdlz_putrr() может потребоваться вызвать несколько раз).

Для удобства существует еще такая функция:

dns_sdlz_putsoa(lookup, mname, rname, serial)
которая представляет собой вот такую обертку:

n = snprintf(str, sizeof str, "%s %s %u %u %u %u %u",
mname, rname, serial,
SDLZ_DEFAULT_REFRESH, SDLZ_DEFAULT_RETRY,
SDLZ_DEFAULT_EXPIRE, SDLZ_DEFAULT_MINIMUM);
return (dns_sdlz_putrr(lookup, "SOA", SDLZ_DEFAULT_TTL, str));
В SDLZ есть и некоторые другие вспомогательные функции, которые были сделаны для SQL-драйверов, но их можно вызывать и самому, поскольку они вынесены в общее API. Например, для разбора строк вида «параметр=значение» имеется:

#define getParameterValue(x,y) sdlzh_get_parameter_value(ns_g_mctx, (x), (y))
Применяется вот так:

char *tmp = getParameterValue(argv[1], "port=");
...
isc_mem_free(ns_g_mctx, tmp);
При этом значение не может содержать пробелы (первый же пробел считается концом) и не может быть длиннее 255 байт.

Для подстановок вида $client$, $zone$ и $record$ (то, что передается параметром name в lookup()) доступны три функции:

#define build_querystring sdlzh_build_querystring
#define build_sqldbinstance sdlzh_build_sqldbinstance
#define destroy_sqldbinstance sdlzh_destroy_sqldbinstance
Они оперируют типом dbinstance_t, хранящим несколько запросов, и потому не очень универсальны, поэтому примеры здесь не приводятся, их лучше смотреть в коде одного из SQL-драйверов (и там же особенности работы в несколько потоков).

Некоторые API-функции самого BIND

Не следует забывать, конечно, о необходимости использования API самого BIND вместо обычных функций, где это требуется - запись логов, выделение памяти, связные списки и т.д. В основном их использование интуитивно понятно, но, к сожалению, не везде. Так, вменяемая документация по менеджменту памяти в libisc мне не попадалась, а doxygen у них очень куцый. Пришлось немного порыться по исходникам в lib/isc/mem.c, краткий обзор дал такую картину.

Используется понятие так называемого контекста памяти, обычно называемого в коде mctx. Контекст предназначен, в основном, для целей ведения учета и статистики, а также возможности использования, например, другого аллокатора памяти. В BIND их можно использовать два: один системный, т.е. malloc() / free() из libc, другой свой, оптимизированный на некоторые паттерны использования (работает лучше обычного на однопроцессорных системах, но хуже на многопроцессорных). Самому контекст обычно создавать не следует, но стоит знать, что создается он функциями isc_mem_create*, которые в конечном итоге сводятся к вызову:

isc_result_t
isc_mem_createx2(size_t max_size, size_t target_size,
isc_memalloc_t memalloc, isc_memfree_t memfree,
void *arg, isc_mem_t **mctxp, unsigned int flags);
Аргументами memalloc и memfree контекст получает ссылки на функции аллокатора. В BIND 9.8 это практически всегда default_memalloc/default_memfree, которые вызовут стандартные malloc() / free() из libc. Реализация своего аллокатора в этой версии сделана таким способом: если в опциях компиляции указан свой аллокатор, то во флаги вызова остальных функций семейства isc_mem_create* (а isc_mem_createx2() почти не вызывается сам) добавляется флаг ISC_MEMFLAG_INTERNAL, который означает использование аргументов max_size and target_size. Запросы памяти размером более max_size передаются в системный алокатор, а запросы менее выделяются из своих собственных списков блоков - у системного аллокатора при этом запрашиваются блоки размером target_size.

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

Кроме собственно полезной работы, часть из перечисленных ниже действий может отсутствовать - BIND делает различные проверки и отладку, часть этого включена при компиляции по умолчанию, часть нет. Проще описывать API-функции небольшими фрагментами кода из их реализации, а потому сначала нужно описать две внутренние вспомогательные функции (стоит обратить внимание, что размер передается в обе, т.е. и при освобождении памяти тоже):
  • mem_get(isc__mem_t *ctx, size_t size) - делает memalloc размером на 1 байт больше, memset() этого байта и/или остальных
  • mem_put(isc__mem_t *ctx, void *mem, size_t size) - делает memfree, проверка на переполнение (неизмененности лишнего байта), memset() для поиска use after free
Стоит отметить, что системный аллокатор обычно округляет запрошенный размер вверх, до кратности степени двойки, так что увеличение размера на 1 байт (а сборка с этой опцией ISC_MEM_CHECKOVERRUN производится по умолчанию) может привести (если запрос уже имел кратный размер), в среднем, к расходованию впустую многих десятков байт - при большом количестве запросов потери могут быть и велики (подробнее про работу аллокаторов см. « Управление памятью в сетевой подсистеме и ядре FreeBSD в целом»).

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

void isc_mem_attach(isc_mem_t *source, isc_mem_t **targetp);

Эквивалентна коду:

source->references++;
*targetp = (isc_mem_t *)source;
Для того, чтобы использовать на практике все эти фичи трассировки, статистики и др. для поиска утечек памяти (действительно, с ними реальную утечку получилось найти сравнительно быстро) - нужно запускать named в отладочном (-g, см. ниже) режиме с ключом -m - правда, комбинацию флагов к нему, при которой BIND не падает еще при старте, придется подобрать к своей версии самостоятельно :-)
void isc_mem_detach(isc_mem_t **ctxp);

Эквивалентна коду:

ctx = (isc__mem_t *)*ctxp;
ctx->references--;
if (ctx->references == 0)
destroy(ctx);
*ctxp = NULL;
void *isc_mem_get(isc_mem_t *ctx, size_t size);

Вызывает mem_get, ведет статистику, трассирует вызовы (кем вызвано выделение), обрабатывает настройки watermark для контекста памяти.

void isc_mem_put(isc_mem_t *ctx, void *ptr, size_t size);

Вызывает mem_put, ведет статистику, трассирует вызовы (кем вызвано освобождение), обрабатывает настройки watermark для контекста (стоит обратить внимание, что размер передается и сюда, т.е. и при освобождении выделенного с помощью isc_mem_get() тоже надо помнить и передавать размер области).

void *isc_mem_allocate(isc_mem_t *ctx, size_t size);

Вызывает либо mem_get либо соответствующую функцию при определенном флаге ISC_MEMFLAG_INTERNAL (см. выше). При этом запрашивается на ALIGNMENT_SIZE байт больше: в увеличенный блок памяти в начало помещается внутренняя struct size_info (размером ALIGNMENT_SIZE), в неё помещаются размер выделенного блока и ссылка на контекст памяти. Также трассирует вызовы (кем вызвано выделение), и затем возвращает указатель на остальное, т.е. на первый байт после своей struct size_info.

void isc_mem_free(isc_mem_t *ctx, void *ptr);

Из переданного указателя лезет в находящийся перед ним struct size_info и проверяет в нем, что это точно тот контекст, с которым был вызван. Берет из этой структуры размер области, вызывает с ним mem_put, трассирует, обрабатывает настройки watermark для контекста памяти.

char *isc_mem_strdup(isc_mem_t *mctx, const char *s);

Вызывает isc_mem_allocate() с размером strlen(s)+1, делает туда strncpy() и возвращает результат.

Использование.

Легко видеть, что функции get и put - парные, так же, как и allocate/free. Первая пара требует помнить размер, вторая нет, но ценой выделения дополнительных ALIGNMENT_SIZE байт (8 в версии BIND 9.8). Поэтому для структур с известным размером дешевле использовать get и put. При этом принято также и подключать их к контексту. В целом типичное использование выглядит примерно так:

mctx = NULL; //должен быть, конечно, не NULL, а какой-то заранее созданный более глобальный контекст, типа ns_g_mctx
ptr = isc_mem_get(mctx, sizeof(*ptr));
isc_mem_attach(mctx, &ptr->mctx);
ptr->myname = isc_mem_strdup(mctx, argv[2]);
//...
//использование
//...
isc_mem_free(mctx, ptr->myname);
isc_mem_detach(&ptr->mctx);
isc_mem_put(mctx, ptr, sizeof(*ptr);
isc_mem_detach(&mctx);
Расширяем DLZ_STUB - делаем DLZ_WILDCARD

Осталось применить полученные знания на практике и проиллюстрировать всё вышесказанное чем-нибудь рабочим. В комплекте поставки BIND есть модуль dlz_stub, "заглушка", которая отдает ровно одну A-запись. Большинство функций SDLZ иллюстрирует, но совершенно непригодно для чего-то мало-мальски полезного в реальной жизни (кроме совсем уж вырожденных случаев). Выглядит его конфиг примерно так:

dlz "dlz_stub_zone" {
database "dlz_stub example2.com web 10.5.5.6";
};

example2.com The first parameter after the driver name. The DLZ stub driver expects a zone name here.
web The second parameter after the driver name. The DLZ stub driver expects an "A" record name here.
10.5.5.6 The last parameter. The DLZ stub driver expects an IPV4 IP address here.
Он отдает данные только об одной A-записи, на остальное у него жесткое вкомпилированы отдаваемые ответы, в методе allnodes():

result = dns_sdlz_putnamedrr(allnodes, cd->myname, "soa", 86400,
"web root.localhost. "
"0 28800 7200 604800 86400");
и в методе authority():

result = dns_sdlz_putsoa(lookup, cd->myname, "root.localhost.", 0);
result = dns_sdlz_putrr(lookup, "ns", 86400, cd->myname);
Хочется же что-нибудь эдакое, чтоб и выделения памяти были, и прочее. Вышепоказанное, при всей своей сферичности в вакууме, однако, наводит на мысль: а давайте сделаем модуль, чтобы можно было описать данные зоны прямо в named.conf, и чтобы эта зона могла отдаваться не для одного домена, а для целого ряда. Например, по какому-то шаблону FQDN-имени - такая задача уже вполне имеет применение на практике. Вообще, постараемся впихнуть в иллюстративный модуль побольше возможностей, если мы можем скопипастить делающий их код задешево. Тогда относительно dlz_stub наш новый модуль, назовем его dlz_wildcard, расширяется с такими требованиями:
  1. надо матчить имя зоны как шелл-паттерн имени файла, а-ля *porno*.com
  2. надо добавить опциональные аргументы - SOA, NS, MX, далее любые другие записи
  3. нужна ли поддержка $zone$, $record$ и $client$ ? типа подставить клиенту его IP в ответе в поддомене? было бы неплохо, если это получится сделать дешево
Будем отталкиваться от потребностей и сначала спроектируем, как будет выглядеть конфиг - лучше ему быть единообразным, и заодно удобным для программирования, так что на каждую запись пойдет 4 поля:

dlz "dlz_stub_zone" {
database "dlz_wildcard * 10.0.* 1800
@ 3600 SOA {ns3-fwl2.nic.ru. support.nic.ru. 42 14400 7200 2592000 600}
@ 3600 NS ns3-fwl2.nic.ru.
@ 3600 NS ns4-fwl2.nic.ru.
@ 3600 NS ns8-fwl2.nic.ru.
@ 3600 MX {5 mf1.nic.ru.}
ftp 86400 A 1.2.3.4
sql 86400 A 9.8.7.6
tmp {} A 192.0.0.2
txt 300 TXT {\"you requested $record$ in $zone$\"}
* 86400 A 109.70.27.4
";
};
Здесь получится:
  • argv[0] = dlz_wildcard - имя драйвера
  • argv[1] = * - паттерн передаваемого в findzone() имени, для которого будет зона, в данном примере - вообще для всех. Можно, например, *.[a-z][a-z] для всех двухбуквенных TLD.
  • argv[2] = 10.0.* - маска клиентского IP-адреса, опять-таки как строки, для тех клиентов, кому разрешен трансфер зоны. Вообще такому модулю стоит ставить *, но вдруг кто-то захочет и ограничить?..
  • argv[3] = 1800 - аналог директивы $TTL в файле зоны, т.е. устанавливает TTL по умолчанию для последующих строк
И далее все записи идут строго всегда по 4 поля, в формате и порядке аргументов для dns_sdlz_putnamedrr(), т.е. хост, TTL, тип, данные. Соответственно, если данных больше одного (как для MX, например), они берутся в «кавычки» синтаксиса DLZ - фигурные скобки. В данных разрешены типичные подстановки DLZ. Если поле TTL не хочется указывать, нужно указать пустое имя {} (обязательно без пробелов между скобками), тогда значением будет взято поле TTL по умолчанию (1800 в этом примере). Совсем обойтись без поля TTL, как в файлах зон, не получится, потому что тогда придется писать свой парсер, а не просто брать готовое поле argv[N+1].

Следует обратить внимание, что в TXT-записи явно добавлены заэкранированные кавычки - если этого не сделать, то в dig у клиента будет видно каждое слово обернутым в свои кавычки. Есть возможность использовать токены $record$ и $zone$ - к сожалению, $client$ поддерживается только в методе allowzonexfr(), было бы удобно использовать записи типа you CNAME your.ip.address.is.$client$.thanks.for.using.our.discovery.service.in.$zone$, но увы.

Готовый модуль можно скачать тут, как можно видеть, для простоты файл примера базируется на DLZ_STUB и использует его имена структур и дефайнов в интерфейсах, то есть, инсталлировать его нужно заменой исходника dlz_stub - в случае FreeBSD, вот так:

cd /usr/ports/dns/bind98
make clean
make patch
cp /путь/dlz_wildcard_driver.c work/bind-9.8.4-P1/contrib/dlz/drivers/dlz_stub_driver.c
make WITH_DEBUG=1 install
Пути следует подправить на актуальные для текущей версии, в диалоге указать галочку для DLZ_STUB (соответствующий ключ для ./configure при инсталляции другим способом), а WITH_DEBUG указывать только если хочется поиграться с допиливанием/отладкой модуля, когда BIND запускается как named -t /var/named -u bind -g -d 2 (для вывода отладочных логов на консоль).

Осталось немного прокомментировать для программистов по использованной копипасте в коде: методы обработки $record$ сотоварищи применяются для SQL-драйверов, но вынесены в код самого SDLZ и объявлены как static, поэтому были грязно скопированы оттуда. Обработка шаблонов имени файлов функцией fnmatch() была украдена из ядра FreeBSD (этот код используется, например, в recv/xmit/via в ipfw), а не вызвана готовая из libc, поскольку юзерлендная версия обрабатывает многобайтовые символы (национальные кодировки), в виду чего она не thread-safe. Для DNS-запросов же это совсем не требуется, а для производительности как раз желательно быть thread-safe (см. исходный код dlz_mysql/dlz_postgresql, какие танцы с бубном приходится делать, когда это нужно).

В общем, для "домашних" разработок всего этого вполне достаточно. Модуль протестирован и используется у нас в RU-CENTER на нескольких серверах (если уж касаться темы DLZ, еще на нескольких из нашего парка используется dlz_postgresql) некоторое продолжительное время - изучайте код, применяйте на здоровье.

own, объяснение

Previous post Next post
Up