geoDNS using Powerdns and nginx

    I love tasks “at the junction of technology”, this is one of those.
    Task:
    • implement geoDNS *
    • with the possibility of wildcard (* .some.tst. A 1.2.3.4)
    • with the ability to change the contents of zones on the go, add new zones in batches
    • without the need to run bulky scripts for every request “past the cache”
    • learn to test this reactor (from a localhost, not a proxy / VDS heap)


    *) by geoDNS, I mean the ability for clients from different regions to give different, for example, server / A-record addresses (for the USA, IP servers in the USA are given, for the CIS in Moscow, for the EU in Europe ...) The

    article describes
    • geoDNS implementation method
    • testing method
    • conceptual solution on “pure nginx”

    If it’s interesting, what’s nginx here, I ask for cat.


    Existing solutions ( patch for bind , geo_backend and pipe_backend for powerdns), for example, did not suit us with something.

    GeoDNS implementation method


    Powerdns (pdns) is an authoritative dns server that has a bunch (as many as 15 pieces ) of backends (information sources) from standard BIND-like to various DBMSs (MySQL, Oracle, PostgreSQL, sqlite), simple pipe and exotic types Lua, LDAP.

    The backend is selected globally for the entire installation (5 domains on mysql, 5 more on sqlite, etc. are not allowed) like this:
    launch=remote
    remote-connection-string=http:url=http://127.0.0.1:4343/dnsapi
    


    When using remote backend , pdns sends an http request to the specified server and expects to receive an http response from it containing data in the json format favorite by web developers.

    As an example:
    > GET /dnsapi/lookup/www.example.com/ANY HTTP/1.1
    < {"result":[{"qtype":"A", "qname":"www.example.com", "content":"192.168.1.2", "ttl": 60}]}
    


    Obviously, it is impossible to set any dynamics for the web server (it will be too bold, and ddos ​​through DNS is quite common), therefore, we are trying to implement the DNS logic on pure nginx, which gives normal statics.

    Surprisingly, the logic turned out to be very simple and nothing but try_files and rewrite was needed, the implementation of the geo component was complicated only by the use of the ngx_http_geo_module module. A
    slightly tricky generator of this same static was required (see below).

    We will store our zone (ready-made json response, excluding geo-binding) in the file structure of the form
    /$1/$2$1_$3.jsn
    $1- zone
    $2- subdomain (_ in the case of wildcard)
    $3- request type (for example, A, CNAME, MX ... ANY)
    Example: / domain .com / sub.domain.com_A.jsn

    Important clarification: the logical domain name nextsub.sub.domain.com may be
    • self-domain /nextsub.sub.domain.com/nextsub.sub.domain.com_A.jsn
    • subdomain /sub.domain.com/nextsub.sub.domain.com_A.jsn
    • wildcard /sub.domain.com/_sub.domain.com_A.jsn


    Therefore, you need to sort out three options (put in try_files).

    If we didn’t find such a subdomain, we’ll look for it above (it’s not according to the RFC, and the practical use is doubtful): we just repeat the search for sub.domain.com (put it in rewrite) It's

    time to recall the geo-component.
    Everything is simple here, add the letter code of the geofence: /domain.com/ def /sub.domain.com_A.jsn

    Sketch solution on pure nginx


    Crutch for wildcard: It is important to understand that when sending a wildcard request of the form ddddd.domain.com, we must give back the subdomain (and not * .domain.com), ngx_http_sub_module comes to the rescue , which replaces% WC% in the static with the requested subdomain.

    Nginx config
    # в хеадер  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//nextsub.sub.domain.com_SOA
    		# nextsub.sub.domain.com//nextsub.sub.domain.com_SOA
    		# sub.domain.com//_sub.domain.com_SOA
    		# ./sub.domain.com/SOA
    		# ...
    	}
    	# идём на уровень выше.
    	location @fallback{
    		rewrite ^/dnsapi/lookup/([^\.]+)\.([^/]*)/([a-z]+) /dnsapi/lookup/$2/$3;
    	}
    } #server
    


    Test method


    It’s still simpler here, note that we distributed our test geofences inside 127.0.0.0/8, you can easily feed the desired IP source to the dig and wget commands.

     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
    

    For our case, everything is perfectly tested like this:
    # 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
    


    A little tricky generator


    There is a bit of code that I am ashamed of in some places. Here he is
    Static Generator
    $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) );
    }
    


    Advantages of this solution:
    • “Hot” add / change
    • Static returns through nginx are well understood and fairly simple.
    • nginx_geo is well studied and documented.
    • Scaled horizontally by adding new pdns workers first, and then pdns + nginx bundle servers
    • It is updated to your needs with nginx config syntax


    But I do not consider it ready for use in combat conditions, and here's why:


    Thanks for attention!

    Please send questions to comments, and typos to PM.
    If you want to promote your DNS service, please proceed to your yard , sorry.

    Also popular now: