]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
dnsdist: Implement SpoofSVCAction to return SVC responses
authorRemi Gacogne <remi.gacogne@powerdns.com>
Mon, 19 Jul 2021 14:27:29 +0000 (16:27 +0200)
committerRemi Gacogne <remi.gacogne@powerdns.com>
Fri, 27 Aug 2021 08:40:06 +0000 (10:40 +0200)
13 files changed:
pdns/dnsdist-console.cc
pdns/dnsdist-lua-actions.cc
pdns/dnsdist-lua-bindings.cc
pdns/dnsdistdist/Makefile.am
pdns/dnsdistdist/dnsdist-svc.cc [new file with mode: 0644]
pdns/dnsdistdist/dnsdist-svc.hh [new file with mode: 0644]
pdns/dnsdistdist/docs/reference/index.rst
pdns/dnsdistdist/docs/reference/svc.rst [new file with mode: 0644]
pdns/dnsdistdist/docs/rules-actions.rst
pdns/dnsdistdist/test-dnsdistsvc_cc.cc [new file with mode: 0644]
pdns/dnswriter.cc
pdns/dnswriter.hh
regression-tests.dnsdist/test_SVCB.py [new file with mode: 0644]

index 4a5599519de455287531b48e525c7ca6920250f0..4d6778460a9490e08fe35e0ed57856b602e25c18 100644 (file)
@@ -532,6 +532,7 @@ const std::vector<ConsoleKeyword> g_consoleKeywords{
   { "newServer", true, "{address=\"ip:port\", qps=1000, order=1, weight=10, pool=\"abuse\", retries=5, tcpConnectTimeout=5, tcpSendTimeout=30, tcpRecvTimeout=30, checkName=\"a.root-servers.net.\", checkType=\"A\", maxCheckFailures=1, mustResolve=false, useClientSubnet=true, source=\"address|interface name|address@interface\", sockets=1, reconnectOnUp=false}", "instantiate a server" },
   { "newServerPolicy", true, "name, function", "create a policy object from a Lua function" },
   { "newSuffixMatchNode", true, "", "returns a new SuffixMatchNode" },
+  { "newSVCRecordParameters", true, "priority, target, mandatoryParams, alpns, noDefaultAlpn [, port [, ech [, ipv4hints [, ipv6hints [, additionalParameters ]]]]]", "return a new SVCRecordParameters object, to use with SpoofSVCAction" },
   { "NegativeAndSOAAction", true, "nxd, zone, ttl, mname, rname, serial, refresh, retry, expire, minimum [, options]", "Turn a query into a NXDomain or NoData answer and sets a SOA record in the additional section" },
   { "NoneAction", true, "", "Does nothing. Subsequent rules are processed after this action" },
   { "NotRule", true, "selector", "Matches the traffic if the selector rule does not match" },
@@ -674,6 +675,7 @@ const std::vector<ConsoleKeyword> g_consoleKeywords{
   { "SpoofAction", true, "ip|list of ips [, options]", "forge a response with the specified IPv4 (for an A query) or IPv6 (for an AAAA). If you specify multiple addresses, all that match the query type (A, AAAA or ANY) will get spoofed in" },
   { "SpoofCNAMEAction", true, "cname [, options]", "Forge a response with the specified CNAME value" },
   { "SpoofRawAction", true, "raw|list of raws [, options]", "Forge a response with the specified record data as raw bytes. If you specify multiple raws (it is assumed they match the query type), all will get spoofed in" },
+  { "SpoofSVCAction", true, "list of svcParams [, options]", "Forge a response with the specified SVC record data" } ,
   { "SuffixMatchNodeRule", true, "smn[, quiet]", "Matches based on a group of domain suffixes for rapid testing of membership. Pass true as second parameter to prevent listing of all domains matched" },
   { "TagRule", true, "name [, value]", "matches if the tag named 'name' is present, with the given 'value' matching if any" },
   { "TCAction", true, "", "create answer to query with TC and RD bits set, to move to TCP" },
index 12df54ae93c1fe33b0f38a75cef9eee583d505a4..43f72a178c7f6b5ef31f1e5abadea4d8af5a23fe 100644 (file)
 #include "dnsdist-lua-ffi.hh"
 #include "dnsdist-protobuf.hh"
 #include "dnsdist-kvs.hh"
+#include "dnsdist-svc.hh"
 
 #include "dolog.hh"
 #include "dnstap.hh"
+#include "dnswriter.hh"
 #include "ednsoptions.hh"
 #include "fstrm_logger.hh"
 #include "remote_logger.hh"
+#include "svc-records.hh"
 
 #include <boost/optional/optional_io.hpp>
 
@@ -355,6 +358,97 @@ private:
   uint8_t d_rcode;
 };
 
+class SpoofSVCAction : public DNSAction
+{
+public:
+  SpoofSVCAction(const std::vector<std::pair<int, SVCRecordParameters>>& parameters)
+  {
+    d_payloads.reserve(parameters.size());
+
+    for (const auto& param : parameters) {
+      std::vector<uint8_t> payload;
+      if (!generateSVCPayload(payload, param.second)) {
+        throw std::runtime_error("Unable to generate a valid SVC record from the supplied parameters");
+      }
+
+      d_totalPayloadsSize += payload.size();
+      d_payloads.push_back(std::move(payload));
+
+      for (const auto& hint : param.second.ipv4hints) {
+        d_additionals4.insert({ param.second.target, ComboAddress(hint) });
+      }
+
+      for (const auto& hint : param.second.ipv6hints) {
+        d_additionals6.insert({ param.second.target, ComboAddress(hint) });
+      }
+    }
+  }
+
+  DNSAction::Action operator()(DNSQuestion* dq, std::string* ruleresult) const override
+  {
+    /* it will likely be a bit bigger than that because of additionals */
+    uint16_t numberOfRecords = d_payloads.size();
+    const auto qnameWireLength = dq->qname->wirelength();
+    if (dq->getMaximumSize() < (sizeof(dnsheader) + qnameWireLength + 4 + numberOfRecords*12 /* recordstart */ + d_totalPayloadsSize)) {
+      return Action::None;
+    }
+
+    PacketBuffer newPacket;
+    newPacket.reserve(sizeof(dnsheader) + qnameWireLength + 4 + numberOfRecords*12 /* recordstart */ + d_totalPayloadsSize);
+    GenericDNSPacketWriter<PacketBuffer> pw(newPacket, *dq->qname, dq->qtype);
+    for (const auto& payload : d_payloads) {
+      pw.startRecord(*dq->qname, dq->qtype, d_responseConfig.ttl);
+      pw.xfrBlob(payload);
+      pw.commit();
+    }
+
+    if (newPacket.size() < dq->getMaximumSize()) {
+      for (const auto& additional : d_additionals4) {
+        pw.startRecord(additional.first.isRoot() ? *dq->qname : additional.first, QType::A, d_responseConfig.ttl, QClass::IN, DNSResourceRecord::ADDITIONAL);
+        pw.xfrCAWithoutPort(4, additional.second);
+        pw.commit();
+      }
+    }
+
+    if (newPacket.size() < dq->getMaximumSize()) {
+      for (const auto& additional : d_additionals6) {
+        pw.startRecord(additional.first.isRoot() ? *dq->qname : additional.first, QType::AAAA, d_responseConfig.ttl, QClass::IN, DNSResourceRecord::ADDITIONAL);
+        pw.xfrCAWithoutPort(6, additional.second);
+        pw.commit();
+      }
+    }
+
+    if (g_addEDNSToSelfGeneratedResponses && queryHasEDNS(*dq)) {
+      bool dnssecOK = getEDNSZ(*dq) & EDNS_HEADER_FLAG_DO;
+      pw.addOpt(g_PayloadSizeSelfGenAnswers, 0, dnssecOK ? EDNS_HEADER_FLAG_DO : 0);
+      pw.commit();
+    }
+
+    if (newPacket.size() >= dq->getMaximumSize()) {
+      /* sorry! */
+      return Action::None;
+    }
+
+    pw.getHeader()->id = dq->getHeader()->id;
+    pw.getHeader()->qr = true; // for good measure
+    setResponseHeadersFromConfig(*pw.getHeader(), d_responseConfig);
+    dq->getMutableData() = std::move(newPacket);
+
+    return Action::HeaderModify;
+  }
+  std::string toString() const override
+  {
+    return "spoof SVC record ";
+  }
+
+  ResponseConfig d_responseConfig;
+private:
+  std::vector<std::vector<uint8_t>> d_payloads;
+  std::set<std::pair<DNSName, ComboAddress>> d_additionals4;
+  std::set<std::pair<DNSName, ComboAddress>> d_additionals6;
+  size_t d_totalPayloadsSize{0};
+};
+
 class TCAction : public DNSAction
 {
 public:
@@ -1989,6 +2083,13 @@ void setupLuaActions(LuaContext& luaCtx)
       return ret;
     });
 
