Обожаю задачи “на стыке технологий”, это одна из таких.
Задача:
- реализовать geoDNS*
- c возможностью wildcard (*.some.tst. A 1.2.3.4)
- с возможностью менять содержимое зон на ходу, добавлять новые зоны пачками
- без необходимости запускать громоздкие скрипты на каждый запрос “мимо кеша”
- научиться тестить этот реактор (с локалхоста, а не кучи proxy/VDS)
*) под geoDNS я подразумеваю возможность для клиентов из разных регионов отдавать разные, например, адреса сервера/А-записи (для США отдаётся IP сервера в США, для СНГ — в москве, для ЕС — в Европе …)
Статья описывает
- метод реализации geoDNS
- метод тестирования
- эскизное решение на “чистом nginx”
Если интересно, причём же здесь nginx, прошу под кат.
Существующие решения (патч для bind, geo_backend и pipe_backend у powerdns), допустим, нас чем-то не устроили.
Метод реализации geoDNS
Powerdns(pdns) — авторитативный dns сервер, который имеет кучу (аж 15 штук) бекендов (источников информации) от стандартных BIND-like до различных СУБД (MySQL, Oracle, PostgreSQL, sqlite), простого pipe и экзотики типа Lua, LDAP.
Бэкэнд выбирается глобально для всей инсталяции (нельзя 5 доменов на mysql, еще 5 на sqlite и т.д) так:
launch=remote remote-connection-string=http:url=http://127.0.0.1:4343/dnsapi
При использовании remote backend, pdns посылает на указанный сервер http-запрос и ожидает получить от онного http-ответ, содержащий данные в любимом web-разработчиками формате json
Как пример:
> GET /dnsapi/lookup/www.example.com/ANY HTTP/1.1 < {"result":[{"qtype":"A", "qname":"www.example.com", "content":"192.168.1.2", "ttl": 60}]}
Очевидно, что ставить за вебсервер какую-то динамику нельзя (слишком жирно будет, да и ddos через DNS довольно распространён), поэтому, пробуем реализовать логику DNS на чистом nginx, отдающем обычную статику.
На удивление, логика оказалась очень простая и ничего, кроме try_files и rewrite не потребовалось, реализация geo составляющей усложнилось только на использование модуля ngx_http_geo_module
Потребовался немного хитровыдуманный генератор этой самой статики (см. ниже).
Будем хранить нашу зону (уже готовый заjson-еный ответ, без учёта geo-привязки) в файловой структуре вида
/$1/$2$1_$3.jsn
$1
— зона
$2
— поддомен (_ в случае wildcard)
$3
— тип запроса (например, A, CNAME,MX… ANY)
Пример: /domain.com/sub.domain.com_A.jsn
Важное уточнение: логически доменное имя nextsub.sub.domain.com может быть
- самостоятельным доменом /nextsub.sub.domain.com/nextsub.sub.domain.com_A.jsn
- поддоменом /sub.domain.com/nextsub.sub.domain.com_A.jsn
- wildcard /sub.domain.com/_sub.domain.com_A.jsn
Поэтому перебрать нужно три варианта (укладываем в try_files).
Если такого поддомена не нашлось, ищем выше(это не по RFC, да и практическая польза сомнительна): просто повторяем поиск для sub.domain.com (укладываем в rewrite)
Самое время вспомнить про geo-составляющую.
Тут всё просто, добавляем буквенный код геозоны: /domain.com/def/sub.domain.com_A.jsn
Эскизное решение на чистом nginx
Костыль для wildcard: Важно понимать, что при wildcard запросе вида ddddd.domain.com мы должны отдать в ответе поддомен(а не *.domain.com), на помощь приходит ngx_http_sub_module, который заменяет %WC% в статике на запрошенный поддомен.
# в хеадер X-remotebackend powerdns кладёт IP клиента # определяем по нему геозону, результат откажется в переменной $src geo $http_x_remotebackend_remote $src{ default def; 127.1.0.0/16 i0; 127.1.1.0/24 i1; } # формат лога, усиленный информацией о geo-зоне и IP клиента log_format ns '$remote_addr - [$time_local] "$request" $status ' '"$http_user_agent" $http_x_remotebackend_real_remote ' ' $http_x_remotebackend_real_remote $http_x_remotebackend_remote $http_x_remotebackend_local $src'; server { listen 127.0.0.1:4343; access_log /var/www/dns/logs/nginx.access.log ns; error_log /var/www/dns/logs/nginx.error.log; # Дебажить тут ! #rewrite_log on; root /var/www/dns/store; # в любой непонятной ситуации отдаём синтаксически-корректную ошибку. error_page 403 /backend.jsn; location / { return 403; } location ~* ^/dnsapi/lookup/([^\.]+)\.([^/]*)/([a-z]+)$ { #Для дебага через http add_header X-geo $src; sub_filter_types text/plain; sub_filter "%WC%" $1.$2. ; # Если вы хотите повторять поиск для домена более высокого уровня, # уберите /empty.jsn try_files /$2/$src/$1.$2_$3.jsn /$1.$2/$src/$1.$2_$3.jsn /$2/$src/_$2_$3.jsn /$2/def/$1.$2_$3.jsn /$1.$2/def/$1.$2_$3.jsn /$2/def/_$2_$3.jsn /empty.jsn @fallback; # сначала пробуем найти ответ для определившейся геозоны ($src) # если не нашлось, пробуем дефолтный. index fallback.jsn; limit_except GET {deny all;} # ./nextsub.sub.domain.com/SOA # sub.domain.com/<geo>/nextsub.sub.domain.com_SOA # nextsub.sub.domain.com/<geo>/nextsub.sub.domain.com_SOA # sub.domain.com/<geo>/_sub.domain.com_SOA # ./sub.domain.com/SOA # ... } # идём на уровень выше. location @fallback{ rewrite ^/dnsapi/lookup/([^\.]+)\.([^/]*)/([a-z]+) /dnsapi/lookup/$2/$3; } } #server
Метод тестирования
Тут всё еще проще, обратите внимание, наши тестовые геозоны мы раздавали внутри 127.0.0.0/8, командам dig и wget можно запросто скормить нужный IP источника.
wget -q -S -O - --bind-address=127.1.0.2 dig -b 127.0.12.1 ANY q.qq @localhost
Для нашего случая всё отлично тестируется так:
# dig +short -b 127.0.0.1 A q.qq @localhost 1.1.1.1 # dig +short -b 127.1.0.1 A q.qq @localhost 127.0.0.1 # dig +short -b 127.1.1.1 A q.qq @localhost 127.1.99.123
Немного хитровыдуманный генератор
Есть немного такого кода, за который мне местами стыдно. Вот и он
<?php $empty=array(); define('TTL',3); opt('empty',true,'empty'); opt('index','true','index'); opt('backend',false,'backend'); $zones=array(); //эталонная зона $q=array(); $q[]=array('','NS','a.ns'); $q[]=array('','NS','b.ns'); $q[]=array('','A','1.1.1.1'); $q[]=array('www','CNAME',''); $q[]=array('*','A','3.2.1.4'); $q[]=array('','MX','mxs.ns',5); $q[]=array('','SOA','a.ns domain.lazutov.net. 5 3600 3600 604800 0'); //запишем в дефолтный geo $zones['q.qq']['def']=$q; $q=unsetrr($q,'','A'); // и немного модифицируем для гео $zones['q.qq']['i0']=$q; $zones['q.qq']['i0'][]=array('','A','127.0.0.1'); $zones['q.qq']['i1']=$q; $zones['q.qq']['i1'][]=array('','A','127.1.99.123'); foreach ($zones as $zone=>$locdata){ foreach ($locdata as $loc=>$rrs){ $sub=array(); $all=$rrs; // разложим зону "поподдоменно" foreach ($rrs as $r){ if ($r[0]==='*'){ $sub['*'][]=$r; } elseif ($r[0]==='') { $sub['@'][]=$r; } else { $sub[$r[0]][]=$r; } } // сформируем массив для записи в файлы и запишем. foreach ($sub as $sd=>$rrs){ $rrs=formdata($zone,$rrs); foreach ($rrs as $type=>$v) writedown($zone,$loc,$sd,$type,$v); } } } // пишем инфломацию о записях типа type поддомена sub зоны zone в гео loc function writedown ($zone,$loc,$sub,$type,$data){ $fn="{$sub}.{$zone}"; if ($sub=='@') $fn=$zone; elseif ($sub=='*') $fn='_'.$zone; opt("{$zone}/{$loc}/{$fn}_{$type}",$data); } //формируем данные для записи в json (раскладываем по типам) function formdata($zone,$rrs){ $r=array(); foreach ($rrs as $rr){ $qname=(empty($rr[0])?$zone:"{$rr[0]}.{$zone}"); $pr=(empty($rr[3])?0:intval($rr[3])); $c=(empty($rr[2])?$zone:$rr[2]); $rd=array('qname'=>$qname,'qtype'=>$rr[1],'content'=>$c,'ttl'=>TTL,'priority'=>$pr,'domain_id'=>-1); if ($rr[0]==='*' AND $rd['qtype']!=='ANY') $rd['qname']='%WC%'; $r[$rr[1]][]=$rd; $r['ANY'][]=$rd; } return $r; } function unsetrr($data,$src,$type){ foreach ($data as $k=>$v) if ($v[0]===$src and $v[1]===$type) unset($data[$k]); return $data; } // типа OutPuT данных data в файл file с комментом add function opt($file,$data,$add=NULL){ $r=array('result'=>$data); if (!empty($add)) $r['desc']=$add; $dir=dirname(__FILE__); $cd=dirname($dir.'/'.$file) ; //echo "{$cd}\n"; if (!is_dir($cd)) mkdir($cd ); file_put_contents($dir.'/'.$file.'.jsn',json_encode($r) ); }
Плюсы данного решения:
- “Горячее” добавление/изменение
- Отдача статики через nginx хорошо изучена и довольно проста
- nginx_geo хорошо изучен и документирован
- Масштабируется горизонтальненько добалением новых сначала воркеров pdns, а затем серверов связки pdns+nginx
- Допиливается под ваши нужды синтаксисом конфигов nginx
Но я не считаю его готовым к использованию в боевых условиях и вот почему:
- remote_backend пока что unstable и pdns нужно прересобрать из транка с его поддержкой
- я даже не смотрел в RFC
Спасибо за внимание!