geoDNS с помощью Powerdns и nginx

в 17:06, , рубрики: DNS, nginx, powerdns, геотаргетинг, системное администрирование, метки: , , ,

Обожаю задачи “на стыке технологий”, это одна из таких.
Задача:

  • реализовать 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% в статике на запрошенный поддомен.

Конфиг nginx

# в хеадер  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  http://127.0.0.1:4343/dnsapi/lookup/d.q.qq/A
 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

Но я не считаю его готовым к использованию в боевых условиях и вот почему:

Спасибо за внимание!

Вопросы прошу направлять в комментарии, а опечатки — в личку.
Желающие попиарить свой DNS сервис пожалуйста, проследуйте в свой двор, извините.

Автор: la0

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js