Skip to content

PowerDNS Authoritative: Lua Records

Dec 15, 2017 6:00:00 AM

Hi everyone,

We are happy to share a new development with you, one that we hinted at over a year ago: Lua resource records. In this post, we ask for your help: did we get the feature right? Are we missing important things? Lua records will be part of Authoritative Server 4.2,  and we need your testing and feedback! At the end of this post you will find exact instructions how to test the new LUA records.

Note: The fine authors of the Lua programming language insist that it is Lua and not LUA. Lua means ‘moon’ in Portuguese, and it is not an abbreviation. Sadly, it is DNS convention for record types to be all uppercase. Sorry.

While PowerDNS ships with a powerful geographical backend (geoip), there was a demand for more broader solutions that include uptime monitoring, which in addition could run from existing zones.

After several trials, we have settled on “LUA” resource records, which look like this:

 @   IN   LUA   A   "ifportup(443, {'52.48.64.3', '45.55.10.200'})"

When inserted in a zone with LUA records enabled, any lookups for your domain name will now return one of the listed IP addresses that listens on port 443. If one is down, only the other gets returned. If both are down, both get returned.

But if both are up, wouldn’t it be great if we could return the ‘best’ IP address for that client? Say no more:

@    IN   LUA A ( "ifportup(443, {'52.48.64.3', '45.55.10.200'}, "
                  "{selector='pickclosest'})                          ")

This will pick the IP address closest to that of the client, according to the MaxMind database as loaded in the geoip backend. This of course also takes the EDNS Client Subnet option into account if present.

But why stop there? Merely checking if a port is open may not be enough, so how about:

@ IN LUA A ( "ifurlup('https://powerdns.com/' ,                    "
             "{'52.48.64.3', '45.55.10.200'}, {selector='pickclosest', "
             "stringmatch='founded in the late 1990s'})            ")

This will check if the IP addresses listed actually want to serve the powerdns.com website for us, and if the content served lists a string that should be there.

The ‘pickclosest’ selector relies on third party data, and if you are a large access provider, you may have more precise ideas where your users should go. There are various ways of doing that. One way goes like this:

www IN LUA CNAME (";if(netmask({'130.161.0.0/16', '213.244.0.0/24'})" 
                  "then return 'local.powerdns.com' else          "
                  "return 'generic.powerdns.com' end              ")
local IN LUA A    "ifportup(443, {'192.0.2.1', '192.0.2.2'}       "
generic IN LUA A ("ifportup(443, {'192.0.2.1', '192.0.2.2',       " 
                  "'198.51.100.1'}, {selector='pickclosest'}          ")

Note: the starting semicolon tells the Lua record that this is a multi-statement record that does not directly return record content. More specifically, PowerDNS will prepend “return ” to your statement normally.

Another way which works without CNAMEs, and thus at the apex, goes like this:

@ IN LUA A (";if(netmask({'130.161.0.0/16', '213.244.0.0/24'})      " 
            "then return ifportup(443, {'192.0.2.1', '192.0.2.2'})"
            "else return ifportup(443, {'192.0.2.1', '192.0.2.2'},"
            "'198.51.100.1'}, {selector='pickclosest'}                ")

Doing dynamic responses at apex level is a common problem of other GSLB solutions.

To steer based on AS numbers, use if(asnum{286,1136}), for example. Countries can be selected based on their two-letter ISO code using if(country{‘BE’,’NL’,’LU’}).

In the examples above we have been typing the same IP addresses a lot. To make this easier, other records can be included to define variables:

config    IN    LUA    LUA (";settings={stringmatch='Programming in Lua'} "
                            "EUips={'192.0.2.1', '192.0.2.2'}             "
                            "USAips={'198.51.100.1'}                      ")

www       IN    LUA    CNAME ( ";if(continent('EU')) then return 'west.powerdns.org' "
                               "else return 'usa.powerdns.org' end" )

