From: bert hubert Date: Tue, 5 Dec 2017 23:36:15 +0000 (+0100) Subject: wip X-Git-Tag: dnsdist-1.3.1~136^2~55 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=4754223fdb2df3dfacc50e67923f19df45631afb;p=thirdparty%2Fpdns.git wip --- diff --git a/docs/conf.py b/docs/conf.py index 05f67d598c..d03103d98a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -61,7 +61,7 @@ author = 'PowerDNS.COM BV' # The short X.Y version. version = '4.1' # The full version, including alpha/beta/rc tags. -release = '4.1.0-alpha0' +release = '4.1.1-pre' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/indexTOC.rst b/docs/indexTOC.rst index c368ef6e05..3bdb5485e3 100644 --- a/docs/indexTOC.rst +++ b/docs/indexTOC.rst @@ -17,6 +17,7 @@ PowerDNS Authoritative Server domainmetadata dnsupdate tsig + lua-records guides/index backends/index http-api/index diff --git a/docs/lua-records.rst b/docs/lua-records.rst new file mode 100644 index 0000000000..76666d2d9a --- /dev/null +++ b/docs/lua-records.rst @@ -0,0 +1,221 @@ +Lua Records +=========== + +To facilitate dynamic behaviour, such as Global Server Load Balancing, +PowerDNS Authoritative Server version 4.1.1 and later support dynamic DNS +records. + +These records contain small snippets of configuration that enable dynamic +behaviour based on requestor IP address, requestor's EDNS Client Subnet, +server availability or other factors. + +Capabilities range from very simple to highly advanced multi-pool +geographically & weighed load balanced IP address selection. + +Although users need not be aware, PowerDNS dynamic DNS records are actually +tiny (or larger) `Lua `_ statements. + +.. note:: + This is a PowerDNS specific feature, and is not (yet) standardized by the + IETF or other standards bodies. We are committed however to + interoperability, and strive to turn this functionalitity into a broadly + supported standard. + +Examples +-------- + +Before delving into the details, some examples may be of use to explain what +dynamic records can do. + +Here is a very basic example:: + + www IN LUA A "return ifportup(443, {'192.0.2.1', '192.0.2.2'})" + +This turns the 'www' name within a zone into a magical record that will +randomly return 192.0.2.1 or 192.0.2.2, as long as both of these IP +addresses listen on port 443. + +If either IP address stops listening, only the other address will be +returned. If all IP addresses are down a random one is returned anyhow. + +Because DNS queries require rapid answers, server availability is not checked +synchronously. In the background, a process periodically determines if IP +addresses mentioned in availability rules are, in fact, available. + +Record format +------------- +.. 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. + +The LUA record consists of an initial query type, which is the selector on +which the snippet will trigger. Optionally this query type itself can be LUA +again for configuration scripts. The query type is then followed by the +actual Lua snippet. + +LUA records can have TTL settings, and these will be honoured. In addition, +LUA record output can be DNSSEC signed like any other record, but see below +for further details. + +More powerful example +--------------------- + +A more powerful example:: + + west IN LUA A ( "return ifurlup('https://www.lua.org/', " + "{{'192.0.2.1', '192.0.2.2'}, {'198.51.100.1'}}, " + "{stringmatch='Programming in Lua'}) " ) + +In this case, IP addresses are tested to see if they will serve +https for 'www.lua.org', and if that page contains the string 'Programming +in Lua'. + +Two sets of IP addresses are supplied. If an IP address from the first set +is available, it will be returned. If no addresses work in the first set, +the second set is tried. + +This configuration makes sense in the following context:: + + www IN LUA CNAME ( "if(continent('EU')) then return 'west.powerdns.org' " + "else return 'usa.powerdns.org' end" ) + + +This sends queries that are geolocated to Europe to 'west.powerdns.org', and +the rest to 'usa.powerdns.org'. The configuration for that name would then +be:: + + usa IN LUA A ( "return ifurlup('https://www.lua.org/', " + "{{'198.51.100.1'}, {'192.0.2.1', '192.0.2.2'}}, " + "{stringmatch='Programming in Lua'}) " ) + +Note that the sets of IP addresses have reversed order - visitors geolocated +outside of Europe will hit 198.51.100.1 as long as it is available, and the +192.0.2.1 and 192.0.2.2 servers as backup. + +Advanced topics +--------------- + +To keep records more concise and readable, configuration can be stored in +separate records. The full example from above can also be written as:: + + 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) " ) + + + +Preset variables +---------------- + +LUA rules run within the same environment as described in +:doc:`modes-of-operation`. + +The Lua snippets can query the following variables: + +``who`` +~~~~~~~ +IP address of requesting resolver + + +``ecs-who`` +~~~~~~~~~~~ +The EDNS Client Subnet, should one have been set on the query. Unset +otherwise. + +``best-who`` +~~~~~~~~~~~~ +In absence of ECS, this is set to the IP address of requesting resolver. +Otherwise set to the network part of the EDNS Client Subnet supplied by the +resolver. + +Functions available +------------------- + +``ifportup(portnum, {'ip1', 'ip2'})`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Simplistic test to see if an IP address listens on a certain port. Note that +both IPv4 and IPv6 addresses can be tested, but that it is an error to list +IPv4 addresses on an AAAA record, or IPv6 addresses on an A record. + +Will return a single IP address from the set of available IP addresses. If +no IP address is available, will return a random element of the set of +addresses suppplied for testing. + +``ifurlup(url, {{'ip1', 'ip2'}, {ip3}, options)`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +More sophisticated test that attempts an actual http(s) connection to +``url``. In addition, multiple groups of IP addresses can be supplied. The +first set with a working (available) IP address is used. + +If all addresses are down, as usual, a random element from all sets is +returned. + +Various options can be set in the ``options`` parameter: + + - ``interval``: number of seconds to wait between checks + - ``stringmatch``: check ``url`` for this string, only declare 'up' if + found + +``pickrandom({'ip1', ip2'})`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Returns a random IP address from the list supplied. + +``view({{{'netmask1', 'netmask2'}, {'content1', 'content2'}}, ...})`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Shorthand function to implement 'views' for all record types. + +The input consists of a list of netmask/result pairs. + +An example:: + + view.v4.powerdns.org IN LUA A ("return view({ " + "{ {'192.168.0.0/16'}, {'192.168.1.54'}}," + "{ {'0.0.0.0/0'}, {'1.2.3.4'}} " + " }) " ) + +This will return IP address 192.168.1.54 for queries coming from +192.168.0.0/16, and 1.2.3.4 for all other queries. + +This function also works for CNAME or TXT records. + +``wrandom({{weight, 'ip1'}, {weight, 'ip2'}})`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Returns a random IP address from the list supplied, as weighted by the +various ``weight`` parameters. Performs no uptime checking. + +``country('NL')`` +~~~~~~~~~~~~~~~~~ +Returns true if the ``best-who`` IP address of the client is within the +two letter ISO country code passed, as described in :doc:`backends/geoip`. + +``continent('EU')`` +~~~~~~~~~~~~~~~~~~~ +Returns true if the ``best-who`` IP address of the client is within the +continent passed, as described in :doc:`backends/geoip`. + +Details & Security +------------------ +LUA records are synthesized on query. They can also be transferred via AXFR +to other PowerDNS servers. LUA records themselves can not be queried +however, as this would allow third parties to see load balancing internals +they do not need to see. + +A non-supporting DNS server will also serve a zone with LUA records, but +they will not function. + +LUA records can be DNSSEC signed, but because they are dynamic, it is not +possible to combine pre-signed DNSSEC zone and LUA records. In other words, +the signing key must be available on the server creating answers based on +LUA records. diff --git a/pdns/lua-auth4.cc b/pdns/lua-auth4.cc index d3f8d727c5..61d7e1a16c 100644 --- a/pdns/lua-auth4.cc +++ b/pdns/lua-auth4.cc @@ -16,6 +16,7 @@ AuthLua4::AuthLua4(const std::string& fname) { } bool AuthLua4::updatePolicy(const DNSName &qname, QType qtype, const DNSName &zonename, DNSPacket *packet) { return false; } bool AuthLua4::axfrfilter(const ComboAddress& remote, const DNSName& zone, const DNSResourceRecord& in, vector& out) { return false; } +LuaContext* AuthLua4::getLua() { return 0; } AuthLua4::~AuthLua4() { } #else @@ -23,6 +24,11 @@ AuthLua4::~AuthLua4() { } #undef L #include "ext/luawrapper/include/LuaContext.hpp" +LuaContext* AuthLua4::getLua() +{ + return d_lw.get(); +} + AuthLua4::AuthLua4(const std::string& fname) { d_lw = std::unique_ptr(new LuaContext); stubParseResolveConf(); @@ -220,15 +226,17 @@ AuthLua4::AuthLua4(const std::string& fname) { d_lw->registerFunction("getPeerPrincipal", [](UpdatePolicyQuery& upq) { return upq.peerPrincipal; }); /* end of update policy */ - ifstream ifs(fname); - if(!ifs) { - theL()<executeCode(ifs); + if(!fname.empty()) { + ifstream ifs(fname); + if(!ifs) { + theL()<executeCode(ifs); - d_update_policy = d_lw->readVariable>("updatepolicy").get_value_or(0); - d_axfr_filter = d_lw->readVariable>("axfrfilter").get_value_or(0); + d_update_policy = d_lw->readVariable>("updatepolicy").get_value_or(0); + d_axfr_filter = d_lw->readVariable>("axfrfilter").get_value_or(0); + } } @@ -391,7 +399,7 @@ bool IsUpOracle::isUp(const ComboAddress& remote, const std::string& url, std::u std::lock_guard l(d_mutex); auto iter = d_statuses.find(cd); if(iter == d_statuses.end()) { - cout<<"First ever query for "<> luaSynth(const std::string& code, const DNSName& query, const DNSName& zone, int zoneid, const DNSPacket& dnsp, uint16_t qtype) { + AuthLua4 alua(""); std::vector> ret; - LuaContext lua; - lua.writeVariable("qname", query.toString()); - lua.writeVariable("who", dnsp.getRemote().toString()); - lua.writeVariable("whoPort", dnsp.getRemote().toStringWithPort()); - if(dnsp.hasEDNSSubnet()) - lua.writeVariable("realwho", dnsp.getRealRemote().toString()); - else - lua.writeVariable("realwho", "no ECS"); + LuaContext& lua = *alua.getLua(); + lua.writeVariable("qname", query); + lua.writeVariable("who", dnsp.getRemote()); + ComboAddress bestwho; + if(dnsp.hasEDNSSubnet()) { + lua.writeVariable("ecs-who", dnsp.getRealRemote()); + bestwho=dnsp.getRealRemote().getNetwork(); + lua.writeVariable("best-who", dnsp.getRealRemote().getNetwork()); + } + else { + lua.writeVariable("ecs-who", dnsp.getRemote()); + bestwho=dnsp.getRemote(); + } + + lua.writeFunction("ifportup", [](int port, const vector >& ips) { vector candidates; for(const auto& i : ips) { @@ -449,13 +465,13 @@ std::vector> luaSynth(const std::string& code, cons if(g_up.isUp(rem)) candidates.push_back(rem); } - cout<<"Have "< ret; if(candidates.empty()) { - cout<<"Everything is down. Returning all of them"<> luaSynth(const std::string& code, cons } // - cout<<"Have "<> luaSynth(const std::string& code, cons if(g_up.isUp(c, url, opts)) available.push_back(c); if(available.empty()) { - cerr<<"Entire unit is down, trying next one if available"<> luaSynth(const std::string& code, cons }); - lua.writeFunction("pickRandom", [](const vector >& ips) { + lua.writeFunction("pickrandom", [](const vector >& ips) { return ips[random()%ips.size()].second; }); @@ -548,7 +564,6 @@ std::vector> luaSynth(const std::string& code, cons int counter=0; lua.writeFunction("report", [&counter](string event, boost::optional line){ - cout<<"It was toooo much"<> luaSynth(const std::string& code, cons return true; }); + lua.writeFunction("country", [](const std::string& country) { + return true; + }); + + lua.writeFunction("netmask", [bestwho](const vector>& ips) { + for(const auto& i :ips) { + Netmask nm(i.second); + if(nm.match(bestwho)) + return true; + } + return false; + }); + + /* { + { + {'192.168.0.0/16', '10.0.0.0/8'}, + {'192.168.20.20', '192.168.20.21'} + }, + { + {'0.0.0.0/0'}, {'192.0.2.1'} + } + } + */ + lua.writeFunction("view", [bestwho](const vector > > > > >& in) { + for(const auto& rule : in) { + const auto& netmasks=rule.second[0].second; + const auto& destinations=rule.second[1].second; + for(const auto& nmpair : netmasks) { + Netmask nm(nmpair.second); + if(nm.match(bestwho)) { + return destinations[random() % destinations.size()].second; + } + } + } + return std::string(); + } + ); + + lua.writeFunction("include", [&lua,zone,zoneid](string record) { try { - cerr<<"Wants to load record '"<(dr.dr); - cout<<"About to execute "<getCode()<getCode()); } }catch(std::exception& e) { cerr<<"Oops: "<&); - + LuaContext* getLua(); ~AuthLua4(); // this is so unique_ptr works with an incomplete type private: struct UpdatePolicyQuery { diff --git a/pdns/minicurl.cc b/pdns/minicurl.cc index 42d073ac83..9ce3724df9 100644 --- a/pdns/minicurl.cc +++ b/pdns/minicurl.cc @@ -40,7 +40,6 @@ void MiniCurl::setupURL(const std::string& str, const ComboAddress* rem) // url = http://hostname.enzo/url string host4=extractHostFromURL(str); - cout<<"Host name: '"<toString()); //cout<<"Setting hardcoded IP: "<