From: Remi Gacogne Date: Fri, 11 Aug 2017 12:41:31 +0000 (+0200) Subject: Add initial XPF support to the recursor and dnsdist X-Git-Tag: dnsdist-1.3.0~126^2~6 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=5cc8371b3b8af00c705730724ae1c723b5df8125;p=thirdparty%2Fpdns.git Add initial XPF support to the recursor and dnsdist --- diff --git a/pdns/dnsdist-lua.cc b/pdns/dnsdist-lua.cc index 03e988f5cd..866ae4050e 100644 --- a/pdns/dnsdist-lua.cc +++ b/pdns/dnsdist-lua.cc @@ -307,6 +307,10 @@ void setupLuaConfig(bool client) if(vars.count("ipBindAddrNoPort")) { ret->ipBindAddrNoPort=boost::get(vars["ipBindAddrNoPort"]); + } + + if(vars.count("addXPF")) { + ret->addXPF=boost::get(vars["addXPF"]); } if(vars.count("maxCheckFailures")) { diff --git a/pdns/dnsdist-tcp.cc b/pdns/dnsdist-tcp.cc index f23b4c1584..54586a4375 100644 --- a/pdns/dnsdist-tcp.cc +++ b/pdns/dnsdist-tcp.cc @@ -440,6 +440,10 @@ void* tcpClientThread(int pipefd) break; } + if (dq.addXPF && ds->addXPF) { + addXPF(dq); + } + int dsock = -1; uint16_t downstreamFailures=0; #ifdef MSG_FASTOPEN diff --git a/pdns/dnsdist.cc b/pdns/dnsdist.cc index 1ff98fd367..293f4616ce 100644 --- a/pdns/dnsdist.cc +++ b/pdns/dnsdist.cc @@ -19,13 +19,17 @@ * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -#include "dnsdist.hh" -#include "dnsdist-ecs.hh" -#include "sstuff.hh" -#include "misc.hh" -#include + +#include "config.h" + +#include +#include +#include #include -#include "dolog.hh" +#include +#include +#include +#include #if defined (__OpenBSD__) || defined(__NetBSD__) #include @@ -33,28 +37,31 @@ #include #endif -#include "dnsname.hh" -#include "dnswriter.hh" -#include "base64.hh" -#include -#include "delaypipe.hh" -#include -#include "sodcrypto.hh" -#include "dnsdist-lua.hh" -#include -#include -#include "lock.hh" -#include -#include -#include "dnsdist-cache.hh" -#include "gettime.hh" -#include "ednsoptions.hh" - #ifdef HAVE_SYSTEMD #include #endif +#include "dnsdist.hh" +#include "dnsdist-cache.hh" +#include "dnsdist-ecs.hh" +#include "dnsdist-lua.hh" + +#include "base64.hh" +#include "delaypipe.hh" +#include "dolog.hh" +#include "dnsname.hh" +#include "dnswriter.hh" +#include "ednsoptions.hh" +#include "gettime.hh" +#include "lock.hh" +#include "misc.hh" +#include "sodcrypto.hh" +#include "sstuff.hh" +#include "xpf.hh" + +#ifdef HAVE_PROTOBUF thread_local boost::uuids::random_generator t_uuidGenerator; +#endif /* Known sins: @@ -1094,6 +1101,38 @@ static ssize_t udpClientSendRequestToBackend(DownstreamState* ss, const int sd, return result; } +bool addXPF(DNSQuestion& dq) +{ + std::string payload = generateXPFPayload(dq.tcp, *dq.remote, *dq.local); + uint8_t root = '\0'; + dnsrecordheader drh; + drh.d_type = htons(QType::XPF); + drh.d_class = htons(QClass::IN); + drh.d_ttl = 0; + drh.d_clen = htons(payload.size()); + size_t recordHeaderLen = sizeof(root) + sizeof(drh); + + size_t available = dq.size - dq.len; + + if ((payload.size() + recordHeaderLen) > available) { + return false; + } + + size_t pos = dq.len; + memcpy(reinterpret_cast(dq.dh) + pos, &root, sizeof(root)); + pos += sizeof(root); + memcpy(reinterpret_cast(dq.dh) + pos, &drh, sizeof(drh)); + pos += sizeof(drh); + memcpy(reinterpret_cast(dq.dh) + pos, payload.data(), payload.size()); + pos += payload.size(); + + dq.len = pos; + + dq.dh->arcount = htons(ntohs(dq.dh->arcount) + 1); + + return true; +} + static bool isUDPQueryAcceptable(ClientState& cs, LocalHolders& holders, const struct msghdr* msgh, const ComboAddress& remote, ComboAddress& dest) { if (msgh->msg_flags & MSG_TRUNC) { @@ -1350,6 +1389,10 @@ static void processUDPQuery(ClientState& cs, LocalHolders& holders, const struct return; } + if (dq.addXPF && ss->addXPF) { + addXPF(dq); + } + ss->queries++; unsigned int idOffset = (ss->idOffset++) % ss->idStates.size(); @@ -1499,6 +1542,7 @@ static void MultipleMessagesUDPClientThread(ClientState* cs, LocalHolders& holde } #endif /* defined(HAVE_RECVMMSG) && defined(HAVE_SENDMMSG) && defined(MSG_WAITFORONE) */ + // listens to incoming queries, sends out to downstream servers, noting the intended return path static void* udpClientThread(ClientState* cs) try diff --git a/pdns/dnsdist.hh b/pdns/dnsdist.hh index aff57f2074..06973257ba 100644 --- a/pdns/dnsdist.hh +++ b/pdns/dnsdist.hh @@ -146,6 +146,7 @@ struct DNSQuestion bool skipCache{false}; bool ecsOverride; bool useECS{true}; + bool addXPF{true}; }; struct DNSResponse : DNSQuestion @@ -631,6 +632,7 @@ struct DownstreamState bool mustResolve{false}; bool upStatus{false}; bool useECS{false}; + bool addXPF{false}; bool setCD{false}; std::atomic connected{false}; bool tcpFastOpen{false}; @@ -867,6 +869,8 @@ int handleDnsCryptQuery(DnsCryptContext* ctx, char* packet, uint16_t len, std::s bool encryptResponse(char* response, uint16_t* responseLen, size_t responseSize, bool tcp, std::shared_ptr dnsCryptQuery, dnsheader** dh, dnsheader* dhCopy); #endif +bool addXPF(DNSQuestion& dq); + #include "dnsdist-snmp.hh" extern bool g_snmpEnabled; diff --git a/pdns/dnsdistdist/Makefile.am b/pdns/dnsdistdist/Makefile.am index 867f9b559a..30b4eddab5 100644 --- a/pdns/dnsdistdist/Makefile.am +++ b/pdns/dnsdistdist/Makefile.am @@ -126,6 +126,7 @@ dnsdist_SOURCES = \ sstuff.hh \ statnode.cc statnode.hh \ tcpiohandler.cc tcpiohandler.hh \ + xpf.cc xpf.hh \ ext/luawrapper/include/LuaContext.hpp \ ext/json11/json11.cpp \ ext/json11/json11.hpp \ @@ -224,7 +225,8 @@ testrunner_SOURCES = \ sholder.hh \ sodcrypto.cc \ sstuff.hh \ - testrunner.cc + testrunner.cc \ + xpf.cc xpf.hh testrunner_LDFLAGS = \ $(AM_LDFLAGS) \ diff --git a/pdns/dnsdistdist/docs/reference/config.rst b/pdns/dnsdistdist/docs/reference/config.rst index 93ac4d21e2..b83f05b361 100644 --- a/pdns/dnsdistdist/docs/reference/config.rst +++ b/pdns/dnsdistdist/docs/reference/config.rst @@ -245,6 +245,8 @@ Servers newServer({ address="IP:PORT", -- IP and PORT of the backend server (mandatory) + addXPF=BOOL, -- Add the client's IP address and port to the query, along with the original destination address and port, + -- using the experimental XPF record from `draft-bellis-dnsop-xpf`. Default to false qps=NUM, -- Limit the number of queries per second to NUM, when using the `firstAvailable` policy order=NUM, -- The order of this server, used by the `leastOustanding` and `firstAvailable` policies weight=NUM, -- The weight of this server, used by the `wrandom` and `whashed` policies diff --git a/pdns/dnsdistdist/xpf.cc b/pdns/dnsdistdist/xpf.cc new file mode 120000 index 0000000000..98ab7228cd --- /dev/null +++ b/pdns/dnsdistdist/xpf.cc @@ -0,0 +1 @@ +../xpf.cc \ No newline at end of file diff --git a/pdns/dnsdistdist/xpf.hh b/pdns/dnsdistdist/xpf.hh new file mode 120000 index 0000000000..023c8c4796 --- /dev/null +++ b/pdns/dnsdistdist/xpf.hh @@ -0,0 +1 @@ +../xpf.hh \ No newline at end of file diff --git a/pdns/iputils.hh b/pdns/iputils.hh index de0cedf897..b80aa800f3 100644 --- a/pdns/iputils.hh +++ b/pdns/iputils.hh @@ -188,6 +188,8 @@ union ComboAddress { sin4.sin_family=AF_INET; sin4.sin_addr.s_addr=0; sin4.sin_port=0; + sin6.sin6_scope_id = 0; + sin6.sin6_flowinfo = 0; } ComboAddress(const struct sockaddr *sa, socklen_t salen) { @@ -267,7 +269,7 @@ union ComboAddress { string toString() const { char host[1024]; - int retval; + int retval = 0; if(sin4.sin_family && !(retval = getnameinfo((struct sockaddr*) this, getSocklen(), host, sizeof(host),0, 0, NI_NUMERICHOST))) return host; else @@ -310,28 +312,30 @@ inline ComboAddress makeComboAddress(const string& str) return address; } -inline ComboAddress makeComboAddressFromRaw(uint8_t version, const string &str) +inline ComboAddress makeComboAddressFromRaw(uint8_t version, const char* raw, size_t len) { ComboAddress address; - size_t len; if (version == 4) { - len = 4; - address.sin4.sin_family=AF_INET; - if(str.size() != len) throw NetmaskException("invalid raw address length"); - memcpy(&address.sin4.sin_addr, str.c_str(), len); + address.sin4.sin_family = AF_INET; + if (len != sizeof(address.sin4.sin_addr)) throw NetmaskException("invalid raw address length"); + memcpy(&address.sin4.sin_addr, raw, sizeof(address.sin4.sin_addr)); } else if (version == 6) { - len = 16; - address.sin4.sin_family=AF_INET6; - if(str.size() != len) throw NetmaskException("invalid raw address length"); - memcpy(&address.sin6.sin6_addr, str.c_str(), len); + address.sin6.sin6_family = AF_INET6; + if (len != sizeof(address.sin6.sin6_addr)) throw NetmaskException("invalid raw address length"); + memcpy(&address.sin6.sin6_addr, raw, sizeof(address.sin6.sin6_addr)); } else throw NetmaskException("invalid address family"); return address; } +inline ComboAddress makeComboAddressFromRaw(uint8_t version, const string &str) +{ + return makeComboAddressFromRaw(version, str.c_str(), str.size()); +} + /** This class represents a netmask and can be queried to see if a certain IP address is matched by this mask */ class Netmask diff --git a/pdns/pdns_recursor.cc b/pdns/pdns_recursor.cc index d96a64be97..ff3ec5ca55 100644 --- a/pdns/pdns_recursor.cc +++ b/pdns/pdns_recursor.cc @@ -94,6 +94,8 @@ #include "namespaces.hh" +#include "xpf.hh" + typedef map tcpClientCounts_t; static thread_local std::shared_ptr t_pdl; @@ -136,6 +138,7 @@ static vector g_localQueryAddresses4, g_localQueryAddresses6; static AtomicCounter counter; static std::shared_ptr g_initialDomainMap; // new threads needs this to be setup static std::shared_ptr g_initialAllowFrom; // new thread needs to be setup with this +static NetmaskGroup g_XPFAcl; static size_t g_tcpMaxQueriesPerConn; static uint64_t g_latencyStatSize; static uint32_t g_disthashseed; @@ -181,10 +184,15 @@ struct DNSComboWriter { DNSComboWriter(const char* data, uint16_t len, const struct timeval& now) : d_mdp(true, data, len), d_now(now), d_tcp(false), d_socket(-1) {} - MOADNSParser d_mdp; - void setRemote(const ComboAddress* sa) + + void setRemote(const ComboAddress& sa) + { + d_remote=sa; + } + + void setSource(const ComboAddress& sa) { - d_remote=*sa; + d_source=sa; } void setLocal(const ComboAddress& sa) @@ -192,6 +200,10 @@ struct DNSComboWriter { d_local=sa; } + void setDestination(const ComboAddress& sa) + { + d_destination=sa; + } void setSocket(int sock) { @@ -200,11 +212,26 @@ struct DNSComboWriter { string getRemote() const { - return d_remote.toString(); + if (d_source == d_remote) { + return d_source.toStringWithPort(); + } + return d_source.toStringWithPort() + " (proxied by " + d_remote.toStringWithPort() + ")"; } + MOADNSParser d_mdp; struct timeval d_now; - ComboAddress d_remote, d_local; + /* Remote client, might differ from d_source + in case of XPF, in which case d_source holds + the IP of the client and d_remote of the proxy + */ + ComboAddress d_remote; + ComboAddress d_source; + /* Destination address, might differ from + d_destination in case of XPF, in which case + d_destination holds the IP of the proxy and + d_local holds our own. */ + ComboAddress d_local; + ComboAddress d_destination; #ifdef HAVE_PROTOBUF boost::uuids::uuid d_uuid; string d_requestorId; @@ -642,7 +669,7 @@ static void updateResponseStats(int res, const ComboAddress& remote, unsigned in static string makeLoginfo(DNSComboWriter* dc) try { - return "("+dc->d_mdp.d_qname.toLogString()+"/"+DNSRecordContent::NumberToType(dc->d_mdp.d_qtype)+" from "+(dc->d_remote.toString())+")"; + return "("+dc->d_mdp.d_qname.toLogString()+"/"+DNSRecordContent::NumberToType(dc->d_mdp.d_qtype)+" from "+(dc->getRemote())+")"; } catch(...) { @@ -764,9 +791,9 @@ static void startDoResolve(void *p) RecProtoBufMessage pbMessage(RecProtoBufMessage::Response); #ifdef HAVE_PROTOBUF if (luaconfsLocal->protobufServer) { - Netmask requestorNM(dc->d_remote, dc->d_remote.sin4.sin_family == AF_INET ? luaconfsLocal->protobufMaskV4 : luaconfsLocal->protobufMaskV6); + Netmask requestorNM(dc->d_source, dc->d_source.sin4.sin_family == AF_INET ? luaconfsLocal->protobufMaskV4 : luaconfsLocal->protobufMaskV6); const ComboAddress& requestor = requestorNM.getMaskedNetwork(); - pbMessage.update(dc->d_uuid, &requestor, &dc->d_local, dc->d_tcp, dc->d_mdp.d_header.id); + pbMessage.update(dc->d_uuid, &requestor, &dc->d_destination, dc->d_tcp, dc->d_mdp.d_header.id); pbMessage.setEDNSSubnet(dc->d_ednssubnet.source, dc->d_ednssubnet.source.isIpv4() ? luaconfsLocal->protobufMaskV4 : luaconfsLocal->protobufMaskV6); pbMessage.setQuestion(dc->d_mdp.d_qname, dc->d_mdp.d_qtype, dc->d_mdp.d_qclass); } @@ -818,7 +845,7 @@ static void startDoResolve(void *p) int res = RCode::NoError; DNSFilterEngine::Policy appliedPolicy; DNSRecord spoofed; - RecursorLua4::DNSQuestion dq(dc->d_remote, dc->d_local, dc->d_mdp.d_qname, dc->d_mdp.d_qtype, dc->d_tcp, variableAnswer, wantsRPZ); + RecursorLua4::DNSQuestion dq(dc->d_source, dc->d_destination, dc->d_mdp.d_qname, dc->d_mdp.d_qtype, dc->d_tcp, variableAnswer, wantsRPZ); dq.ednsFlags = &edo.d_Z; dq.ednsOptions = &dc->d_ednsOpts; dq.tag = dc->d_tag; @@ -865,7 +892,7 @@ static void startDoResolve(void *p) // Check if the query has a policy attached to it if (wantsRPZ) { - appliedPolicy = luaconfsLocal->dfe.getQueryPolicy(dc->d_mdp.d_qname, dc->d_remote, sr.d_discardedPolicies); + appliedPolicy = luaconfsLocal->dfe.getQueryPolicy(dc->d_mdp.d_qname, dc->d_source, sr.d_discardedPolicies); } // if there is a RecursorLua active, and it 'took' the query in preResolve, we don't launch beginResolve @@ -1054,14 +1081,14 @@ static void startDoResolve(void *p) if(!shouldNotValidate && sr.isDNSSECValidationRequested()) { try { if(sr.doLog()) { - L<d_mdp.d_qname<<"|"<d_mdp.d_qtype).getName()<<" for "<d_remote.toStringWithPort()<d_mdp.d_qname<<"|"<d_mdp.d_qtype).getName()<<" for "<getRemote()<d_mdp.d_qname<<"|"<d_mdp.d_qtype).getName()<<" for "<d_remote.toStringWithPort()<<" validates correctly"<d_mdp.d_qname<<"|"<d_mdp.d_qtype).getName()<<" for "<getRemote()<<" validates correctly"<d_mdp.d_qname<<"|"<d_mdp.d_qtype).getName()<<" for "<d_remote.toStringWithPort()<<" validates as Insecure"<d_mdp.d_qname<<"|"<d_mdp.d_qtype).getName()<<" for "<getRemote()<<" validates as Insecure"<ad=0; } else if(state == Bogus) { if(g_dnssecLogBogus || sr.doLog() || g_dnssecmode == DNSSECMode::ValidateForLog) { - L<d_mdp.d_qname<<"|"<d_mdp.d_qtype).getName()<<" for "<d_remote.toStringWithPort()<<" validates as Bogus"<d_mdp.d_qname<<"|"<d_mdp.d_qtype).getName()<<" for "<getRemote()<<" validates as Bogus"<sortlist.getOrderCmp(dc->d_remote)) { + if(auto sl = luaconfsLocal->sortlist.getOrderCmp(dc->d_source)) { stable_sort(ret.begin(), ret.end(), *sl); variableAnswer=true; } @@ -1157,7 +1184,7 @@ static void startDoResolve(void *p) } g_rs.submitResponse(dc->d_mdp.d_qtype, packet.size(), !dc->d_tcp); - updateResponseStats(res, dc->d_remote, packet.size(), &dc->d_mdp.d_qname, dc->d_mdp.d_qtype); + updateResponseStats(res, dc->d_source, packet.size(), &dc->d_mdp.d_qname, dc->d_mdp.d_qtype); #ifdef HAVE_PROTOBUF if (luaconfsLocal->protobufServer && (!luaconfsLocal->protobufTaggedOnly || (appliedPolicy.d_name && !appliedPolicy.d_name->empty()) || !dc->d_policyTags.empty())) { pbMessage.setBytes(packet.size()); @@ -1184,7 +1211,7 @@ static void startDoResolve(void *p) addCMsgSrcAddr(&msgh, cbuf, &dc->d_local, 0); } if(sendmsg(dc->d_socket, &msgh, 0) < 0 && g_logCommonErrors) - L<d_remote.toStringWithPort()<<" failed with: "<getRemote()<<" failed with: "<insertResponsePacket(dc->d_tag, dc->d_qhash, dc->d_mdp.d_qname, dc->d_mdp.d_qtype, dc->d_mdp.d_qclass, string((const char*)&*packet.begin(), packet.size()), @@ -1353,48 +1380,75 @@ static void makeControlChannelSocket(int processNum=-1) } } -static bool getQNameAndSubnet(const std::string& question, DNSName* dnsname, uint16_t* qtype, uint16_t* qclass, EDNSSubnetOpts* ednssubnet, std::map* options) +static void getQNameAndSubnet(const std::string& question, DNSName* dnsname, uint16_t* qtype, uint16_t* qclass, + bool& foundECS, EDNSSubnetOpts* ednssubnet, std::map* options, + bool& foundXPF, ComboAddress* xpfSource, ComboAddress* xpfDest) { - bool found = false; - const struct dnsheader* dh = (struct dnsheader*)question.c_str(); + const bool lookForXPF = xpfSource != nullptr; + const bool lookForECS = ednssubnet != nullptr; + const struct dnsheader* dh = reinterpret_cast(question.c_str()); size_t questionLen = question.length(); unsigned int consumed=0; *dnsname=DNSName(question.c_str(), questionLen, sizeof(dnsheader), false, qtype, qclass, &consumed); size_t pos= sizeof(dnsheader)+consumed+4; - /* at least OPT root label (1), type (2), class (2) and ttl (4) + OPT RR rdlen (2) - = 11 */ - if(ntohs(dh->arcount) == 1 && questionLen > pos + 11) { // this code can extract one (1) EDNS Subnet option + const size_t headerSize = /* root */ 1 + sizeof(dnsrecordheader); + const uint16_t arcount = ntohs(dh->arcount); + + for (uint16_t arpos = 0; arpos < arcount && questionLen > (pos + headerSize) && ((lookForECS && !foundECS) || (lookForXPF && !foundXPF)); arpos++) { + if (question.at(pos) != 0) { + /* not an OPT or a XPF, bye. */ + return; + } + + pos += 1; + const dnsrecordheader* drh = reinterpret_cast(&question.at(pos)); + pos += sizeof(dnsrecordheader); + + if (pos >= questionLen) { + return; + } + /* OPT root label (1) followed by type (2) */ - if(question.at(pos)==0 && question.at(pos+1)==0 && question.at(pos+2)==QType::OPT) { + if(lookForECS && ntohs(drh->d_type) == QType::OPT) { if (!options) { char* ecsStart = nullptr; size_t ecsLen = 0; - int res = getEDNSOption((char*)question.c_str()+pos+9, questionLen - pos - 9, EDNSOptionCode::ECS, &ecsStart, &ecsLen); + /* we need to pass the record len */ + int res = getEDNSOption(const_cast(reinterpret_cast(&question.at(pos - sizeof(drh->d_clen)))), questionLen - pos + sizeof(drh->d_clen), EDNSOptionCode::ECS, &ecsStart, &ecsLen); if (res == 0 && ecsLen > 4) { EDNSSubnetOpts eso; if(getEDNSSubnetOptsFromString(ecsStart + 4, ecsLen - 4, &eso)) { *ednssubnet=eso; - found = true; + foundECS = true; } } } else { - int res = getEDNSOptions((char*)question.c_str()+pos+9, questionLen - pos - 9, *options); + /* we need to pass the record len */ + int res = getEDNSOptions(reinterpret_cast(&question.at(pos -sizeof(drh->d_clen))), questionLen - pos + (sizeof(drh->d_clen)), *options); if (res == 0) { const auto& it = options->find(EDNSOptionCode::ECS); if (it != options->end() && it->second.content != nullptr && it->second.size > 0) { EDNSSubnetOpts eso; if(getEDNSSubnetOptsFromString(it->second.content, it->second.size, &eso)) { *ednssubnet=eso; - found = true; + foundECS = true; } } } } } + else if (lookForXPF && ntohs(drh->d_type) == QType::XPF && ntohs(drh->d_class) == QClass::IN && drh->d_ttl == 0) { + if ((questionLen - pos) < ntohs(drh->d_clen)) { + return; + } + + foundXPF = parseXPFPayload(reinterpret_cast(&question.at(pos)), ntohs(drh->d_clen), *xpfSource, xpfDest); + } + + pos += ntohs(drh->d_clen); } - return found; } static void handleRunningTCPQuestion(int fd, FDMultiplexer::funcparam_t& var) @@ -1424,7 +1478,7 @@ static void handleRunningTCPQuestion(int fd, FDMultiplexer::funcparam_t& var) } if(!bytes || bytes < 0) { if(g_logCommonErrors) - L<d_remote.toString() <<" disconnected after first byte"<d_remote.toStringWithPort() <<" disconnected after first byte"<removeReadFD(fd); return; } @@ -1432,7 +1486,7 @@ static void handleRunningTCPQuestion(int fd, FDMultiplexer::funcparam_t& var) else if(conn->state==TCPConnection::GETQUESTION) { ssize_t bytes=recv(conn->getFD(), conn->data + conn->bytesread, conn->qlen - conn->bytesread, 0); if(!bytes || bytes < 0 || bytes > std::numeric_limits::max()) { - L<d_remote.toString() <<" disconnected while reading question body"<d_remote.toStringWithPort() <<" disconnected while reading question body"<removeReadFD(fd); return; } @@ -1447,23 +1501,26 @@ static void handleRunningTCPQuestion(int fd, FDMultiplexer::funcparam_t& var) catch(MOADNSException &mde) { g_stats.clientParseError++; if(g_logCommonErrors) - L<d_remote.toString() <d_remote.toStringWithPort() <d_tcpConnection = conn; // carry the torch dc->setSocket(conn->getFD()); // this is the only time a copy is made of the actual fd dc->d_tcp=true; - dc->setRemote(&conn->d_remote); + dc->setRemote(conn->d_remote); + dc->setSource(conn->d_remote); ComboAddress dest; memset(&dest, 0, sizeof(dest)); dest.sin4.sin_family = conn->d_remote.sin4.sin_family; socklen_t len = dest.getSocklen(); getsockname(conn->getFD(), (sockaddr*)&dest, &len); // if this fails, we're ok with it dc->setLocal(dest); + dc->setDestination(dest); DNSName qname; uint16_t qtype=0; uint16_t qclass=0; bool needECS = false; + bool needXPF = g_XPFAcl.match(conn->d_remote); string requestorId; string deviceId; #ifdef HAVE_PROTOBUF @@ -1473,16 +1530,20 @@ static void handleRunningTCPQuestion(int fd, FDMultiplexer::funcparam_t& var) } #endif - if(needECS || (t_pdl && t_pdl->d_gettag)) { + if(needECS || needXPF || (t_pdl && t_pdl->d_gettag)) { try { std::map ednsOptions; + bool xpfFound = false; dc->d_ecsParsed = true; - dc->d_ecsFound = getQNameAndSubnet(std::string(conn->data, conn->qlen), &qname, &qtype, &qclass, &dc->d_ednssubnet, g_gettagNeedsEDNSOptions ? &ednsOptions : nullptr); + dc->d_ecsFound = false; + getQNameAndSubnet(std::string(conn->data, conn->qlen), &qname, &qtype, &qclass, + dc->d_ecsFound, &dc->d_ednssubnet, g_gettagNeedsEDNSOptions ? &ednsOptions : nullptr, + xpfFound, needXPF ? &dc->d_source : nullptr, needXPF ? &dc->d_destination : nullptr); if(t_pdl && t_pdl->d_gettag) { try { - dc->d_tag = t_pdl->gettag(conn->d_remote, dc->d_ednssubnet.source, dest, qname, qtype, &dc->d_policyTags, dc->d_data, ednsOptions, true, requestorId, deviceId); + dc->d_tag = t_pdl->gettag(dc->d_source, dc->d_ednssubnet.source, dc->d_destination, qname, qtype, &dc->d_policyTags, dc->d_data, ednsOptions, true, requestorId, deviceId); } catch(std::exception& e) { if(g_logCommonErrors) @@ -1508,7 +1569,7 @@ static void handleRunningTCPQuestion(int fd, FDMultiplexer::funcparam_t& var) const struct dnsheader* dh = (const struct dnsheader*) conn->data; if (!luaconfsLocal->protobufTaggedOnly) { - protobufLogQuery(luaconfsLocal->protobufServer, luaconfsLocal->protobufMaskV4, luaconfsLocal->protobufMaskV6, dc->d_uuid, conn->d_remote, dest, dc->d_ednssubnet.source, true, dh->id, conn->qlen, qname, qtype, qclass, dc->d_policyTags, dc->d_requestorId, dc->d_deviceId); + protobufLogQuery(luaconfsLocal->protobufServer, luaconfsLocal->protobufMaskV4, luaconfsLocal->protobufMaskV6, dc->d_uuid, dc->d_source, dc->d_destination, dc->d_ednssubnet.source, true, dh->id, conn->qlen, qname, qtype, qclass, dc->d_policyTags, dc->d_requestorId, dc->d_deviceId); } } catch(std::exception& e) { @@ -1520,13 +1581,13 @@ static void handleRunningTCPQuestion(int fd, FDMultiplexer::funcparam_t& var) if(dc->d_mdp.d_header.qr) { delete dc; g_stats.ignoredCount++; - L<d_remote.toString() <<" on server socket!"<getRemote() <<" on server socket!"<d_mdp.d_header.opcode) { delete dc; g_stats.ignoredCount++; - L<d_remote.toString() <<" on server socket!"<getRemote() <<" on server socket!"< policyTags; LuaContext::LuaObject data; + ComboAddress source = fromaddr; + ComboAddress destination = destaddr; string requestorId; string deviceId; #ifdef HAVE_PROTOBUF @@ -1650,16 +1714,23 @@ static string* doProcessUDPQuestion(const std::string& question, const ComboAddr */ #endif - if(needECS || (t_pdl && t_pdl->d_gettag)) { + if(needECS || needXPF || (t_pdl && t_pdl->d_gettag)) { try { std::map ednsOptions; - ecsFound = getQNameAndSubnet(question, &qname, &qtype, &qclass, &ednssubnet, g_gettagNeedsEDNSOptions ? &ednsOptions : nullptr); + bool xpfFound = false; + + ecsFound = false; + + getQNameAndSubnet(question, &qname, &qtype, &qclass, + ecsFound, &ednssubnet, g_gettagNeedsEDNSOptions ? &ednsOptions : nullptr, + xpfFound, needXPF ? &source : nullptr, needXPF ? &destination : nullptr); + qnameParsed = true; ecsParsed = true; if(t_pdl && t_pdl->d_gettag) { try { - ctag=t_pdl->gettag(fromaddr, ednssubnet.source, destaddr, qname, qtype, &policyTags, data, ednsOptions, false, requestorId, deviceId); + ctag=t_pdl->gettag(source, ednssubnet.source, destination, qname, qtype, &policyTags, data, ednsOptions, false, requestorId, deviceId); } catch(std::exception& e) { if(g_logCommonErrors) @@ -1679,7 +1750,7 @@ static string* doProcessUDPQuestion(const std::string& question, const ComboAddr #ifdef HAVE_PROTOBUF if(luaconfsLocal->protobufServer) { if (!luaconfsLocal->protobufTaggedOnly || !policyTags.empty()) { - protobufLogQuery(luaconfsLocal->protobufServer, luaconfsLocal->protobufMaskV4, luaconfsLocal->protobufMaskV6, uniqueId, fromaddr, destaddr, ednssubnet.source, false, dh->id, question.size(), qname, qtype, qclass, policyTags, requestorId, deviceId); + protobufLogQuery(luaconfsLocal->protobufServer, luaconfsLocal->protobufMaskV4, luaconfsLocal->protobufMaskV6, uniqueId, source, destination, ednssubnet.source, false, dh->id, question.size(), qname, qtype, qclass, policyTags, requestorId, deviceId); } } #endif /* HAVE_PROTOBUF */ @@ -1694,9 +1765,9 @@ static string* doProcessUDPQuestion(const std::string& question, const ComboAddr if (cacheHit) { #ifdef HAVE_PROTOBUF if(luaconfsLocal->protobufServer && (!luaconfsLocal->protobufTaggedOnly || !pbMessage.getAppliedPolicy().empty() || !pbMessage.getPolicyTags().empty())) { - Netmask requestorNM(fromaddr, fromaddr.sin4.sin_family == AF_INET ? luaconfsLocal->protobufMaskV4 : luaconfsLocal->protobufMaskV6); + Netmask requestorNM(source, source.sin4.sin_family == AF_INET ? luaconfsLocal->protobufMaskV4 : luaconfsLocal->protobufMaskV6); const ComboAddress& requestor = requestorNM.getMaskedNetwork(); - pbMessage.update(uniqueId, &requestor, &destaddr, false, dh->id); + pbMessage.update(uniqueId, &requestor, &destination, false, dh->id); pbMessage.setEDNSSubnet(ednssubnet.source, ednssubnet.source.isIpv4() ? luaconfsLocal->protobufMaskV4 : luaconfsLocal->protobufMaskV6); pbMessage.setQueryTime(g_now.tv_sec, g_now.tv_usec); pbMessage.setRequestorId(requestorId); @@ -1705,7 +1776,7 @@ static string* doProcessUDPQuestion(const std::string& question, const ComboAddr } #endif /* HAVE_PROTOBUF */ if(!g_quiet) - L<= sizeof(struct dnsheader)) { struct dnsheader tmpdh; memcpy(&tmpdh, response.c_str(), sizeof(tmpdh)); - updateResponseStats(tmpdh.rcode, fromaddr, response.length(), 0, 0); + updateResponseStats(tmpdh.rcode, source, response.length(), 0, 0); } g_stats.avgLatencyUsec=(1-1.0/g_latencyStatSize)*g_stats.avgLatencyUsec + 0.0; // we assume 0 usec g_stats.avgLatencyOursUsec=(1-1.0/g_latencyStatSize)*g_stats.avgLatencyOursUsec + 0.0; // we assume 0 usec @@ -1738,9 +1809,9 @@ static string* doProcessUDPQuestion(const std::string& question, const ComboAddr } if(t_pdl) { - if(t_pdl->ipfilter(fromaddr, destaddr, *dh)) { + if(t_pdl->ipfilter(source, destination, *dh)) { if(!g_quiet) - L<getTid()<<"/"<numProcesses()<<"] DROPPED question from "<getTid()<<"/"<numProcesses()<<"] DROPPED question from "<numProcesses() > g_maxMThreads) { if(!g_quiet) - L<getTid()<<"/"<numProcesses()<<"] DROPPED question from "<getTid()<<"/"<numProcesses()<<"] DROPPED question from "<d_tag=ctag; dc->d_qhash=qhash; dc->d_query = question; - dc->setRemote(&fromaddr); + dc->setRemote(fromaddr); + dc->setSource(source); dc->setLocal(destaddr); + dc->setDestination(destination); dc->d_tcp=false; dc->d_policyTags = policyTags; dc->d_data = data; @@ -2990,6 +3063,8 @@ static int serviceMain(int argc, char*argv[]) SyncRes::parseEDNSSubnetAddFor(::arg()["ecs-add-for"]); g_useIncomingECS = ::arg().mustDo("use-incoming-edns-subnet"); + g_XPFAcl.toMasks(::arg()["xpf-allow-from"]); + g_networkTimeoutMsec = ::arg().asNum("network-timeout"); g_initialDomainMap = parseAuthAndForwards(); @@ -3242,7 +3317,7 @@ try for(expired_t::iterator i=expired.begin() ; i != expired.end(); ++i) { shared_ptr conn=any_cast >(i->second); if(g_logCommonErrors) - L<d_remote.toString() <d_remote.toStringWithPort() <removeReadFD(i->first); } } @@ -3422,6 +3497,8 @@ int main(int argc, char **argv) ::arg().setSwitch("log-rpz-changes", "Log additions and removals to RPZ zones at Info level")="no"; + ::arg().set("xpf-allow-from","XPF information is only processed from these subnets")=""; + ::arg().setCmd("help","Provide a helpful message"); ::arg().setCmd("version","Print version string"); ::arg().setCmd("config","Output blank configuration"); diff --git a/pdns/recursordist/Makefile.am b/pdns/recursordist/Makefile.am index 8c2070ae22..4770fd6f34 100644 --- a/pdns/recursordist/Makefile.am +++ b/pdns/recursordist/Makefile.am @@ -164,6 +164,7 @@ pdns_recursor_SOURCES = \ webserver.cc webserver.hh \ ws-api.cc ws-api.hh \ ws-recursor.cc ws-recursor.hh \ + xpf.cc xpf.hh \ zoneparser-tng.cc zoneparser-tng.hh if !HAVE_LUA_HPP @@ -248,11 +249,13 @@ testrunner_SOURCES = \ test-signers.cc \ test-syncres_cc.cc \ test-tsig.cc \ + test-xpf_cc.cc \ testrunner.cc \ tsigverifier.cc tsigverifier.hh \ unix_utility.cc \ validate.cc validate.hh \ validate-recursor.cc validate-recursor.hh \ + xpf.cc xpf.hh \ zoneparser-tng.cc zoneparser-tng.hh testrunner_LDFLAGS = \ diff --git a/pdns/recursordist/docs/settings.rst b/pdns/recursordist/docs/settings.rst index e7e3fb5256..13a1e6b44b 100644 --- a/pdns/recursordist/docs/settings.rst +++ b/pdns/recursordist/docs/settings.rst @@ -1162,3 +1162,17 @@ TCP port where the webserver should listen on. - Default: yes If a PID file should be written to `socket-dir`_ + +``xpf-allow-from`` +------------- +.. versionadded:: 4.1.0 + +- IP ranges, separated by commas +- Default: empty + +This is an experimental implementation of `draft-bellis-dnsop-xpf`. +The server will trust XPF records found in queries sent from those netmasks (both IPv4 and IPv6), +and will adjust queries' source and destination accordingly. This is especially useful when the recursor +is placed behind a proxy like dnsdist. +Note that the `allow-from`_ setting is still applied to the original source address, and thus access restriction +should be done on the proxy. diff --git a/pdns/recursordist/test-ednsoptions_cc.cc b/pdns/recursordist/test-ednsoptions_cc.cc index 2a4cab01ed..fe12a962cc 100644 --- a/pdns/recursordist/test-ednsoptions_cc.cc +++ b/pdns/recursordist/test-ednsoptions_cc.cc @@ -14,9 +14,6 @@ #include "ednssubnet.hh" #include "iputils.hh" -/* extract a specific EDNS0 option from a pointer on the beginning rdLen of the OPT RR */ -int getEDNSOption(char* optRR, size_t len, uint16_t wantedOption, char ** optionValue, size_t * optionValueSize); - BOOST_AUTO_TEST_SUITE(ednsoptions_cc) static void getRawQueryWithECSAndCookie(const DNSName& name, const Netmask& ecs, const std::string& clientCookie, const std::string& serverCookie, std::vector& query) diff --git a/pdns/recursordist/test-xpf_cc.cc b/pdns/recursordist/test-xpf_cc.cc new file mode 100644 index 0000000000..61d079ca29 --- /dev/null +++ b/pdns/recursordist/test-xpf_cc.cc @@ -0,0 +1,180 @@ +#define BOOST_TEST_DYN_LINK +#define BOOST_TEST_NO_MAIN + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif +#include + +#include "xpf.hh" + +BOOST_AUTO_TEST_SUITE(xpf_cc) + +BOOST_AUTO_TEST_CASE(test_generateXPFPayload) { + + /* Mixing v4 with v6 should throw */ + BOOST_CHECK_THROW(generateXPFPayload(false, ComboAddress("192.0.2.1"), ComboAddress("2001:db8::1")), std::runtime_error); + BOOST_CHECK_THROW(generateXPFPayload(false, ComboAddress("2001:db8::1"), ComboAddress("192.0.2.1")), std::runtime_error); + + { + /* v4 payload over UDP */ + ComboAddress source("192.0.2.1:53"); + ComboAddress destination("192.0.2.2:65535"); + + auto payload = generateXPFPayload(false, source, destination); + BOOST_CHECK_EQUAL(payload.size(), 14); + BOOST_CHECK_EQUAL(payload.at(0), 4); + BOOST_CHECK_EQUAL(payload.at(1), 17); + + ComboAddress parsedSource; + ComboAddress parsedDestination; + BOOST_CHECK(parseXPFPayload(payload.c_str(), payload.size(), parsedSource, &parsedDestination)); + BOOST_CHECK_EQUAL(parsedSource.toStringWithPort(), source.toStringWithPort()); + BOOST_CHECK_EQUAL(parsedDestination.toStringWithPort(), destination.toStringWithPort()); + } + + { + /* v4 payload over TCP */ + ComboAddress source("192.0.2.1:53"); + ComboAddress destination("192.0.2.2:65535"); + + auto payload = generateXPFPayload(true, source, destination); + BOOST_CHECK_EQUAL(payload.size(), 14); + BOOST_CHECK_EQUAL(payload.at(0), 4); + BOOST_CHECK_EQUAL(payload.at(1), 6); + + ComboAddress parsedSource; + ComboAddress parsedDestination; + BOOST_CHECK(parseXPFPayload(payload.c_str(), payload.size(), parsedSource, &parsedDestination)); + BOOST_CHECK_EQUAL(parsedSource.toStringWithPort(), source.toStringWithPort()); + BOOST_CHECK_EQUAL(parsedDestination.toStringWithPort(), destination.toStringWithPort()); + } + + { + /* v6 payload over UDP */ + ComboAddress source("[2001:db8::1]:42"); + ComboAddress destination("[::1]:65535"); + + auto payload = generateXPFPayload(false, source, destination); + BOOST_CHECK_EQUAL(payload.size(), 38); + BOOST_CHECK_EQUAL(payload.at(0), 6); + BOOST_CHECK_EQUAL(payload.at(1), 17); + + ComboAddress parsedSource; + ComboAddress parsedDestination; + BOOST_CHECK(parseXPFPayload(payload.c_str(), payload.size(), parsedSource, &parsedDestination)); + BOOST_CHECK_EQUAL(parsedSource.toStringWithPort(), source.toStringWithPort()); + BOOST_CHECK_EQUAL(parsedDestination.toStringWithPort(), destination.toStringWithPort()); + } + + { + /* v6 payload over TCP */ + ComboAddress source("[2001:db8::1]:42"); + ComboAddress destination("[::1]:65535"); + + auto payload = generateXPFPayload(true, source, destination); + BOOST_CHECK_EQUAL(payload.size(), 38); + BOOST_CHECK_EQUAL(payload.at(0), 6); + BOOST_CHECK_EQUAL(payload.at(1), 6); + + ComboAddress parsedSource; + ComboAddress parsedDestination; + BOOST_CHECK(parseXPFPayload(payload.c_str(), payload.size(), parsedSource, &parsedDestination)); + BOOST_CHECK_EQUAL(parsedSource.toStringWithPort(), source.toStringWithPort()); + BOOST_CHECK_EQUAL(parsedDestination.toStringWithPort(), destination.toStringWithPort()); + } + +} + +BOOST_AUTO_TEST_CASE(test_parseXPFPayload) { + + /* invalid sizes */ + { + ComboAddress source; + ComboAddress destination; + + BOOST_CHECK_EQUAL(parseXPFPayload(nullptr, 0, source, &destination), false); + BOOST_CHECK_EQUAL(parseXPFPayload(nullptr, 13, source, &destination), false); + BOOST_CHECK_EQUAL(parseXPFPayload(nullptr, 15, source, &destination), false); + BOOST_CHECK_EQUAL(parseXPFPayload(nullptr, 37, source, &destination), false); + BOOST_CHECK_EQUAL(parseXPFPayload(nullptr, 39, source, &destination), false); + } + + + { + /* invalid protocol */ + ComboAddress source("[2001:db8::1]:42"); + ComboAddress destination("[::1]:65535"); + + auto payload = generateXPFPayload(true, source, destination); + /* set protocol to 0 */ + payload.at(1) = 0; + + ComboAddress parsedSource; + ComboAddress parsedDestination; + BOOST_CHECK_EQUAL(parseXPFPayload(payload.c_str(), payload.size(), parsedSource, &parsedDestination), false); + } + + { + /* invalid version */ + ComboAddress source("[2001:db8::1]:42"); + ComboAddress destination("[::1]:65535"); + + auto payload = generateXPFPayload(true, source, destination); + /* set version to 0 */ + payload.at(0) = 0; + + ComboAddress parsedSource; + ComboAddress parsedDestination; + BOOST_CHECK_EQUAL(parseXPFPayload(payload.c_str(), payload.size(), parsedSource, &parsedDestination), false); + } + + { + /* payload too short (v6 size with v4 payload) */ + ComboAddress source("192.0.2.1:53"); + ComboAddress destination("192.0.2.2:65535"); + + + auto payload = generateXPFPayload(true, source, destination); + /* set version to 6 */ + payload.at(0) = 6; + + ComboAddress parsedSource; + ComboAddress parsedDestination; + BOOST_CHECK_EQUAL(parseXPFPayload(payload.c_str(), payload.size(), parsedSource, &parsedDestination), false); + } + + { + /* payload too long (v6 size with v4 payload) */ + ComboAddress source("[2001:db8::1]:42"); + ComboAddress destination("[::1]:65535"); + + + auto payload = generateXPFPayload(true, source, destination); + /* set version to 4 */ + payload.at(0) = 4; + + ComboAddress parsedSource; + ComboAddress parsedDestination; + BOOST_CHECK_EQUAL(parseXPFPayload(payload.c_str(), payload.size(), parsedSource, &parsedDestination), false); + } + + { + /* v4 payload over UDP */ + ComboAddress source("192.0.2.1:53"); + ComboAddress destination("192.0.2.2:65535"); + + auto payload = generateXPFPayload(false, source, destination); + BOOST_CHECK_EQUAL(payload.size(), 14); + BOOST_CHECK_EQUAL(payload.at(0), 4); + BOOST_CHECK_EQUAL(payload.at(1), 17); + + ComboAddress parsedSource; + BOOST_CHECK(parseXPFPayload(payload.c_str(), payload.size(), parsedSource, nullptr)); + BOOST_CHECK_EQUAL(parsedSource.toStringWithPort(), source.toStringWithPort()); + } + +} + + +BOOST_AUTO_TEST_SUITE_END() diff --git a/pdns/recursordist/xpf.cc b/pdns/recursordist/xpf.cc new file mode 120000 index 0000000000..98ab7228cd --- /dev/null +++ b/pdns/recursordist/xpf.cc @@ -0,0 +1 @@ +../xpf.cc \ No newline at end of file diff --git a/pdns/recursordist/xpf.hh b/pdns/recursordist/xpf.hh new file mode 120000 index 0000000000..023c8c4796 --- /dev/null +++ b/pdns/recursordist/xpf.hh @@ -0,0 +1 @@ +../xpf.hh \ No newline at end of file diff --git a/pdns/xpf.cc b/pdns/xpf.cc new file mode 100644 index 0000000000..8b093e44c8 --- /dev/null +++ b/pdns/xpf.cc @@ -0,0 +1,120 @@ +/* + * This file is part of PowerDNS or dnsdist. + * Copyright -- PowerDNS.COM B.V. and its contributors + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of version 2 of the GNU General Public License as + * published by the Free Software Foundation. + * + * In addition, for the avoidance of any doubt, permission is granted to + * link this program with OpenSSL and to (re)distribute the binaries + * produced as the result of such linking. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "xpf.hh" + +std::string generateXPFPayload(bool tcp, const ComboAddress& remote, const ComboAddress& local) +{ + if (remote.sin4.sin_family != local.sin4.sin_family) { + throw std::runtime_error("The XPF local and remote addresses must be of the same family"); + } + + std::string ret; + const uint8_t version = remote.isIPv4() ? 4 : 6; + const uint8_t protocol = tcp ? 6 : 17; + const size_t addrSize = remote.isIPv4() ? sizeof(remote.sin4.sin_addr.s_addr) : sizeof(remote.sin6.sin6_addr.s6_addr); + const uint16_t remotePort = remote.sin4.sin_port; + const uint16_t localPort = local.sin4.sin_port; + + ret.reserve(sizeof(version) + sizeof(protocol) + (addrSize * 2) + sizeof(remotePort) + sizeof(localPort)); + + ret.append(reinterpret_cast(&version), sizeof(version)); + ret.append(reinterpret_cast(&protocol), sizeof(protocol)); + + if (remote.isIPv4()) { + assert(addrSize == sizeof(remote.sin4.sin_addr.s_addr)); + ret.append(reinterpret_cast(&remote.sin4.sin_addr.s_addr), addrSize); + } + else { + assert(addrSize == sizeof(remote.sin6.sin6_addr.s6_addr)); + ret.append(reinterpret_cast(&remote.sin6.sin6_addr.s6_addr), addrSize); + } + + if (remote.isIPv4()) { + assert(addrSize == sizeof(local.sin4.sin_addr.s_addr)); + ret.append(reinterpret_cast(&local.sin4.sin_addr.s_addr), addrSize); + } + else { + assert(addrSize == sizeof(local.sin6.sin6_addr.s6_addr)); + ret.append(reinterpret_cast(&local.sin6.sin6_addr.s6_addr), addrSize); + } + + ret.append(reinterpret_cast(&remotePort), sizeof(remotePort)); + ret.append(reinterpret_cast(&localPort), sizeof(localPort)); + + return ret; +} + +bool parseXPFPayload(const char* payload, size_t len, ComboAddress& source, ComboAddress* destination) +{ + static const size_t addr4Size = sizeof(source.sin4.sin_addr.s_addr); + static const size_t addr6Size = sizeof(source.sin6.sin6_addr.s6_addr); + uint8_t version; + uint8_t protocol; + uint16_t sourcePort; + uint16_t destinationPort; + + if (len != (sizeof(version) + sizeof(protocol) + (addr4Size * 2) + sizeof(sourcePort) + sizeof(destinationPort)) && + len != (sizeof(version) + sizeof(protocol) + (addr6Size * 2) + sizeof(sourcePort) + sizeof(destinationPort))) { + return false; + } + + size_t pos = 0; + + memcpy(&version, payload + pos, sizeof(version)); + pos += sizeof(version); + + if (version != 4 && version != 6) { + return false; + } + + memcpy(&protocol, payload + pos, sizeof(protocol)); + pos += sizeof(protocol); + + if (protocol != 6 && protocol != 17) { + return false; + } + + const size_t addrSize = version == 4 ? sizeof(source.sin4.sin_addr.s_addr) : sizeof(source.sin6.sin6_addr.s6_addr); + if (len - pos != ((addrSize * 2) + sizeof(sourcePort) + sizeof(destinationPort))) { + return false; + } + + source = makeComboAddressFromRaw(version, payload + pos, addrSize); + pos += addrSize; + if (destination != nullptr) { + *destination = makeComboAddressFromRaw(version, payload + pos, addrSize); + } + pos += addrSize; + + memcpy(&sourcePort, payload + pos, sizeof(sourcePort)); + pos += sizeof(sourcePort); + source.sin4.sin_port = sourcePort; + + memcpy(&destinationPort, payload + pos, sizeof(destinationPort)); + pos += sizeof(destinationPort); + if (destination != nullptr) { + destination->sin4.sin_port = destinationPort; + } + + return true; +} diff --git a/pdns/xpf.hh b/pdns/xpf.hh new file mode 100644 index 0000000000..e3e18c3749 --- /dev/null +++ b/pdns/xpf.hh @@ -0,0 +1,29 @@ +/* + * This file is part of PowerDNS or dnsdist. + * Copyright -- PowerDNS.COM B.V. and its contributors + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of version 2 of the GNU General Public License as + * published by the Free Software Foundation. + * + * In addition, for the avoidance of any doubt, permission is granted to + * link this program with OpenSSL and to (re)distribute the binaries + * produced as the result of such linking. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#pragma once + +#include + +std::string generateXPFPayload(bool tcp, const ComboAddress& remote, const ComboAddress& local); +bool parseXPFPayload(const char* payload, size_t len, ComboAddress& source, ComboAddress* destination); + diff --git a/regression-tests.dnsdist/test_XPF.py b/regression-tests.dnsdist/test_XPF.py new file mode 100644 index 0000000000..261c5172b3 --- /dev/null +++ b/regression-tests.dnsdist/test_XPF.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python + +import dns +from dnsdisttests import DNSDistTest + +class XPFTest(DNSDistTest): + """ + dnsdist is configured to add XPF to the query + """ + + _xpfCode = 65422 + _config_template = """ + newServer{address="127.0.0.1:%s", addXPF=true} + """ + + def checkMessageHasXPF(self, msg, expectedValue): + self.assertGreaterEqual(len(msg.additional), 1) + + found = False + for add in msg.additional: + if add.rdtype == self._xpfCode: + found = True + self.assertEquals(add.rdclass, dns.rdataclass.IN) + self.assertEquals(add.ttl, 0) + xpfData = add.to_rdataset()[0].to_text() + # skip the ports + self.assertEquals(xpfData[:26], expectedValue[:26]) + + self.assertTrue(found) + + def testXPF(self): + """ + XPF + """ + name = 'xpf.tests.powerdns.com.' + query = dns.message.make_query(name, 'A', 'IN') + + expectedQuery = dns.message.make_query(name, 'A', 'IN') + # 0x04 is IPv4, 0x11 (17) is UDP then 127.0.0.1 as source and destination + # and finally the ports, zeroed because we have no way to know them beforehand + xpfData = "\# 14 04117f0000017f00000100000000" + rdata = dns.rdata.from_text(dns.rdataclass.IN, self._xpfCode, xpfData) + rrset = dns.rrset.from_rdata(name, 60, rdata) + expectedQuery.additional.append(rrset) + + response = dns.message.make_response(expectedQuery) + + (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response) + self.assertTrue(receivedQuery) + self.assertTrue(receivedResponse) + receivedQuery.id = expectedQuery.id + receivedResponse.id = response.id + + self.assertEquals(receivedQuery, expectedQuery) + self.checkMessageHasXPF(receivedQuery, xpfData) + self.assertEquals(response, receivedResponse) + + expectedQuery = dns.message.make_query(name, 'A', 'IN') + # 0x04 is IPv4, 0x06 (6) is TCP then 127.0.0.1 as source and destination + # and finally the ports, zeroed because we have no way to know them beforehand + xpfData = "\# 14 04067f0000017f00000100000000" + rdata = dns.rdata.from_text(dns.rdataclass.IN, self._xpfCode, xpfData) + rrset = dns.rrset.from_rdata(name, 60, rdata) + expectedQuery.additional.append(rrset) + + response = dns.message.make_response(expectedQuery) + + (receivedQuery, receivedResponse) = self.sendTCPQuery(query, response) + self.assertTrue(receivedQuery) + self.assertTrue(receivedResponse) + receivedQuery.id = expectedQuery.id + receivedResponse.id = response.id + + self.assertEquals(receivedQuery, expectedQuery) + self.checkMessageHasXPF(receivedQuery, xpfData) + self.assertEquals(response, receivedResponse)