usa       IN    LUA    A    ( ";include('config')                              "
                              "return ifurlup('https://www.lua.org/',        "
                              "{USAips, EUips}, settings)                    " )

west      IN    LUA    A    ( ";include('config')                              "
                              "return ifurlup('https://www.lua.org/',        "
                              "{EUips, USAips}, settings)                    " )

This shows off another feature of ifurlup, it knows about IP groups, where it prefers to give an answer from the first set of IP addresses, and if all of those are down, it tries the second set etc etc. In this example, the ‘local’ set of IP addresses is listed first for both regions.

More possibilities

We use LUA records to power our ‘lua.powerdns.org’, ‘v4.powerdns.org’ and ‘v6.powerdns.org’ zones:

$ dig -t aaaa whoami.v6.powerdns.org +short
2a02:a440:b085:1:20d:b9ff:fe3f:8018
$ dig -t txt whoami-ecs.v6.powerdns.org +short @8.8.8.8
"ip: 2a00:1450:4013:c02::10a, netmask: 86.82.68.0/24"
$ dig -t loc latlon.v4.powerdns.org +short
51 37 15.236 N 5 26 31.920 E 0.00m 1m 10000m 10m
$ dig -t txt whoami.lua.powerdns.org +short
"2a02:a440:b085:1:20d:b9ff:fe3f:8018"

These queries deliver, respectively:

  • IPv6 address of your resolver (will not resolve without IPv6)
  • Any EDNS Client Subnet details over IPv6 (also works on v4.powerdns.org)
  • LOC record of where Maxmind thinks your resolver (or ECS address) is
  • A ‘pick your protocol’ equivalent of the v4 or v6 specific whoami queries

The actual records look like this:

whoami.lua     IN LUA TXT  "who:toString()"
whoami-ecs.lua IN LUA TXT  "'ip: '..who:toString()..', netmask: '..(ecswho and ecswho:toString() or 'no ECS')"
latlon.lua     IN LUA LOC  "latlonloc()"
whoami.v6      IN LUA AAAA "who:toString()"
whoami.v4      IN LUA A    "who:toString()"

Further details

Full documentation for this feature can be found here. To test, packages can be found on https://repo.powerdns.com/ where you should pick the ‘master’ repository for your distribution.

Setting up PowerDNS & Lua

Setup gsqlite3 as described here (or gmysql, gpgsql), then edit the pdns.conf to include:

launch=gsqlite3,geoip
gsqlite3-database=/location/of/powerdns.sqlite
local-address=0.0.0.0
local-ipv6=::
edns-subnet-processing
log-dns-queries
loglevel=9
geoip-database-files=/usr/share/GeoIP/GeoIPCity.dat,/usr/share/GeoIP/GeoIPASNum.dat
enable-lua-records

Most of this is generic to PowerDNS. Specific for our use is loading the geoip backend and its database files, enabling the LUA record, EDNS Client Subnet processing, and some debug logging so you see what is happening. The geoip-database-files path may be different depending on your operating system.

Next up, generate a test zone, and edit it:

$ pdnsutil create-zone geo.example.com ns1.example.com
Creating empty zone 'geo.example.com'
Also adding one NS record
$ pdnsutil edit-zone geo.example.com

This will fire up an editor, and allows you to insert your first LUA record. For fun, try:

geo.example.com 3600 IN LUA TXT "os.date()"

Save, and pdnsutil will ask you if you want to apply this change. Do so, and then query your PowerDNS:

$ dig -t txt geo.example.com @127.0.0.1 +short
"Thu Dec 14 21:49:00 2017"

After this you can try the zonefiles listed above, or paste from the ‘lua.powerdns.org’, ‘v4.powerdns.org’ and ‘v6.powerdns.org’ zones.

If this does not work for you (even after reading the documentation), please find us through our Open Source page. In addition, if it does work for you but you have feedback or features you need, please also let us know through powerdns.ideas@powerdns.com.

Thanks & enjoy!

Back to overview

Related Articles