From: Otto Moerbeek Date: Tue, 1 Mar 2022 14:32:59 +0000 (+0100) Subject: proxyMapping: a table based approach to let the recursor know the actual IP address... X-Git-Tag: rec-4.7.0-beta1~45^2 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=refs%2Fpull%2F11396%2Fhead;p=thirdparty%2Fpdns.git proxyMapping: a table based approach to let the recursor know the actual IP address it should use for ACLS etc --- diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index 2cb4babb2b..8ea552de0d 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -1301,6 +1301,7 @@ progid protobuf protozero providername +proxymapping proxyprotocol proxyprotocolvalues pseudonymize diff --git a/pdns/pdns_recursor.cc b/pdns/pdns_recursor.cc index e82b2c26d0..c696a76703 100644 --- a/pdns/pdns_recursor.cc +++ b/pdns/pdns_recursor.cc @@ -923,7 +923,7 @@ void startDoResolve(void* p) #ifdef HAVE_FSTRM sr.setFrameStreamServers(t_frameStreamServers); #endif - sr.setQuerySource(dc->d_source, g_useIncomingECS && !dc->d_ednssubnet.source.empty() ? boost::optional(dc->d_ednssubnet) : boost::none); + sr.setQuerySource(dc->d_mappedSource, g_useIncomingECS && !dc->d_ednssubnet.source.empty() ? boost::optional(dc->d_ednssubnet) : boost::none); sr.setQueryReceivedOverTCP(dc->d_tcp); bool tracedQuery = false; // we could consider letting Lua know about this too @@ -1516,11 +1516,23 @@ void startDoResolve(void* p) pbMessage.setQueryTime(dc->d_now.tv_sec, dc->d_now.tv_usec); } pbMessage.setMessageIdentity(dc->d_uuid); - pbMessage.setSocketFamily(dc->d_source.sin4.sin_family); pbMessage.setSocketProtocol(dc->d_tcp ? pdns::ProtoZero::Message::TransportProtocol::TCP : pdns::ProtoZero::Message::TransportProtocol::UDP); - Netmask requestorNM(dc->d_source, dc->d_source.sin4.sin_family == AF_INET ? luaconfsLocal->protobufMaskV4 : luaconfsLocal->protobufMaskV6); - ComboAddress requestor = requestorNM.getMaskedNetwork(); - pbMessage.setFrom(requestor); + + if (!luaconfsLocal->protobufExportConfig.logMappedFrom) { + pbMessage.setSocketFamily(dc->d_source.sin4.sin_family); + Netmask requestorNM(dc->d_source, dc->d_source.sin4.sin_family == AF_INET ? luaconfsLocal->protobufMaskV4 : luaconfsLocal->protobufMaskV6); + ComboAddress requestor = requestorNM.getMaskedNetwork(); + pbMessage.setFrom(requestor); + pbMessage.setFromPort(dc->d_source.getPort()); + } + else { + pbMessage.setSocketFamily(dc->d_mappedSource.sin4.sin_family); + Netmask requestorNM(dc->d_mappedSource, dc->d_mappedSource.sin4.sin_family == AF_INET ? luaconfsLocal->protobufMaskV4 : luaconfsLocal->protobufMaskV6); + ComboAddress requestor = requestorNM.getMaskedNetwork(); + pbMessage.setFrom(requestor); + pbMessage.setFromPort(dc->d_mappedSource.getPort()); + } + pbMessage.setTo(dc->d_destination); pbMessage.setId(dc->d_mdp.d_header.id); @@ -1529,7 +1541,6 @@ void startDoResolve(void* p) pbMessage.setRequestorId(dq.requestorId); pbMessage.setDeviceId(dq.deviceId); pbMessage.setDeviceName(dq.deviceName); - pbMessage.setFromPort(dc->d_source.getPort()); pbMessage.setToPort(dc->d_destination.getPort()); for (const auto& m : dq.meta) { @@ -1777,7 +1788,12 @@ bool expectProxyProtocol(const ComboAddress& from) return g_proxyProtocolACL.match(from); } -static string* doProcessUDPQuestion(const std::string& question, const ComboAddress& fromaddr, const ComboAddress& destaddr, ComboAddress source, ComboAddress destination, struct timeval tv, int fd, std::vector& proxyProtocolValues, RecEventTrace& eventTrace) +// fromaddr: the address the query is coming from +// destaddr: the address the query was received on +// source: the address we assume the query is coming from, might be set by proxy protocol +// destination: the address we assume the query was sent to, might be set by proxy protocol +// mappedSource: the address we assume the query is coming from. Differs from source if table based mapping has been applied +static string* doProcessUDPQuestion(const std::string& question, const ComboAddress& fromaddr, const ComboAddress& destaddr, ComboAddress source, ComboAddress destination, const ComboAddress& mappedSource, struct timeval tv, int fd, std::vector& proxyProtocolValues, RecEventTrace& eventTrace) { ++(RecThreadInfo::self().numberOfDistributedQueries); gettimeofday(&g_now, nullptr); @@ -1900,7 +1916,7 @@ static string* doProcessUDPQuestion(const std::string& question, const ComboAddr RecursorPacketCache::OptPBData pbData{boost::none}; if (t_protobufServers) { if (logQuery && !(luaconfsLocal->protobufExportConfig.taggedOnly && policyTags.empty())) { - protobufLogQuery(luaconfsLocal, uniqueId, source, destination, ednssubnet.source, false, dh->id, question.size(), qname, qtype, qclass, policyTags, requestorId, deviceId, deviceName, meta); + protobufLogQuery(luaconfsLocal, uniqueId, source, destination, mappedSource, ednssubnet.source, false, dh->id, question.size(), qname, qtype, qclass, policyTags, requestorId, deviceId, deviceName, meta); } } @@ -1932,7 +1948,7 @@ static string* doProcessUDPQuestion(const std::string& question, const ComboAddr eventTrace.add(RecEventTrace::AnswerSent); if (t_protobufServers && logResponse && !(luaconfsLocal->protobufExportConfig.taggedOnly && pbData && !pbData->d_tagged)) { - protobufLogResponse(dh, luaconfsLocal, pbData, tv, false, source, destination, ednssubnet, uniqueId, requestorId, deviceId, deviceName, meta, eventTrace); + protobufLogResponse(dh, luaconfsLocal, pbData, tv, false, source, destination, mappedSource, ednssubnet, uniqueId, requestorId, deviceId, deviceName, meta, eventTrace); } if (eventTrace.enabled() && SyncRes::s_event_trace_enabled & SyncRes::event_trace_to_log) { @@ -2014,10 +2030,11 @@ static string* doProcessUDPQuestion(const std::string& question, const ComboAddr dc->setSocket(fd); dc->d_tag = ctag; dc->d_qhash = qhash; - dc->setRemote(fromaddr); - dc->setSource(source); - dc->setLocal(destaddr); - dc->setDestination(destination); + dc->setRemote(fromaddr); // the address the query is coming from + dc->setSource(source); // the address we assume the query is coming from, might be set by proxy protocol + dc->setLocal(destaddr); // the address the query was received on + dc->setDestination(destination); // the address we assume the query is sent to, might be set by proxy protocol + dc->setMappedSource(mappedSource); // the address we assume the query is coming from. Differs from source if table-based mapping has been applied dc->d_tcp = false; dc->d_ecsFound = ecsFound; dc->d_ecsParsed = ecsParsed; @@ -2052,9 +2069,9 @@ static void handleNewUDPQuestion(int fd, FDMultiplexer::funcparam_t& var) ssize_t len; static const size_t maxIncomingQuerySize = g_proxyProtocolACL.empty() ? 512 : (512 + g_proxyProtocolMaximumSize); static thread_local std::string data; - ComboAddress fromaddr; - ComboAddress source; - ComboAddress destination; + ComboAddress fromaddr; // the address the query is coming from + ComboAddress source; // the address we assume the query is coming from, might be set by proxy protocol + ComboAddress destination; // the address we assume the query was sent to, might be set by proxy protocol struct msghdr msgh; struct iovec iov; cmsgbuf_aligned cbuf; @@ -2126,14 +2143,19 @@ static void handleNewUDPQuestion(int fd, FDMultiplexer::funcparam_t& var) if (!proxyProto) { source = fromaddr; } - + ComboAddress mappedSource = source; + if (t_proxyMapping) { + if (auto it = t_proxyMapping->lookup(source)) { + mappedSource = it->second; + } + } if (t_remotes) { t_remotes->push_back(fromaddr); } - if (t_allowFrom && !t_allowFrom->match(&source)) { + if (t_allowFrom && !t_allowFrom->match(&mappedSource)) { if (!g_quiet) { - g_log << Logger::Error << "[" << MT->getTid() << "] dropping UDP query from " << source.toString() << ", address not matched by allow-from" << endl; + g_log << Logger::Error << "[" << MT->getTid() << "] dropping UDP query from " << mappedSource.toString() << ", address not matched by allow-from" << endl; } g_stats.unauthorizedUDP++; @@ -2174,9 +2196,9 @@ static void handleNewUDPQuestion(int fd, FDMultiplexer::funcparam_t& var) } else { if (dh->opcode == Opcode::Notify) { - if (!t_allowNotifyFrom || !t_allowNotifyFrom->match(&source)) { + if (!t_allowNotifyFrom || !t_allowNotifyFrom->match(&mappedSource)) { if (!g_quiet) { - g_log << Logger::Error << "[" << MT->getTid() << "] dropping UDP NOTIFY from " << source.toString() << ", address not matched by allow-notify-from" << endl; + g_log << Logger::Error << "[" << MT->getTid() << "] dropping UDP NOTIFY from " << mappedSource.toString() << ", address not matched by allow-notify-from" << endl; } g_stats.sourceDisallowedNotify++; @@ -2186,7 +2208,7 @@ static void handleNewUDPQuestion(int fd, FDMultiplexer::funcparam_t& var) struct timeval tv = {0, 0}; HarvestTimestamp(&msgh, &tv); - ComboAddress dest; + ComboAddress dest; // the address the query was sent to to dest.reset(); // this makes sure we ignore this address if not returned by recvmsg above auto loc = rplookup(g_listenSocketsAddresses, fd); if (HarvestDestinationAddress(&msgh, &dest)) { @@ -2211,12 +2233,12 @@ static void handleNewUDPQuestion(int fd, FDMultiplexer::funcparam_t& var) if (RecThreadInfo::weDistributeQueries()) { std::string localdata = data; - distributeAsyncFunction(data, [localdata, fromaddr, dest, source, destination, tv, fd, proxyProtocolValues, eventTrace]() mutable { - return doProcessUDPQuestion(localdata, fromaddr, dest, source, destination, tv, fd, proxyProtocolValues, eventTrace); + distributeAsyncFunction(data, [localdata, fromaddr, dest, source, destination, mappedSource, tv, fd, proxyProtocolValues, eventTrace]() mutable { + return doProcessUDPQuestion(localdata, fromaddr, dest, source, destination, mappedSource, tv, fd, proxyProtocolValues, eventTrace); }); } else { - doProcessUDPQuestion(data, fromaddr, dest, source, destination, tv, fd, proxyProtocolValues, eventTrace); + doProcessUDPQuestion(data, fromaddr, dest, source, destination, mappedSource, tv, fd, proxyProtocolValues, eventTrace); } } } diff --git a/pdns/rec-lua-conf.cc b/pdns/rec-lua-conf.cc index 8f730e4642..70d2017175 100644 --- a/pdns/rec-lua-conf.cc +++ b/pdns/rec-lua-conf.cc @@ -146,6 +146,10 @@ static void parseProtobufOptions(boost::optional vars, Protob config.logResponses = boost::get((*vars)["logResponses"]); } + if (vars->count("logMappedFrom")) { + config.logMappedFrom = boost::get((*vars)["logMappedFrom"]); + } + if (vars->count("exportTypes")) { config.exportTypes.clear(); @@ -350,7 +354,7 @@ public: } }; -void loadRecursorLuaConfig(const std::string& fname, luaConfigDelayedThreads& delayedThreads) +void loadRecursorLuaConfig(const std::string& fname, luaConfigDelayedThreads& delayedThreads, ProxyMapping& proxyMapping) { LuaConfigItems lci; @@ -721,6 +725,20 @@ void loadRecursorLuaConfig(const std::string& fname, luaConfigDelayedThreads& de lci.allowAdditionalQTypes.insert_or_assign(qtype, pair(targets, mode)); }); + Lua->writeFunction("addProxyMapping", [&proxyMapping](const string& netmaskArg, const string& addressArg) { + try { + Netmask netmask(netmaskArg); + ComboAddress address(addressArg); + proxyMapping.insert_or_assign(netmask, address); + } + catch (std::exception& e) { + g_log << Logger::Error << "Error processing addProxyMapping: " << e.what() << endl; + } + catch (PDNSException& e) { + g_log << Logger::Error << "Error processing addProxyMapping: " << e.reason << endl; + } + }); + try { Lua->executeCode(ifs); g_luaconfs.setState(std::move(lci)); diff --git a/pdns/rec-lua-conf.hh b/pdns/rec-lua-conf.hh index a88ae913db..482ffaa499 100644 --- a/pdns/rec-lua-conf.hh +++ b/pdns/rec-lua-conf.hh @@ -40,6 +40,7 @@ struct ProtobufExportConfig bool logQueries{true}; bool logResponses{true}; bool taggedOnly{false}; + bool logMappedFrom{false}; }; struct FrameStreamExportConfig @@ -71,6 +72,8 @@ enum class AdditionalMode : uint8_t ResolveDeferred }; +using ProxyMapping = NetmaskTree; + class LuaConfigItems { public: @@ -101,5 +104,5 @@ struct luaConfigDelayedThreads std::vector, boost::optional, bool, uint32_t, size_t, TSIGTriplet, size_t, ComboAddress, uint16_t, uint32_t, std::shared_ptr, std::string>> rpzPrimaryThreads; }; -void loadRecursorLuaConfig(const std::string& fname, luaConfigDelayedThreads& delayedThreads); +void loadRecursorLuaConfig(const std::string& fname, luaConfigDelayedThreads& delayedThreads, ProxyMapping&); void startLuaConfigDelayedThreads(const luaConfigDelayedThreads& delayedThreads, uint64_t generation); diff --git a/pdns/rec_channel_rec.cc b/pdns/rec_channel_rec.cc index b5d41b77dd..88b2e81b69 100644 --- a/pdns/rec_channel_rec.cc +++ b/pdns/rec_channel_rec.cc @@ -1873,6 +1873,12 @@ static string setEventTracing(T begin, T end) } } +static void* pleaseSupplantProxyMapping(std::shared_ptr pm) +{ + t_proxyMapping = pm; + return nullptr; +} + RecursorControlChannel::Answer RecursorControlParser::getAnswer(int s, const string& question, RecursorControlParser::func_t** command) { *command = nop; @@ -2012,8 +2018,11 @@ RecursorControlChannel::Answer RecursorControlParser::getAnswer(int s, const str try { luaConfigDelayedThreads delayedLuaThreads; - loadRecursorLuaConfig(::arg()["lua-config-file"], delayedLuaThreads); + ProxyMapping proxyMapping; + loadRecursorLuaConfig(::arg()["lua-config-file"], delayedLuaThreads, proxyMapping); startLuaConfigDelayedThreads(delayedLuaThreads, g_luaconfs.getCopy().generation); + std::shared_ptr ptr = proxyMapping.empty() ? nullptr : std::make_shared(proxyMapping); + broadcastFunction([=] { return pleaseSupplantProxyMapping(ptr); }); g_log << Logger::Warning << "Reloaded Lua configuration file '" << ::arg()["lua-config-file"] << "', requested via control channel" << endl; return {0, "Reloaded Lua configuration file '" + ::arg()["lua-config-file"] + "'\n"}; } diff --git a/pdns/recursordist/docs/lua-config/index.rst b/pdns/recursordist/docs/lua-config/index.rst index a602f22d67..36a3ec7a2e 100644 --- a/pdns/recursordist/docs/lua-config/index.rst +++ b/pdns/recursordist/docs/lua-config/index.rst @@ -11,5 +11,6 @@ Since version 4.0.0, the PowerDNS Recursor supports additional configuration opt sortlist ztc additionals + proxymapping In addition, :func:`pdnslog` together with ``pdns.loglevels`` is also supported in the Lua configuration file. diff --git a/pdns/recursordist/docs/lua-config/protobuf.rst b/pdns/recursordist/docs/lua-config/protobuf.rst index 21a222f89e..dcb5f37944 100644 --- a/pdns/recursordist/docs/lua-config/protobuf.rst +++ b/pdns/recursordist/docs/lua-config/protobuf.rst @@ -37,6 +37,10 @@ Protobuf export to a server is enabled using the ``protobufServer()`` directive: The values in ``exportTypes`` can be numeric as well as strings. Symbolic names from ``pdns`` can be used, e.g. ``exportTypes = { pdns.A, pdns.AAAA, pdns.CNAME }`` + .. versionadded:: 4.7.0 + + * ``logMappedFrom=false``: bool - whether to log the remote address before substitution by :ref:`proxymapping` (the default) or after + .. function:: protobufServer(server [[[[[[[, timeout=2], maxQueuedEntries=100], reconnectWaitTime=1], maskV4=32], maskV6=128], asyncConnect=false], taggedOnly=false]) .. deprecated:: 4.2.0 diff --git a/pdns/recursordist/docs/lua-config/proxymapping.rst b/pdns/recursordist/docs/lua-config/proxymapping.rst new file mode 100644 index 0000000000..888494633f --- /dev/null +++ b/pdns/recursordist/docs/lua-config/proxymapping.rst @@ -0,0 +1,51 @@ +.. _proxymapping: + +Table Based Proxy Mapping +========================= +Starting with version 4.7.0, the PowerDNS Recursor has the ability to map source IP addresses to alternative addresses, which is for example useful when some clients reach the recursor via a reverse-proxy. +The mapped address is used internally for ACL and similar checks. +If the :ref:`setting-proxy-protocol-from` is also used, the substitution is done on the source address specified in the proxy protocol header. + +Depending on context, the incoming address can be + +The physical address ``P`` + the physical address the query is received on. +The source address ``S`` + the source address as specified in the Proxy protocol +The mapped address ``M`` + the source address mapped by Table Based Proxy Mapping + +``S equals P`` if no Proxy Protocol is used. + +``M equals S`` if no Table Based Proxy Mapping is used. + +``P`` determines if the Proxy Protocol is used (:ref:`setting-proxy-protocol-from`). + +``S`` is passed to Lua functions and RPZ processing + +``M`` is used for incoming ACL checking (:ref:`setting-allow-from`) and to determine the ECS processing (:ref:`setting-ecs-add-for`). + +An example use: + +.. code-block:: Lua + + addProxyMapping("127.0.0.0/24", "203.0.113.1") + addProxyMapping("10.0.0.0/8", "203.0.113.2") + + +The following function is available to configure table based proxy mapping. +Reloading the Lua configuration will replace the current configuration with the new one. +If the subnets specified in multiple :func:`addProxyMapping` calls overlap, the most specific one is used. +By default, the address *before* mapping ``S`` is used for internal logging and ``Protobuf`` messages. +See :func:`protobufServer` on how to tune the source address logged in ``Protobuf` messages. + +.. function:: addProxyMapping(subnet, ip) + + .. versionadded:: 4.7.0 + + Specify a table based mapping for a subnet. + + :param string subnet: a subnet to match + :param string ip: the IP address or IPaddress port combination to match the subnet to. + + diff --git a/pdns/recursordist/rec-main.cc b/pdns/recursordist/rec-main.cc index cd1116d75f..9018dbe2b8 100644 --- a/pdns/recursordist/rec-main.cc +++ b/pdns/recursordist/rec-main.cc @@ -103,6 +103,9 @@ deferredAdd_t g_deferredAdds; and finally the workers */ std::vector RecThreadInfo::s_threadInfos; +std::shared_ptr g_proxyMapping; // new threads needs this to be setup +thread_local std::shared_ptr t_proxyMapping; + bool RecThreadInfo::s_weDistributeQueries; // if true, 1 or more threads listen on the incoming query sockets and distribute them to workers unsigned int RecThreadInfo::s_numDistributorThreads; unsigned int RecThreadInfo::s_numWorkerThreads; @@ -443,15 +446,23 @@ bool checkOutgoingProtobufExport(LocalStateHolder& luaconfsLocal return true; } -void protobufLogQuery(LocalStateHolder& luaconfsLocal, const boost::uuids::uuid& uniqueId, const ComboAddress& remote, const ComboAddress& local, const Netmask& ednssubnet, bool tcp, uint16_t id, size_t len, const DNSName& qname, uint16_t qtype, uint16_t qclass, const std::unordered_set& policyTags, const std::string& requestorId, const std::string& deviceId, const std::string& deviceName, const std::map& meta) +void protobufLogQuery(LocalStateHolder& luaconfsLocal, const boost::uuids::uuid& uniqueId, const ComboAddress& remote, const ComboAddress& local, const ComboAddress& mappedRemote, const Netmask& ednssubnet, bool tcp, uint16_t id, size_t len, const DNSName& qname, uint16_t qtype, uint16_t qclass, const std::unordered_set& policyTags, const std::string& requestorId, const std::string& deviceId, const std::string& deviceName, const std::map& meta) { if (!t_protobufServers) { return; } - Netmask requestorNM(remote, remote.sin4.sin_family == AF_INET ? luaconfsLocal->protobufMaskV4 : luaconfsLocal->protobufMaskV6); - ComboAddress requestor = requestorNM.getMaskedNetwork(); - requestor.setPort(remote.getPort()); + ComboAddress requestor; + if (!luaconfsLocal->protobufExportConfig.logMappedFrom) { + Netmask requestorNM(remote, remote.sin4.sin_family == AF_INET ? luaconfsLocal->protobufMaskV4 : luaconfsLocal->protobufMaskV6); + requestor = requestorNM.getMaskedNetwork(); + requestor.setPort(remote.getPort()); + } + else { + Netmask requestorNM(mappedRemote, mappedRemote.sin4.sin_family == AF_INET ? luaconfsLocal->protobufMaskV4 : luaconfsLocal->protobufMaskV6); + requestor = requestorNM.getMaskedNetwork(); + requestor.setPort(mappedRemote.getPort()); + } pdns::ProtoZero::RecMessage m{128, std::string::size_type(policyTags.empty() ? 0 : 64)}; // It's a guess m.setType(pdns::ProtoZero::Message::MessageType::DNSQueryType); @@ -490,6 +501,7 @@ void protobufLogResponse(pdns::ProtoZero::RecMessage& message) void protobufLogResponse(const struct dnsheader* dh, LocalStateHolder& luaconfsLocal, const RecursorPacketCache::OptPBData& pbData, const struct timeval& tv, bool tcp, const ComboAddress& source, const ComboAddress& destination, + const ComboAddress& mappedSource, const EDNSSubnetOpts& ednssubnet, const boost::uuids::uuid& uniqueId, const string& requestorId, const string& deviceId, const string& deviceName, const std::map& meta, @@ -512,10 +524,19 @@ void protobufLogResponse(const struct dnsheader* dh, LocalStateHolderprotobufMaskV4 : luaconfsLocal->protobufMaskV6); - ComboAddress requestor = requestorNM.getMaskedNetwork(); + if (!luaconfsLocal->protobufExportConfig.logMappedFrom) { + Netmask requestorNM(source, source.sin4.sin_family == AF_INET ? luaconfsLocal->protobufMaskV4 : luaconfsLocal->protobufMaskV6); + auto requestor = requestorNM.getMaskedNetwork(); + pbMessage.setFrom(requestor); + pbMessage.setFromPort(source.getPort()); + } + else { + Netmask requestorNM(mappedSource, mappedSource.sin4.sin_family == AF_INET ? luaconfsLocal->protobufMaskV4 : luaconfsLocal->protobufMaskV6); + auto requestor = requestorNM.getMaskedNetwork(); + pbMessage.setFrom(requestor); + pbMessage.setFromPort(mappedSource.getPort()); + } pbMessage.setMessageIdentity(uniqueId); - pbMessage.setFrom(requestor); pbMessage.setTo(destination); pbMessage.setSocketProtocol(tcp ? pdns::ProtoZero::Message::TransportProtocol::TCP : pdns::ProtoZero::Message::TransportProtocol::UDP); pbMessage.setId(dh->id); @@ -525,7 +546,6 @@ void protobufLogResponse(const struct dnsheader* dh, LocalStateHolder(proxyMapping); } catch (PDNSException& e) { g_log << Logger::Error << "Cannot load Lua configuration: " << e.reason << endl; @@ -2024,6 +2047,7 @@ static void recursorThread() t_allowNotifyFor = g_initialAllowNotifyFor; t_udpclientsocks = std::make_unique(); t_tcpClientCounts = std::make_unique(); + t_proxyMapping = g_proxyMapping; if (threadInfo.isHandler()) { if (!primeHints()) { diff --git a/pdns/recursordist/rec-main.hh b/pdns/recursordist/rec-main.hh index da1b8d82fd..89eb0a93d9 100644 --- a/pdns/recursordist/rec-main.hh +++ b/pdns/recursordist/rec-main.hh @@ -58,21 +58,29 @@ struct DNSComboWriter { } + // The address the query is coming from void setRemote(const ComboAddress& sa) { d_remote = sa; } + // The address we assume the query is coming from, might be set by proxy protocol void setSource(const ComboAddress& sa) { d_source = sa; } + void setMappedSource(const ComboAddress& sa) + { + d_mappedSource = sa; + } + void setLocal(const ComboAddress& sa) { d_local = sa; } + // The address we assume the query is sent to, might be set by proxy protocol void setDestination(const ComboAddress& sa) { d_destination = sa; @@ -83,6 +91,7 @@ struct DNSComboWriter d_socket = sock; } + // get a string repesentation of the client address, including proxy info if applicable string getRemote() const { if (d_source == d_remote) { @@ -94,18 +103,12 @@ struct DNSComboWriter std::vector d_proxyProtocolValues; MOADNSParser d_mdp; struct timeval d_now; - /* 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; + + ComboAddress d_remote; // the address the query is coming from + ComboAddress d_source; // the address we assume the query is coming from, might be set by proxy protocol + ComboAddress d_local; // the address we received the query on + ComboAddress d_destination; // the address we assume the query is sent to, might be set by proxy protocol + ComboAddress d_mappedSource; // the source address after being mapped by table based proxy mapping RecEventTrace d_eventTrace; boost::uuids::uuid d_uuid; string d_requestorId; @@ -224,6 +227,8 @@ extern string g_programname; extern string g_pidfname; extern RecursorControlChannel g_rcc; // only active in the handler thread +extern thread_local std::shared_ptr t_proxyMapping; + #ifdef NOD_ENABLED extern bool g_nodEnabled; extern DNSName g_nodLookupDomain; @@ -495,7 +500,7 @@ bool checkFrameStreamExport(LocalStateHolder& luaconfsLocal); void getQNameAndSubnet(const std::string& question, DNSName* dnsname, uint16_t* qtype, uint16_t* qclass, bool& foundECS, EDNSSubnetOpts* ednssubnet, EDNSOptionViewMap* options, bool& foundXPF, ComboAddress* xpfSource, ComboAddress* xpfDest); -void protobufLogQuery(LocalStateHolder& luaconfsLocal, const boost::uuids::uuid& uniqueId, const ComboAddress& remote, const ComboAddress& local, const Netmask& ednssubnet, bool tcp, uint16_t id, size_t len, const DNSName& qname, uint16_t qtype, uint16_t qclass, const std::unordered_set& policyTags, const std::string& requestorId, const std::string& deviceId, const std::string& deviceName, const std::map& meta); +void protobufLogQuery(LocalStateHolder& luaconfsLocal, const boost::uuids::uuid& uniqueId, const ComboAddress& remote, const ComboAddress& local, const ComboAddress& mappedSource, const Netmask& ednssubnet, bool tcp, uint16_t id, size_t len, const DNSName& qname, uint16_t qtype, uint16_t qclass, const std::unordered_set& policyTags, const std::string& requestorId, const std::string& deviceId, const std::string& deviceName, const std::map& meta); bool isAllowNotifyForZone(DNSName qname); bool checkForCacheHit(bool qnameParsed, unsigned int tag, const string& data, DNSName& qname, uint16_t& qtype, uint16_t& qclass, @@ -506,7 +511,7 @@ void protobufLogResponse(pdns::ProtoZero::RecMessage& message); void protobufLogResponse(const struct dnsheader* dh, LocalStateHolder& luaconfsLocal, const RecursorPacketCache::OptPBData& pbData, const struct timeval& tv, bool tcp, const ComboAddress& source, const ComboAddress& destination, - const EDNSSubnetOpts& ednssubnet, + const ComboAddress& mappedSource, const EDNSSubnetOpts& ednssubnet, const boost::uuids::uuid& uniqueId, const string& requestorId, const string& deviceId, const string& deviceName, const std::map& meta, const RecEventTrace& eventTrace); diff --git a/pdns/recursordist/rec-tcp.cc b/pdns/recursordist/rec-tcp.cc index 0ad1cb6721..406d9df933 100644 --- a/pdns/recursordist/rec-tcp.cc +++ b/pdns/recursordist/rec-tcp.cc @@ -260,9 +260,15 @@ static void handleRunningTCPQuestion(int fd, FDMultiplexer::funcparam_t& var) /* Now that we have retrieved the address of the client, as advertised by the proxy via the proxy protocol header, check that it is allowed by our ACL */ /* note that if the proxy header used a 'LOCAL' command, the original source and destination are untouched so everything should be fine */ - if (t_allowFrom && !t_allowFrom->match(&conn->d_source)) { + conn->d_mappedSource = conn->d_source; + if (t_proxyMapping) { + if (auto it = t_proxyMapping->lookup(conn->d_source)) { + conn->d_mappedSource = it->second; + } + } + if (t_allowFrom && !t_allowFrom->match(&conn->d_mappedSource)) { if (!g_quiet) { - g_log << Logger::Error << "[" << MT->getTid() << "] dropping TCP query from " << conn->d_source.toString() << ", address not matched by allow-from" << endl; + g_log << Logger::Error << "[" << MT->getTid() << "] dropping TCP query from " << conn->d_mappedSource.toString() << ", address not matched by allow-from" << endl; } ++g_stats.unauthorizedTCP; @@ -349,15 +355,16 @@ static void handleRunningTCPQuestion(int fd, FDMultiplexer::funcparam_t& var) dc->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->setSource(conn->d_source); + dc->setRemote(conn->d_remote); // the address the query was received from + dc->setSource(conn->d_source); // the address we assume the query is coming from, might be set by proxy protocol ComboAddress dest; dest.reset(); 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(conn->d_destination); + dc->setLocal(dest); // the address we received the query on + dc->setDestination(conn->d_destination); // the address we assume the query is received on, might be set by proxy protocol + dc->setMappedSource(conn->d_mappedSource); // the address we assume the query is coming from after table based mapping /* we can't move this if we want to be able to access the values in all queries sent over this connection */ dc->d_proxyProtocolValues = conn->proxyProtocolValues; @@ -447,7 +454,7 @@ static void handleRunningTCPQuestion(int fd, FDMultiplexer::funcparam_t& var) try { if (logQuery && !(luaconfsLocal->protobufExportConfig.taggedOnly && dc->d_policyTags.empty())) { - protobufLogQuery(luaconfsLocal, 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, dc->d_deviceName, dc->d_meta); + protobufLogQuery(luaconfsLocal, dc->d_uuid, dc->d_source, dc->d_destination, dc->d_mappedSource, dc->d_ednssubnet.source, true, dh->id, conn->qlen, qname, qtype, qclass, dc->d_policyTags, dc->d_requestorId, dc->d_deviceId, dc->d_deviceName, dc->d_meta); } } catch (const std::exception& e) { @@ -499,9 +506,9 @@ static void handleRunningTCPQuestion(int fd, FDMultiplexer::funcparam_t& var) ++g_stats.tcpqcounter; if (dc->d_mdp.d_header.opcode == Opcode::Notify) { - if (!t_allowNotifyFrom || !t_allowNotifyFrom->match(dc->d_source)) { + if (!t_allowNotifyFrom || !t_allowNotifyFrom->match(dc->d_mappedSource)) { if (!g_quiet) { - g_log << Logger::Error << "[" << MT->getTid() << "] dropping TCP NOTIFY from " << dc->d_source.toString() << ", address not matched by allow-notify-from" << endl; + g_log << Logger::Error << "[" << MT->getTid() << "] dropping TCP NOTIFY from " << dc->d_mappedSource.toString() << ", address not matched by allow-notify-from" << endl; } g_stats.sourceDisallowedNotify++; @@ -510,7 +517,7 @@ static void handleRunningTCPQuestion(int fd, FDMultiplexer::funcparam_t& var) if (!isAllowNotifyForZone(qname)) { if (!g_quiet) { - g_log << Logger::Error << "[" << MT->getTid() << "] dropping TCP NOTIFY from " << dc->d_source.toString() << ", for " << qname.toLogString() << ", zone not matched by allow-notify-for" << endl; + g_log << Logger::Error << "[" << MT->getTid() << "] dropping TCP NOTIFY from " << dc->d_mappedSource.toString() << ", for " << qname.toLogString() << ", zone not matched by allow-notify-for" << endl; } g_stats.zoneDisallowedNotify++; @@ -547,7 +554,7 @@ static void handleRunningTCPQuestion(int fd, FDMultiplexer::funcparam_t& var) { 0, 0 }; - protobufLogResponse(dh, luaconfsLocal, pbData, tv, true, dc->d_source, dc->d_destination, dc->d_ednssubnet, dc->d_uuid, dc->d_requestorId, dc->d_deviceId, dc->d_deviceName, dc->d_meta, dc->d_eventTrace); + protobufLogResponse(dh, luaconfsLocal, pbData, tv, true, dc->d_source, dc->d_destination, dc->d_mappedSource, dc->d_ednssubnet, dc->d_uuid, dc->d_requestorId, dc->d_deviceId, dc->d_deviceName, dc->d_meta, dc->d_eventTrace); } if (dc->d_eventTrace.enabled() && SyncRes::s_event_trace_enabled & SyncRes::event_trace_to_log) { @@ -615,9 +622,15 @@ void handleNewTCPQuestion(int fd, FDMultiplexer::funcparam_t&) } bool fromProxyProtocolSource = expectProxyProtocol(addr); - if (t_allowFrom && !t_allowFrom->match(&addr) && !fromProxyProtocolSource) { + ComboAddress mappedSource = addr; + if (!fromProxyProtocolSource && t_proxyMapping) { + if (auto it = t_proxyMapping->lookup(addr)) { + mappedSource = it->second; + } + } + if (!fromProxyProtocolSource && t_allowFrom && !t_allowFrom->match(&mappedSource)) { if (!g_quiet) - g_log << Logger::Error << "[" << MT->getTid() << "] dropping TCP query from " << addr.toString() << ", address neither matched by allow-from nor proxy-protocol-from" << endl; + g_log << Logger::Error << "[" << MT->getTid() << "] dropping TCP query from " << mappedSource.toString() << ", address neither matched by allow-from nor proxy-protocol-from" << endl; g_stats.unauthorizedTCP++; try { @@ -647,6 +660,7 @@ void handleNewTCPQuestion(int fd, FDMultiplexer::funcparam_t&) tc->d_destination.sin4.sin_family = addr.sin4.sin_family; socklen_t len = tc->d_destination.getSocklen(); getsockname(tc->getFD(), reinterpret_cast(&tc->d_destination), &len); // if this fails, we're ok with it + tc->d_mappedSource = mappedSource; if (fromProxyProtocolSource) { tc->proxyProtocolNeed = s_proxyProtocolMinimumHeaderSize; diff --git a/pdns/syncres.hh b/pdns/syncres.hh index db4dc45ad7..1020763673 100644 --- a/pdns/syncres.hh +++ b/pdns/syncres.hh @@ -1140,6 +1140,7 @@ public: const ComboAddress d_remote; ComboAddress d_source; ComboAddress d_destination; + ComboAddress d_mappedSource; size_t queriesCount{0}; size_t proxyProtocolGot{0}; ssize_t proxyProtocolNeed{0}; diff --git a/regression-tests.recursor-dnssec/test_Protobuf.py b/regression-tests.recursor-dnssec/test_Protobuf.py index 92f5155102..134b84b75c 100644 --- a/regression-tests.recursor-dnssec/test_Protobuf.py +++ b/regression-tests.recursor-dnssec/test_Protobuf.py @@ -368,6 +368,82 @@ auth-zones=example=configs/%s/example.zone""" % _confdir self.assertEqual(socket.inet_ntop(socket.AF_INET, rr.rdata), '192.0.2.42') self.checkNoRemainingMessage() +class ProtobufProxyMappingTest(TestRecursorProtobuf): + """ + This test makes sure that we correctly export queries and response over protobuf with a proxyMapping + """ + + _confdir = 'ProtobufProxyMappingTest' + _config_template = """ + auth-zones=example=configs/%s/example.zone + allow-from=3.4.5.0/24 + """ % _confdir + + _lua_config_file = """ + addProxyMapping("127.0.0.1/24", "3.4.5.6:99") + protobufServer({"127.0.0.1:%d", "127.0.0.1:%d"}) + """ % (protobufServersParameters[0].port, protobufServersParameters[1].port) + + def testA(self): + name = 'a.example.' + expected = dns.rrset.from_text(name, 0, dns.rdataclass.IN, 'A', '192.0.2.42') + query = dns.message.make_query(name, 'A', want_dnssec=True) + query.flags |= dns.flags.CD + res = self.sendUDPQuery(query) + + self.assertRRsetInAnswer(res, expected) + + # check the protobuf messages corresponding to the UDP query and answer + msg = self.getFirstProtobufMessage() + self.checkProtobufQuery(msg, dnsmessage_pb2.PBDNSMessage.UDP, query, dns.rdataclass.IN, dns.rdatatype.A, name) + # then the response + msg = self.getFirstProtobufMessage() + self.checkProtobufResponse(msg, dnsmessage_pb2.PBDNSMessage.UDP, res, '127.0.0.1') + self.assertEqual(len(msg.response.rrs), 1) + rr = msg.response.rrs[0] + # we have max-cache-ttl set to 15 + self.checkProtobufResponseRecord(rr, dns.rdataclass.IN, dns.rdatatype.A, name, 15) + self.assertEqual(socket.inet_ntop(socket.AF_INET, rr.rdata), '192.0.2.42') + self.checkNoRemainingMessage() + +class ProtobufProxyMappingLogMappedTest(TestRecursorProtobuf): + """ + This test makes sure that we correctly export queries and response over protobuf. + """ + + _confdir = 'ProtobufProxyMappingLogMappedTest' + _config_template = """ + auth-zones=example=configs/%s/example.zone + allow-from=3.4.5.0/0" + """ % _confdir + + _lua_config_file = """ + addProxyMapping("127.0.0.1/24", "3.4.5.6:99") + protobufServer({"127.0.0.1:%d", "127.0.0.1:%d"}, { logMappedFrom = true }) + """ % (protobufServersParameters[0].port, protobufServersParameters[1].port) + + def testA(self): + name = 'a.example.' + expected = dns.rrset.from_text(name, 0, dns.rdataclass.IN, 'A', '192.0.2.42') + query = dns.message.make_query(name, 'A', want_dnssec=True) + query.flags |= dns.flags.CD + res = self.sendUDPQuery(query) + + self.assertRRsetInAnswer(res, expected) + + # check the protobuf messages corresponding to the UDP query and answer + msg = self.getFirstProtobufMessage() + self.checkProtobufQuery(msg, dnsmessage_pb2.PBDNSMessage.UDP, query, dns.rdataclass.IN, dns.rdatatype.A, name, '3.4.5.6') + # then the response + msg = self.getFirstProtobufMessage() + self.checkProtobufResponse(msg, dnsmessage_pb2.PBDNSMessage.UDP, res, '3.4.5.6') + self.assertEqual(len(msg.response.rrs), 1) + rr = msg.response.rrs[0] + # we have max-cache-ttl set to 15 + self.checkProtobufResponseRecord(rr, dns.rdataclass.IN, dns.rdatatype.A, name, 15) + self.assertEqual(socket.inet_ntop(socket.AF_INET, rr.rdata), '192.0.2.42') + self.checkNoRemainingMessage() + class ProtobufProxyTest(TestRecursorProtobuf): """ This test makes sure that we correctly export addresses over protobuf when the proxy protocol is used. @@ -402,6 +478,85 @@ allow-from=127.0.0.1,6.6.6.6 self.assertEqual(socket.inet_ntop(socket.AF_INET, rr.rdata), '192.0.2.42') self.checkNoRemainingMessage() +class ProtobufProxyWithProxyByTableTest(TestRecursorProtobuf): + """ + This test makes sure that we correctly export addresses over protobuf when the proxy protocol and a proxy table mapping is used + """ + + _confdir = 'ProtobufProxyWithProxyByTable' + _config_template = """ +auth-zones=example=configs/%s/example.zone +proxy-protocol-from=127.0.0.1/32 +allow-from=3.4.5.6 +""" % _confdir + + _lua_config_file = """ + addProxyMapping("6.6.6.6/24", "3.4.5.6:99") + protobufServer({"127.0.0.1:%d", "127.0.0.1:%d"}) + """ % (protobufServersParameters[0].port, protobufServersParameters[1].port) + + def testA(self): + name = 'a.example.' + expected = dns.rrset.from_text(name, 0, dns.rdataclass.IN, 'A', '192.0.2.42') + query = dns.message.make_query(name, 'A', want_dnssec=True) + query.flags |= dns.flags.CD + res = self.sendUDPQueryWithProxyProtocol(query, False, '6.6.6.6', '7.7.7.7', 666, 777) + + self.assertRRsetInAnswer(res, expected) + + # check the protobuf messages corresponding to the UDP query and answer + msg = self.getFirstProtobufMessage() + self.checkProtobufQuery(msg, dnsmessage_pb2.PBDNSMessage.UDP, query, dns.rdataclass.IN, dns.rdatatype.A, name, '6.6.6.6', '7.7.7.7') + # then the response + msg = self.getFirstProtobufMessage() + self.checkProtobufResponse(msg, dnsmessage_pb2.PBDNSMessage.UDP, res, '6.6.6.6') + self.assertEqual(len(msg.response.rrs), 1) + rr = msg.response.rrs[0] + # we have max-cache-ttl set to 15 + self.checkProtobufResponseRecord(rr, dns.rdataclass.IN, dns.rdatatype.A, name, 15) + self.assertEqual(socket.inet_ntop(socket.AF_INET, rr.rdata), '192.0.2.42') + self.checkNoRemainingMessage() + +class ProtobufProxyWithProxyByTableLogMappedTest(TestRecursorProtobuf): + """ + This test makes sure that we correctly export addresses over protobuf when the proxy protocol and a proxy table mapping is used + """ + + _confdir = 'ProtobufProxyWithProxyByTableLogMapped' + _config_template = """ +auth-zones=example=configs/%s/example.zone +proxy-protocol-from=127.0.0.1/32 +allow-from=3.4.5.6 +""" % _confdir + + _lua_config_file = """ + addProxyMapping("6.6.6.6/24", "3.4.5.6:99") + protobufServer({"127.0.0.1:%d", "127.0.0.1:%d"}, { logMappedFrom = true }) + """ % (protobufServersParameters[0].port, protobufServersParameters[1].port) + + def testA(self): + name = 'a.example.' + expected = dns.rrset.from_text(name, 0, dns.rdataclass.IN, 'A', '192.0.2.42') + query = dns.message.make_query(name, 'A', want_dnssec=True) + query.flags |= dns.flags.CD + res = self.sendUDPQueryWithProxyProtocol(query, False, '6.6.6.6', '7.7.7.7', 666, 777) + + self.assertRRsetInAnswer(res, expected) + + # check the protobuf messages corresponding to the UDP query and answer + msg = self.getFirstProtobufMessage() + self.checkProtobufQuery(msg, dnsmessage_pb2.PBDNSMessage.UDP, query, dns.rdataclass.IN, dns.rdatatype.A, name, '3.4.5.6', '7.7.7.7') + # then the response + msg = self.getFirstProtobufMessage() + self.checkProtobufResponse(msg, dnsmessage_pb2.PBDNSMessage.UDP, res, '3.4.5.6') + self.assertEqual(len(msg.response.rrs), 1) + rr = msg.response.rrs[0] + # we have max-cache-ttl set to 15 + self.checkProtobufResponseRecord(rr, dns.rdataclass.IN, dns.rdatatype.A, name, 15) + self.assertEqual(socket.inet_ntop(socket.AF_INET, rr.rdata), '192.0.2.42') + self.checkNoRemainingMessage() + + class OutgoingProtobufDefaultTest(TestRecursorProtobuf): """ This test makes sure that we correctly export outgoing queries over protobuf. diff --git a/regression-tests.recursor-dnssec/test_ProxyByTable.py b/regression-tests.recursor-dnssec/test_ProxyByTable.py new file mode 100644 index 0000000000..b804001fb2 --- /dev/null +++ b/regression-tests.recursor-dnssec/test_ProxyByTable.py @@ -0,0 +1,44 @@ +import dns +import os +from recursortests import RecursorTest + +class testProxyByTable(RecursorTest): + """ + This test makes sure that we correctly use the proxy-mapped address during the ACL check + """ + _confdir = 'ProxyByTable' + + _config_template = """dnssec=validate + auth-zones=authzone.example=configs/%s/authzone.zone + allow-from=3.4.5.0/24 + """ % _confdir + + _lua_config_file = """ + addProxyMapping("127.0.0.0/24", "3.4.5.6:99") + """ + + @classmethod + def generateRecursorConfig(cls, confdir): + authzonepath = os.path.join(confdir, 'authzone.zone') + with open(authzonepath, 'w') as authzone: + authzone.write("""$ORIGIN authzone.example. +@ 3600 IN SOA {soa} +@ 3600 IN A 192.0.2.88 +""".format(soa=cls._SOA)) + super(testProxyByTable, cls).generateRecursorConfig(confdir) + + + def testA(self): + expected = dns.rrset.from_text('ns.secure.example.', 0, dns.rdataclass.IN, 'A', '{prefix}.9'.format(prefix=self._PREFIX)) + query = dns.message.make_query('ns.secure.example', 'A', want_dnssec=True) + query.flags |= dns.flags.AD + + for method in ("sendUDPQuery", "sendTCPQuery"): + sender = getattr(self, method) + res = sender(query) + + self.assertMessageIsAuthenticated(res) + self.assertRRsetInAnswer(res, expected) + self.assertMatchingRRSIGInAnswer(res, expected) + +