+  luaCtx.writeFunction("SpoofSVCAction", [](const std::vector<std::pair<int, SVCRecordParameters>>& parameters, boost::optional<responseParams_t> vars) {
+      auto ret = std::shared_ptr<DNSAction>(new SpoofSVCAction(parameters));
+      auto sa = std::dynamic_pointer_cast<SpoofSVCAction>(ret);
+      parseResponseConfig(vars, sa->d_responseConfig);
+      return ret;
+    });
+
   luaCtx.writeFunction("SpoofCNAMEAction", [](const std::string& a, boost::optional<responseParams_t> vars) {
       auto ret = std::shared_ptr<DNSAction>(new SpoofAction(DNSName(a)));
       auto sa = std::dynamic_pointer_cast<SpoofAction>(ret);
index 7f0568ff8816fcdebfb2aca835db80e46f6b90a4..cadf71f4c896854b72c4fd57c95b74c46b5f8bcf 100644 (file)
@@ -22,6 +22,7 @@
 #include "config.h"
 #include "dnsdist.hh"
 #include "dnsdist-lua.hh"
+#include "dnsdist-svc.hh"
 
 #include "dolog.hh"
 
@@ -534,4 +535,49 @@ void setupLuaBindings(LuaContext& luaCtx, bool client)
     }
     return std::make_shared<DOHResponseMapEntry>(regex, status, PacketBuffer(content.begin(), content.end()), headers);
   });
