В связи с тем, что dyndns.org испортился и начал хотеть денег за свои услуги, пришлось таки озаботиться этим вопросом, изучить матчасть..
В общем-то, динамические апдейты в DNS реализованы уже много лет назад, но как обычно, есть некоторые тонкости. Под катом краткое руководство для тех, кому интересно.
Подразумевается, что у вас уже есть собственный домен, собственный первичный DNS-сервер для этого домена (BIND), и клиент под юниксом/линуксом. Вероятно, на роутере, работающем под openwrt/dd-wrt, такой метод тоже сработает с небольшими поправками, но я лично не пробовал.
Итак..
1. На клиенте генерируем секретный ключ (на самом деле, генерировать его можно где угодно, но поскольку он нужен на клиенте, то удобнее прямо там):
dnssec-keygen -a hmac-sha256 -b 128 -n HOST -r /dev/urandom dyndns
Тонкости:
-a: алгоритм можно выбирать из HMAC-MD5, HMAC-SHA1, HMAC-SHA224, HMAC-SHA256, HMAC-SHA384, HMAC-SHA512, но MD5 уже считается небезопасным, SHA1 тоже слабоват, поэтому ориентируемся на SHA256 и выше.
-b: длина ключа в битах, может быть от 1 до 512, но не более длины контрольной суммы, поэтому, например, для SHA256 ключ не может быть длиннее 256 бит.
-n: должно быть HOST, другие значения предназначены для других целей.
-r: устройство для получения случайных чисел. В принципе, его можно и не указывать, но с ним генерация ключей работает гораздо быстрее.
последний параметр (dyndns): название генерируемого ключа. Его можно выбирать достаточно произвольно.
2. В результате этой операции получаем два файла:
Knnnn.+aaa+iiiii.key
и Knnnn.+aaa+iiiii.private
где nnnn – название ключа (dyndns), aaa – номер алгоритма (например, hmac-sha256 – это 163, а hmac-sha512 – 165), iiiii – идентификатор (fingerprint) ключа.
Cкладываем оба файла куда-нибудь в /usr/local/etc/dyndns/, и на всякий случай даём доступ к ним только руту.
Внтури обоих файлов есть строка с ключом, закодированным в base64. Выглядит этот ключ примерно так: wVkp4d562LIR/h1JUm4fiulmkoijaF7lZTVp3YeEsVk=
3. Переходим к серверу. Предположим, у вас есть домен example.com. В принципе, можно настроить динамические изменения прямо в нём, но из соображений паранойи безопасности лучше завести отдельный поддомен и разрешить изменения только в нём.
Итак, на первичном DNS-сервере зоны example.com (будем считать, что его зовут ns1.example.com, а адрес у него 10.2.3.4) в файле зоны example.com заводим отдельный NS для поддомена:
dyn IN NS ns1 ; или так: dyn IN NS ns1.example.com. ; не забываем про точку в конце!
Можно также указать дополнительные NS’ы для поддомена. Они совершенно не обязательно должны совпадать с NS’ами самого домена example.com. В принципе, и первичный NS тоже вполне может быть на другом IP, тогда надо настроить записи в example.com точно так же, как это делается для обычного делегирования поддомена. Я этот вариант не рассматриваю, предполагая, что всё настраивается на одном и том же сервере, но вообще это не обязательно.
4. В конфиг сервера заносим ранее сгенерированный ключ:
key dyndns. { algorithm hmac-sha256; secret "wVkp4d562LIR/h1JUm4fiulmkoijaF7lZTVp3YeEsVk="; };
Название ключа dyndns. не обязано совпадать c тем, что было указано при генерации, но лучше использовать то же самое, чтоб было понятно, что это за ключ.
И создаём запись про поддомен:
zone "dyn.example.com" { type master; file "dyn.example.com"; allow-update { key dyndns. ; }; };
Название файла не обязано совпадать с именем зоны, но так яснее.
Тонкость: в процессе динамического изменения зоны BIND создаст журнал (файл с тем же именем и суффиксом .jnl) в той же директории, где лежит файл зоны. И время от времени будет переносить изменения из журнала в файл зоны. Соответственно, сам файл зоны и директория, где он лежит, должны быть писабельными для пользователя (или хотя бы группы), от которых запущен BIND (обычно bind:bind). Поэтому лучше положить файл зоны в стандартную директорию для временных файлов BIND’а (в линуксе обычно /var/cache/bind).
Хинт: если маршрут к файлу в директиве file не абсолютный, он отсчитывается от директории, указанной в конфиге директивой directory (которая по умолчанию и указывает на этот самый /var/cache/bind).
Либо можно указать в file абсолютный маршрут.
И не забыть проверить права на файл и директорию, чтоб BIND мог там читать и писать.
5. Создаём в выбранной директории начальный файл для зоны dyn.example.com примерно такого содержания:
$TTL 900 ; TTL по умолчанию @ IN SOA ns1.example.com. dnsmaster.example.com. ( 42 ; serial 900 ; refresh 900 ; retry 691200 ; expire 300 ; negative TTL ) 86400 IN NS ns1.example.com. ; ещё раз тот же NS
Не забываем поменять ему владельца на bind, или от кого там у вас BIND запускается..
– Поскольку вся эта конструкция собирается для адресов, которые будут относительно часто меняться, TTL по умолчанию должен быть достаточно маленьким, чтобы предотвратить длительное кэширование записей в кэширующих NS’ах после того, как адреса уже поменялись в авторитетных.
– серийник будет автоматически увеличиваться BIND’ом при каждом изменении зоны, поэтому классический вариант YYYYMMDDNN не имеет смысла. Лучше использовать какое-нибудь маленькое число, хотя бы и 1.
– refresh и retry должны быть достаточно маленькими, сравнимыми с TTL, чтобы вторичные авторитетные серверы (если они предусмотрены) побыстрее синхронизировались с первичным, если посылаемые туда уведомления об изменении зоны случайно потеряются.
– expire — по желанию.
– negative TTL тоже должен быть маленьким, лучше даже меньше TTL по умолчанию, чтоб предотвратить длительное кэширование отрицательных ответов.
– а вот конкретно для записей NS можно явно указать большой TTL, они обычно редко меняются. Но это тоже по желанию.
6. Перезапускаем BIND, убеждаемся, что он поднялся и нормально отдаёт записи SOA и NS для поддомена (других там пока нет).
7. Возвращаемся на клиентскую машину и пробуем оттуда обновить зону:
# nsupdate -k /usr/local/etc/dyndns/Knnnn.+aaa+iiiii.private > server 10.2.3.4 > zone dyn.example.com > update add foo.dyn.example.com 900 A 127.1.2.3 > send
Затем запрашиваем у авторитетного сервера адрес хоста foo.dyn.example.com, должно получиться 127.1.2.3.
Если не получилось, добавляем перед send
команды debug yes
и show
, читаем логи, и т.п.
8. Автоматизируем процесс узнавания клиентского IP и отправки его на сервер примерно таким скриптиком /usr/local/sbin/dyndns_update.sh:
#!/bin/bash
PATH=/sbin:/bin:/usr/sbin:/usr/bin
KEYFILE=/usr/local/etc/dyndns/Knnnn.+aaa+iiiii.private
SERVER=1.2.3.4
ZONE=dyn.example.com
FQDN=foo.$ZONE
TTL=900
IFACE=eth0
LOG_FACILITY=syslog
LOG_TAG=dyndns
IP=`ip addr sh $IFACE 2>/dev/null | sed -n 's|^\s\+inet \([0-9.]\+\)/.*|\1|p' | grep -v 192.168.100.`
# последний grep не обязателен
# это у меня на eth0 одновременно и DHCP и статический адрес 192.168.100
# для общения с модемом. Вот его и надо исключить
if [ "X$IP" == "X" ]; then
logger -t $LOG_TAG -p ${LOG_FACILITY}.err "Failed to detect external IP on $IFACE"
exit 1
fi
OLDIP=`host $FQDN $SERVER | sed -n 's|^.* has address \([0-9.]\+\).*|\1|p'`
if [ "X$OLDIP" == "X$IP" ]; then
logger -t $LOG_TAG -p ${LOG_FACILITY}.info "$FQDN is $OLDIP, no need to update"
exit 0
fi
# echo -e "server $SERVER\nzone $ZONE\nupdate delete $FQDN\nupdate add $FQDN $TTL A $IP\nsend"
MSG=`echo -e "server $SERVER\nzone $ZONE\nupdate delete $FQDN\nupdate add $FQDN $TTL A $IP\nsend" | nsupdate -k $KEYFILE 2>&1`
R=$?
if [ $R -eq 0 -a "X$MSG" == "X" ]; then
logger -t $LOG_TAG -p ${LOG_FACILITY}.info "Successfully updated $FQDN with $IP"
else
logger -t $LOG_TAG -p ${LOG_FACILITY}.err "$FQDN DNS update failed: $MSG"
exit $R
fi
9. И засовываем его в крон, скажем, раз в 15 минут. Можно ещё приделать непосредственно к DHCP-клиенту в /etc/dhcp/dhclient-exit-hooks.d/, тогда он после обновления адреса будет срабатывать оперативнее
Изменено: 14.08.2015