📜 ⬆️ ⬇️

geoDNS using Powerdns and nginx

I love the task “at the junction of technology,” this is one of those.
Task:


*) By geoDNS, I mean the possibility for clients from different regions to give different, for example, server / A-record addresses (for the USA, the server IP is given in the USA, for the CIS - in Moscow, for the EU - in Europe ...)

Article describes

If it is interesting, and here nginx, I ask under kat.

')
The existing solutions ( patch for bind , geo_backend and pipe_backend for powerdns), for example, did not suit us.

GeoDNS implementation method


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

The backend is selected globally for the entire installation (5 domains per mysql are not allowed, 5 more per sqlite, etc.) 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 the one containing data in json's favorite web developers format.

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}]} 


It is obvious that it is impossible to set any dynamics for a web server (it will be too fat, and ddos ​​through DNS is quite common), therefore, we are trying to implement the DNS logic on pure nginx, which renders the usual statics.

Surprisingly, the logic turned out to be very simple and nothing was needed except try_files and rewrite, the implementation of the geo component was complicated only by using the ngx_http_geo_module module
It took a little clever generator of this very static (see below).

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

Important clarification: the logical domain name nextsub.sub.domain.com can be


Therefore, you need to go through three options (put in try_files).

If such a subdomain was not found, we search above (this is not according to the RFC, and the practical use is doubtful): we simply repeat the search for sub.domain.com (we put it in rewrite)

It's time to remember about the geo-component.
Everything is simple, we add a letter code of the geofence: /domain.com/ def /sub.domain.com_A.jsn

Sketchy solution on pure nginx


Crutch for a wildcard: It is important to understand that in a wildcard request of the form ddddd.domain.com we have to give a subdomain in the answer (and not * .domain.com), ngx_http_sub_module comes to the rescue, which replaces% WC% in statics 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/([^\.]+)\.([^/]*)/([az]+)$ { #   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/([^\.]+)\.([^/]*)/([az]+) /dnsapi/lookup/$2/$3; } } #server 


Testing method


It's still easier here, pay attention, we distributed our test geofences inside 127.0.0.0/8, to the dig and wget commands you can easily feed the desired IP source.

  wget -q -S -O - --bind-address=127.1.0.2 http://127.0.0.1:4343/dnsapi/lookup/dqqq/A dig -b 127.0.12.1 ANY q.qq @localhost 

For our case, everything is perfectly tested as follows:
 # 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 clever generator


There is a bit of code for which I am ashamed in some places. Here he is
Static generator
 <?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) ); } 


Advantages of this solution:


But I do not think it is ready for use in combat conditions and that is why:


Thanks for attention!

I ask to send questions in comments, and typographical errors - in a pm.
Those who wish to use your DNS service, please proceed to your yard , sorry.

Source: https://habr.com/ru/post/178727/


All Articles