+
+  luaCtx.writeFunction("newSVCRecordParameters", [](uint16_t priority, const std::string& target, const std::vector<std::pair<int, uint16_t>>& mandatoryParams, const std::vector<std::pair<int, std::string>>& alpns, bool noDefaultAlpn, boost::optional<uint16_t> port, const boost::optional<std::string> ech, boost::optional<std::vector<std::pair<int, std::string>>> ipv4hints, boost::optional<std::vector<std::pair<int, std::string>>> ipv6hints, boost::optional<std::vector<std::pair<int, std::string>>> additionalParameters)
+  {
+    SVCRecordParameters parameters;
+    parameters.priority = priority;
+    parameters.target = DNSName(target);
+
+    for (const auto& entry : mandatoryParams) {
+      parameters.mandatoryParams.insert(entry.second);
+    }
+
+    for (const auto& entry : alpns) {
+      parameters.alpns.push_back(entry.second);
+    }
+
+    parameters.noDefaultAlpn = noDefaultAlpn;
+
+    if (port) {
+      parameters.port = *port;
+    }
+
+    if (ech) {
+      parameters.ech = *ech;
+    }
+
+    if (ipv4hints) {
+      for (const auto& entry : *ipv4hints) {
+        parameters.ipv4hints.push_back(ComboAddress(entry.second));
+      }
+    }
+
+    if (ipv6hints) {
+      for (const auto& entry : *ipv6hints) {
+        parameters.ipv6hints.push_back(ComboAddress(entry.second));
+      }
+    }
+
+    if (additionalParameters) {
+      for (const auto& entry : *additionalParameters) {
+        parameters.additionalParams.push_back({entry.first, entry.second});
+      }
+    }
+
+    return parameters;
+  });
 }
index 07ddd1267a6fde523c9f6bc9afe10c01ae213eab..bc18aaf62ca0450537ca0c9e0089d556a46323e7 100644 (file)
@@ -167,6 +167,7 @@ dnsdist_SOURCES = \
        dnsdist-secpoll.cc dnsdist-secpoll.hh \
        dnsdist-session-cache.cc dnsdist-session-cache.hh \
        dnsdist-snmp.cc dnsdist-snmp.hh \
+       dnsdist-svc.cc dnsdist-svc.hh \
        dnsdist-systemd.cc dnsdist-systemd.hh \
        dnsdist-tcp-downstream.cc dnsdist-tcp-downstream.hh \
        dnsdist-tcp-upstream.hh \
@@ -245,6 +246,7 @@ testrunner_SOURCES = \
        dnsdist-rings.cc dnsdist-rings.hh \
        dnsdist-rules.cc dnsdist-rules.hh \
        dnsdist-session-cache.cc dnsdist-session-cache.hh \
+       dnsdist-svc.cc dnsdist-svc.hh \
        dnsdist-tcp-downstream.cc \
        dnsdist-tcp.cc dnsdist-tcp.hh \
        dnsdist-xpf.cc dnsdist-xpf.hh \
@@ -284,6 +286,7 @@ testrunner_SOURCES = \
        test-dnsdistpacketcache_cc.cc \
        test-dnsdistrings_cc.cc \
        test-dnsdistrules_cc.cc \
+       test-dnsdistsvc_cc.cc \
        test-dnsdisttcp_cc.cc \
        test-dnsparser_cc.cc \
        test-iputils_hh.cc \
diff --git a/pdns/dnsdistdist/dnsdist-svc.cc b/pdns/dnsdistdist/dnsdist-svc.cc
new file mode 100644 (file)
index 0000000..72a0c18
--- /dev/null
@@ -0,0 +1,93 @@
+/*
+ * 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 "dnsdist-svc.hh"
+#include "dnswriter.hh"
+#include "svc-records.hh"
+
+bool generateSVCPayload(std::vector<uint8_t>& payload, uint16_t priority, const DNSName& target, const std::set<uint16_t>& mandatoryParams, const std::vector<std::string>& alpns, bool noDefaultAlpn, std::optional<uint16_t> port, const std::string& ech, const std::vector<ComboAddress>& ipv4hints, const std::vector<ComboAddress>& ipv6hints, const std::vector<std::pair<uint16_t, std::string>>& additionalParams)
+{
+  // this is an _ordered_ set and the comparison operator is properly defined,
+  // so the parameters will be ordered as defined in the RFC
+  std::set<SvcParam> params;
+
+  if (!mandatoryParams.empty()) {
+    std::set<SvcParam::SvcParamKey> mandatoryKeys;
+    for (const auto& entry : mandatoryParams) {
+      mandatoryKeys.insert(static_cast<SvcParam::SvcParamKey>(entry));
+    }
+    params.insert({SvcParam::SvcParamKey::mandatory, std::move(mandatoryKeys)});
+  }
+
+  if (!alpns.empty()) {
+    params.insert({SvcParam::SvcParamKey::alpn, std::vector<std::string>(alpns)});
+  }
+
+  if (noDefaultAlpn) {
+    params.insert({SvcParam::SvcParamKey::no_default_alpn});
+  }
+
+  if (port) {
+    params.insert({SvcParam::SvcParamKey::port, *port});
+  }
+
+  if (!ipv4hints.empty()) {
+    params.insert({SvcParam::SvcParamKey::ipv4hint,  std::vector<ComboAddress>(ipv4hints)});
+  }
+
+  if (!ech.empty()) {
+    params.insert({SvcParam::SvcParamKey::ech, ech});
+  }
+
+  if (!ipv6hints.empty()) {
+    params.insert({SvcParam::SvcParamKey::ipv6hint,  std::vector<ComboAddress>(ipv6hints)});
+  }
+
+  for (const auto& param : additionalParams) {
+    params.insert({static_cast<SvcParam::SvcParamKey>(param.first), param.second});
+  }
+
+  if (priority == 0 && params.size() != 0) {
+    return false;
+  }
+
+  payload.clear();
+  /* we will remove the header, question and record header parts later */
+  DNSPacketWriter pw(payload, g_rootdnsname, QType::A, QClass::IN, 0);
+  pw.startRecord(g_rootdnsname, QType::A, 60, QClass::IN, DNSResourceRecord::ANSWER, false);
+  size_t offset = pw.size();
+  pw.xfr16BitInt(priority);
+  pw.xfrName(target, false, true);
+  pw.xfrSvcParamKeyVals(params);
+  pw.commit();
+
+  if (payload.size() <= offset) {
+    return false;
+  }
+
+  payload.erase(payload.begin(), payload.begin() + offset);
+  return true;
+}
+
+bool generateSVCPayload(std::vector<uint8_t>& payload, const SVCRecordParameters& parameters)
+{
+  return generateSVCPayload(payload, parameters.priority, parameters.target, parameters.mandatoryParams, parameters.alpns, parameters.noDefaultAlpn, parameters.port, parameters.ech, parameters.ipv4hints, parameters.ipv6hints, parameters.additionalParams);
+}
diff --git a/pdns/dnsdistdist/dnsdist-svc.hh b/pdns/dnsdistdist/dnsdist-svc.hh
new file mode 100644 (file)
index 0000000..fefd078
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+ * 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 <optional>
+#include <set>
+#include <string>
+#include <vector>
+
+#include "dnsname.hh"
+#include "iputils.hh"
+
+struct SVCRecordParameters
+{
+  SVCRecordParameters()
+  {
+  }
+
+  std::set<uint16_t> mandatoryParams;
+  std::vector<std::string> alpns;
+  std::vector<ComboAddress> ipv4hints;
+  std::vector<ComboAddress> ipv6hints;
+  std::vector<std::pair<uint16_t, std::string>> additionalParams;
+  std::string ech;
+  DNSName target;
+  std::optional<uint16_t> port{std::nullopt};
+  uint16_t priority{0};
+  bool noDefaultAlpn{false};
+};
+
+bool generateSVCPayload(std::vector<uint8_t>& payload, uint16_t priority, const DNSName& target, const std::set<uint16_t>& mandatoryParams, const std::vector<std::string>& alpns, bool noDefaultAlpn, std::optional<uint16_t> port, const std::string& ech, const std::vector<ComboAddress>& ipv4hints, const std::vector<ComboAddress>& ipv6hints, const std::vector<std::pair<uint16_t, std::string>>& additionalParams);
+
+bool generateSVCPayload(std::vector<uint8_t>& payload, const SVCRecordParameters& parameters);
index 92ba86af655fa479e9d328ae02744a7506377cbc..9bccb195c18a8163db25f2de590101f37e024ba6 100755 (executable)
@@ -24,3 +24,4 @@ These chapters contain extensive information on all functions and object availab
   kvs
   logging
   web
+  svc
\ No newline at end of file
diff --git a/pdns/dnsdistdist/docs/reference/svc.rst b/pdns/dnsdistdist/docs/reference/svc.rst
new file mode 100644 (file)
index 0000000..d57ae53
--- /dev/null
@@ -0,0 +1,33 @@
+SVCRecordParameters
+===================
+
+.. function:: newSVCRecordParameters(priority, target, mandatoryParams, alpns, noDefaultAlpn [, port [, ech [, ipv4hints [, ipv6hints [, additionalParameters ]]]]]) -> SVCRecordParameters
+
+  .. versionadded:: 1.7.0
+
+  Returns a :class:`SVCRecordParameters` to use with :func:`SpoofSVCAction`.
+
+  .. code-block:: Lua
+
+    -- reply to SVCB queries for resolver.powerdns.com. indicating DoT on port 853 of dot.powerdns.com. (192.0.2.1/2001:db8::1), DoH on https://doh.powerdns.com/dns-query (192.0.2.2/2001:db8::2)
+    local svc = { newSVCRecordParameters(1, "dot.powerdns.com.", { 3 }, { "dot" }, false, 853, "", { "192.0.2.1" }, { "2001:db8::1" }),
+                  newSVCRecordParameters(2, "doh.powerdns.com.", { 3 }, { "h2" },  false, 443, "", { "192.0.2.2" }, { "2001:db8::2" }, { ["42"] = "/dns-query{?dns}" })
+                }    
+    addAction(AndRule{QTypeRule(64), QNameRule('resolver.powerdns.com.')}, SpoofSVCAction(svc))
+
+  :param int priority: The priority of this record. if more than one record is returned, they all should have different priorities. A priority of 0 indicates Alias mode and no other record should be present in the RRSet.
+  :param str target: A domain name indicating the target name.
+  :param list of integers mandatoryParams: The numeric values of the supplied parameters that are mandatory for the client to understand.
+  :param list of strings alpns: The ALPN values, like "dot" or "h2".
+  :param bool noDefaultAlpn: Whether the default ALPN value should be ignored and replaced by the supplied ones.
+  :param int port: Optional port to connect to.
+  :param str ech: Optional Encrypted Client Hello value, as a raw string (null bytes are supported).
+  :param list of strings ipv4hints: Optional list of IPv4 addresses.
+  :param list of strings ipv6hints: Optional list of IPv6 addresses.
+  :param table of strings additionalParameters: Optional table of additionals parameters. The key should be numerical and will be used as the SvcParamKey, while the value should be a raw binary string (null bytes are supported) and will be passed as the SvcParamValue as-is.
+
+.. class:: SVCRecordParameters
+
+  .. versionadded:: 1.7.0
+
+  Represents Service Binding (SVCB, HTTPS) record parameters, which can be used with :func:`SpoofSVCAction`.
index a76bf8368b57ae83a9980ce693538231031921b1..3fea22df58abfefdb74d05105449849db7c4e895 100644 (file)
@@ -1499,6 +1499,24 @@ The following actions exist.
   * ``ra``: bool - Set the RA bit to this value (true means the bit is set, false means it's cleared). Default is to copy the value of the RD bit from the incoming query.
   * ``ttl``: int - The TTL of the record.
 
+.. function:: SpoofSVCAction(svcParams [, options])
+
+  .. versionadded:: 1.7.0
+
+  Forge a response with the specified SVC record data. If the list contains more than one class:`SVCRecordParameters` (generated via :func:`newSVCRecordParameters`) object, they are all returned,
+  and should have different priorities.
+  The hints provided in the SVC parameters, if any, will also be added as A/AAAA records in the additional section, using the target name present in the parameters as owner name if it's not empty (root) and the qname instead.
+
+  :param list of class:`SVCRecordParameters` svcParams: The record data to return
+  :param table options: A table with key: value pairs with options.
+
+  Options:
+
+  * ``aa``: bool - Set the AA bit to this value (true means the bit is set, false means it's cleared). Default is to clear it.
+  * ``ad``: bool - Set the AD bit to this value (true means the bit is set, false means it's cleared). Default is to clear it.
+  * ``ra``: bool - Set the RA bit to this value (true means the bit is set, false means it's cleared). Default is to copy the value of the RD bit from the incoming query.
+  * ``ttl``: int - The TTL of the record.
+
 .. function:: TagAction(name, value)
 
   .. deprecated:: 1.6.0
diff --git a/pdns/dnsdistdist/test-dnsdistsvc_cc.cc b/pdns/dnsdistdist/test-dnsdistsvc_cc.cc
new file mode 100644 (file)
index 0000000..2615bc8
--- /dev/null
@@ -0,0 +1,96 @@
+
+#define BOOST_TEST_DYN_LINK
+#define BOOST_TEST_NO_MAIN
+
+#include <boost/test/unit_test.hpp>
+
+#include "dnsdist-svc.hh"
+#include "svc-records.hh"
+#include "dnsparser.hh"
+
+BOOST_AUTO_TEST_SUITE(dnsdistsvc_cc)
+
+BOOST_AUTO_TEST_CASE(test_Basic) {
+
+  DNSName target("powerdns.com.");
+
+  {
+    std::vector<uint8_t> payload;
+    const uint16_t priority = 1;
+    BOOST_CHECK(generateSVCPayload(payload, priority, target, { SvcParam::SvcParamKey::port }, { "dot" }, false, 853, std::string(), { ComboAddress("192.0.2.1") }, { ComboAddress("2001:db8::1") }, {}));
+    /* 2 octet field for SvcPriority as an integer in network byte order */
+    /* uncompressed, fully-qualified TargetName */
+    /* list of SvcParams as:
+       - 2 octet field containing the SvcParamKey as an integer in network byte order
+       - 2 octet field containing the length of the SvcParamValue as an integer between 0 and 65535 in network byte order (but constrained by the RDATA and DNS message sizes)
+       - an octet string of this length whose contents are in a format determined by the SvcParamKey
+       SvcParamKeys SHALL appear in increasing numeric order
+    */
+    size_t expectedSize = (/* priority */ 2) + target.wirelength() + (/* mandatory */ 2 + 2 + 2) + (/* alpns with 1-byte length field for each value */ 2 + 2 + 4) + (/* no-alpn-default is false */ 0) + (/* port */ 2 + 2 + 2) + (/* ech */ 0) + (/* v4 hints */ 2 + 2 + 9) + (/* v6 hints */ 2 + 2 + 11);
+    BOOST_CHECK_EQUAL(payload.size(), expectedSize);
+
+    std::set<SvcParam> params;
+    PacketReader pr(std::string_view(reinterpret_cast<const char*>(payload.data()), payload.size()), 0);
+    BOOST_CHECK_EQUAL(pr.get16BitInt(), priority);
+
+    /* we can't use getName() directly because it assumes that there has to be a dnsheader before the name */
+    DNSName parsedTarget(reinterpret_cast<const char*>(payload.data()), payload.size(), pr.getPosition(), false /* uncompress */, nullptr /* qtype */, nullptr /* qclass */, nullptr /* consumed */, 0);
+    pr.skip(parsedTarget.wirelength());
+    BOOST_CHECK_EQUAL(target.toString(), parsedTarget.toString());
+
+    pr.xfrSvcParamKeyVals(params);
+    BOOST_REQUIRE_EQUAL(params.size(), 5U);
+    auto param = params.begin();
+    BOOST_CHECK_EQUAL(param->getKey(), SvcParam::SvcParamKey::mandatory);
+    ++param;
+    BOOST_CHECK_EQUAL(param->getKey(), SvcParam::SvcParamKey::alpn);
+    ++param;
+    BOOST_CHECK_EQUAL(param->getKey(), SvcParam::SvcParamKey::port);
+    ++param;
+    BOOST_CHECK_EQUAL(param->getKey(), SvcParam::SvcParamKey::ipv4hint);
+    ++param;
+    BOOST_CHECK_EQUAL(param->getKey(), SvcParam::SvcParamKey::ipv6hint);
+  }
+
+  {
+    std::vector<uint8_t> payload;
+    const uint16_t priority = 2;
+    const std::string ech("whatever");
+    const std::string dohParam("/dns-query{?dns}");
+
+    BOOST_CHECK(generateSVCPayload(payload, priority, target, { SvcParam::SvcParamKey::port }, { "h2" }, true, 443, ech, { ComboAddress("192.0.2.2") }, { ComboAddress("2001:db8::2") }, { std::pair<uint16_t, std::string>(42, dohParam) }));
+
+    size_t expectedSize = (/* priority */ 2) + target.wirelength() + (/* mandatory */ 2 + 2 + 2) + (/* alpns */ 2 + 2 + 3) + (/* no-alpn-default is true */ 2 + 2) + (/* port */ 2 + 2 + 2) + (/* ech */ 2 + 2 + ech.size()) + (/* v4 hints */ 2 + 2 + 9) + (/* v6 hints */ 2 + 2 + 11) + (/* doh parameter */ 2 + 2 + dohParam.size());
+    BOOST_CHECK_EQUAL(payload.size(), expectedSize);
+
+    std::set<SvcParam> params;
+    PacketReader pr(std::string_view(reinterpret_cast<const char*>(payload.data()), payload.size()), 0);
+    BOOST_CHECK_EQUAL(pr.get16BitInt(), priority);
+
+    /* we can't use getName() directly because it assumes that there has to be a dnsheader before the name */
+    DNSName parsedTarget(reinterpret_cast<const char*>(payload.data()), payload.size(), pr.getPosition(), false /* uncompress */, nullptr /* qtype */, nullptr /* qclass */, nullptr /* consumed */, 0);
+    pr.skip(parsedTarget.wirelength());
+    BOOST_CHECK_EQUAL(target.toString(), parsedTarget.toString());
+
+    pr.xfrSvcParamKeyVals(params);
+    BOOST_REQUIRE_EQUAL(params.size(), 8U);
+    auto param = params.begin();
+    BOOST_CHECK_EQUAL(param->getKey(), SvcParam::SvcParamKey::mandatory);
+    ++param;
+    BOOST_CHECK_EQUAL(param->getKey(), SvcParam::SvcParamKey::alpn);
+    ++param;
+    BOOST_CHECK_EQUAL(param->getKey(), SvcParam::SvcParamKey::no_default_alpn);
+    ++param;
+    BOOST_CHECK_EQUAL(param->getKey(), SvcParam::SvcParamKey::port);
+    ++param;
+    BOOST_CHECK_EQUAL(param->getKey(), SvcParam::SvcParamKey::ipv4hint);
+    ++param;
+    BOOST_CHECK_EQUAL(param->getKey(), SvcParam::SvcParamKey::ech);
+    ++param;
+    BOOST_CHECK_EQUAL(param->getKey(), SvcParam::SvcParamKey::ipv6hint);
+    ++param;
+    BOOST_CHECK_EQUAL(static_cast<uint16_t>(param->getKey()), 42U);
+  }
+}
+
+BOOST_AUTO_TEST_SUITE_END()
index 5bf32a182e8af3788180fe9424857e87af5675ec..0fd81b4d3d0b776953f3645683ef27a6534ebfff 100644 (file)
@@ -382,6 +382,11 @@ template <typename Container> void GenericDNSPacketWriter<Container>::xfrBlob(co
   d_content.insert(d_content.end(), ptr, ptr+blob.size());
 }
 
+template <typename Container> void GenericDNSPacketWriter<Container>::xfrBlob(const std::vector<uint8_t>& blob)
+{
+  d_content.insert(d_content.end(), blob.begin(), blob.end());
+}
+
 template <typename Container> void GenericDNSPacketWriter<Container>::xfrBlobNoSpaces(const string& blob, int  )
 {
   xfrBlob(blob);
index 55b5f6f0625d0cdf76b4c75a2206d99dfc1e3d71..485b5478337c895a586cca52d96ec315cb93b2b7 100644 (file)
@@ -102,7 +102,7 @@ public:
     xfrBlob(val,16);
   }
 
-  void xfrCAWithoutPort(uint8_t version, ComboAddress &val)
+  void xfrCAWithoutPort(uint8_t version, const ComboAddress &val)
   {
     if (version == 4) xfrIP(val.sin4.sin_addr.s_addr);
     else if (version == 6) {
@@ -113,7 +113,7 @@ public:
     else throw runtime_error("invalid IP protocol");
   }
 
-  void xfrCAPort(ComboAddress &val)
+  void xfrCAPort(const ComboAddress &val)
   {
     uint16_t port;
     port = val.sin4.sin_port;
@@ -131,6 +131,7 @@ public:
   void xfrText(const string& text, bool multi=false, bool lenField=true);
   void xfrUnquotedText(const string& text, bool lenField);
   void xfrBlob(const string& blob, int len=-1);
+  void xfrBlob(const vector<uint8_t>& blob);
   void xfrSvcParamKeyVals(const set<SvcParam>& kvs);
   void xfrBlobNoSpaces(const string& blob, int len=-1);
   void xfrHexBlob(const string& blob, bool keepReading=false);
diff --git a/regression-tests.dnsdist/test_SVCB.py b/regression-tests.dnsdist/test_SVCB.py
new file mode 100644 (file)
index 0000000..6e572ab
--- /dev/null
@@ -0,0 +1,103 @@
+#!/usr/bin/env python
+import dns
+from dnsdisttests import DNSDistTest
+
+class TestSVCB(DNSDistTest):
+
+    _config_template = """
+    local basicSVC = { newSVCRecordParameters(1, "dot.powerdns.com.", { 3 }, { "dot" }, true, 853, "whatever", { "192.0.2.1" }, { "2001:db8::1" }),
+                       newSVCRecordParameters(2, "doh.powerdns.com.", { 3 }, { "h2" },  false, 443, "whatever", { "192.0.2.2" }, { "2001:db8::2" }, { ["42"] = "/dns-query{?dns}" })
+                     }
+    addAction(AndRule{QTypeRule(64), makeRule("basic.svcb.tests.powerdns.com.")}, SpoofSVCAction(basicSVC, {aa=true}))
+
+    local noHintsSVC = { newSVCRecordParameters(1, "dot.powerdns.com.", { 3 }, { "dot" }, true, 853),
+                         newSVCRecordParameters(2, "doh.powerdns.com.", { 3 }, { "h2" },  false, 443, "", { }, { }, { ["42"] = "/dns-query{?dns}" })
+                     }
+    addAction(AndRule{QTypeRule(64), makeRule("no-hints.svcb.tests.powerdns.com.")}, SpoofSVCAction(noHintsSVC, {aa=true}))
+
+    local effectiveTargetSVC = { newSVCRecordParameters(1, ".", { 3 }, { "dot" }, true, 853, "", { "192.0.2.1" }, { "2001:db8::1" }),
+                                 newSVCRecordParameters(2, ".", { 3 }, { "h2" },  false, 443, "", { "192.0.2.1" }, { "2001:db8::1" }, { ["42"] = "/dns-query{?dns}" })
+                     }
+    addAction(AndRule{QTypeRule(64), makeRule("effective-target.svcb.tests.powerdns.com.")}, SpoofSVCAction(effectiveTargetSVC, {aa=true}))
+
+    local httpsSVC = { newSVCRecordParameters(1, ".", { 3 }, { "h2" }, true, 8002, "...", { "192.0.2.2" }, { "2001:db8::2" }) }
+    addAction(AndRule{QTypeRule(65), makeRule("https.svcb.tests.powerdns.com.")}, SpoofSVCAction(httpsSVC))
+
+    newServer{address="127.0.0.1:%s"}
+    """
+
+    def testBasic(self):
+        """
+        SVCB: Basic service binding
+        """
+        name = 'basic.svcb.tests.powerdns.com.'
+        query = dns.message.make_query(name, 64, 'IN')
+        # dnsdist set RA = RD for spoofed responses
+        query.flags &= ~dns.flags.RD
+
+        for method in ("sendUDPQuery", "sendTCPQuery"):
+            sender = getattr(self, method)
+            (_, receivedResponse) = sender(query, response=None, useQueue=False)
+            self.assertTrue(receivedResponse)
+            self.assertEqual(len(receivedResponse.answer), 1)
+            self.assertEqual(receivedResponse.answer[0].rdtype, 64)
+            self.assertEqual(len(receivedResponse.additional), 4)
+            self.assertEqual(receivedResponse.additional[0], dns.rrset.from_text("doh.powerdns.com.", 60, dns.rdataclass.IN, dns.rdatatype.A, '192.0.2.2'))
+            self.assertEqual(receivedResponse.additional[1], dns.rrset.from_text("dot.powerdns.com.", 60, dns.rdataclass.IN, dns.rdatatype.A, '192.0.2.1'))
+            self.assertEqual(receivedResponse.additional[2], dns.rrset.from_text("doh.powerdns.com.", 60, dns.rdataclass.IN, dns.rdatatype.AAAA, '2001:db8::2'))
+            self.assertEqual(receivedResponse.additional[3], dns.rrset.from_text("dot.powerdns.com.", 60, dns.rdataclass.IN, dns.rdatatype.AAAA, '2001:db8::1'))
+
+    def testNoHints(self):
+        """
+        SVCB: No hints
+        """
+        name = 'no-hints.svcb.tests.powerdns.com.'
+        query = dns.message.make_query(name, 64, 'IN')
+        # dnsdist set RA = RD for spoofed responses
+        query.flags &= ~dns.flags.RD
+
+        for method in ("sendUDPQuery", "sendTCPQuery"):
+            sender = getattr(self, method)
+            (_, receivedResponse) = sender(query, response=None, useQueue=False)
+            self.assertTrue(receivedResponse)
+            self.assertEqual(len(receivedResponse.answer), 1)
+            self.assertEqual(receivedResponse.answer[0].rdtype, 64)
+            self.assertEqual(len(receivedResponse.additional), 0)
+
+    def testEffectiveTarget(self):
+        """
+        SVCB: Effective target
+        """
+        name = 'effective-target.svcb.tests.powerdns.com.'
+        query = dns.message.make_query(name, 64, 'IN')
+        # dnsdist set RA = RD for spoofed responses
+        query.flags &= ~dns.flags.RD
+
+        for method in ("sendUDPQuery", "sendTCPQuery"):
+            sender = getattr(self, method)
+            (_, receivedResponse) = sender(query, response=None, useQueue=False)
+            self.assertTrue(receivedResponse)
+            self.assertEqual(len(receivedResponse.answer), 1)
+            self.assertEqual(receivedResponse.answer[0].rdtype, 64)
+            self.assertEqual(len(receivedResponse.additional), 2)
+            self.assertEqual(receivedResponse.additional[0], dns.rrset.from_text(name, 60, dns.rdataclass.IN, dns.rdatatype.A, '192.0.2.1'))
+            self.assertEqual(receivedResponse.additional[1], dns.rrset.from_text(name, 60, dns.rdataclass.IN, dns.rdatatype.AAAA, '2001:db8::1'))
+
+    def testHTTPS(self):
+        """
+        SVCB: HTTPS
+        """
+        name = 'https.svcb.tests.powerdns.com.'
+        query = dns.message.make_query(name, 65, 'IN')
+        # dnsdist set RA = RD for spoofed responses
+        query.flags &= ~dns.flags.RD
+
+        for method in ("sendUDPQuery", "sendTCPQuery"):
+            sender = getattr(self, method)
+            (_, receivedResponse) = sender(query, response=None, useQueue=False)
+            self.assertTrue(receivedResponse)
+            self.assertEqual(len(receivedResponse.answer), 1)
+            self.assertEqual(receivedResponse.answer[0].rdtype, 65)
+            self.assertEqual(len(receivedResponse.additional), 2)
+            self.assertEqual(receivedResponse.additional[0], dns.rrset.from_text(name, 60, dns.rdataclass.IN, dns.rdatatype.A, '192.0.2.2'))
+            self.assertEqual(receivedResponse.additional[1], dns.rrset.from_text(name, 60, dns.rdataclass.IN, dns.rdatatype.AAAA, '2001:db8::2'))