]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
Support for APL Records
authorNicko Dehaine <nicko@threatstop.com>
Sat, 14 Dec 2019 00:06:03 +0000 (16:06 -0800)
committerNicko Dehaine <nicko@threatstop.com>
Tue, 18 Aug 2020 23:16:44 +0000 (23:16 +0000)
This patch adds support for APL records (rfc3123)
- Tested with the BIND and SQLite3 backends
- Tested IPv4 and IPv6
- Tested Dynamic updates
- Tested negation flag
- Closes #8379

The patch includes a documentation update (need to set release version
once accepted) and unit tests.

Contributors:
nicko@threatstop.com - Nicolas Dehaine
dshabanov@threatstop.com - Dmitry Shabanov

I (nicko@threatstop.com) agree that this code can become GPLv2 licensed.

docs/appendices/types.rst
pdns/dnsrecords.cc
pdns/dnsrecords.hh
pdns/qtype.hh
pdns/test-dnsrecords_cc.cc

index f22552b7495a48c8447d8582ce29e599fd76ab22..e7dd3607e9afdd25ea37e55e7ad787825a17bc03 100644 (file)
@@ -57,6 +57,17 @@ records.
 
 .. _types-caa:
 
+APL
+-----
+
+.. versionadded:: tbd
+
+The APL record,
+specified in :rfc:`3123`, is used
+to specify a DNS RR type "APL" for address prefix lists.
+
+.. _types-caa:
+
 CAA
 ---
 
index 5d85ed1d03cbe49604b13a1815bad7f897cdbd62..bf44f6e772851292277b4f29a2ea1bb5066a8290 100644 (file)
@@ -489,6 +489,242 @@ string EUI64RecordContent::getZoneRepresentation(bool noDot) const
 
 /* EUI64 end */
 
+/* APL start */
+/* https://tools.ietf.org/html/rfc3123 */
+void APLRecordContent::report(void)
+{
+  regist(1, QType::APL, &make, &make, "APL");
+}
+// Parse incoming packets (e.g. nsupdate)
+std::shared_ptr<DNSRecordContent> APLRecordContent::make(const DNSRecord &dr, PacketReader& pr) {
+  uint8_t temp;
+
+  if(dr.d_clen < 5 or dr.d_clen > 20)
+    throw MOADNSException("Wrong size for APL record");
+
+  auto ret=std::make_shared<APLRecordContent>();
+  pr.xfr16BitInt(ret->d_family);
+  pr.xfr8BitInt(ret->d_prefix);
+  pr.xfr8BitInt(temp);
+  ret->d_n = (temp & 128) >> 7;
+  ret->d_afdlength = temp & 127;
+
+  if (ret->d_family == APL_FAMILY_IPV4) { // IPv4
+    if (ret->d_afdlength > 4) {
+      throw MOADNSException("Invalid IP length for IPv4 APL");
+    }
+    bzero(ret->d_ip4, sizeof(ret->d_ip4));
+    for (int i=0; i < 4; i++) {
+      if (i < ret->d_afdlength)
+        pr.xfr8BitInt(ret->d_ip4[i]);
+    }
+  } else if (ret->d_family == APL_FAMILY_IPV6) {
+    if (ret->d_afdlength > 16) {
+      throw MOADNSException("Invalid IP length for IPv6 APL");
+    }
+    bzero(ret->d_ip6, sizeof(ret->d_ip6));
+    for (int i=0; i< 16; i++) {
+      if (i < ret->d_afdlength)
+        pr.xfr8BitInt(ret->d_ip6[i]);
+    }
+  } else
+    throw MOADNSException("Unknown family for APL record");
+
+  return ret;
+}
+
+// Parse backend record
+std::shared_ptr<DNSRecordContent> APLRecordContent::make(const string& zone) {
+  string record;
+
+  auto ret=std::make_shared<APLRecordContent>();
+
+  // Strip the optional leading ! (negate)
+  if (zone[0] == '!') {
+    ret->d_n = 1;
+    record = zone.substr(1, zone.length()-1);
+  } else {
+    ret->d_n = 0;
+    record = zone;
+  }
+
+  if (record.find("1:", 0) == 0) { // IPv4
+    unsigned int prefix;
+    uint32_t v4ip;
+    string ipstr, subnetstr;
+    int subnet_pos;
+    ComboAddress ca;
+    int bytes;
+
+    ret->d_family = APL_FAMILY_IPV4;
+
+    // Find netmask
+     subnet_pos = record.rfind("/");
+
+    if (subnet_pos < 0) {
+      throw MOADNSException("Asked to decode '"+zone+"' as an APL record, but missing subnet mask");
+    }
+
+    // Read IPv4 string into a ComboAddress
+    ipstr = record.substr(2, subnet_pos - 2);
+    subnetstr = record.substr(subnet_pos + 1, record.length() - subnet_pos - 1);
+    ca = makeComboAddress(ipstr);
+    v4ip = ntohl(ca.sin4.sin_addr.s_addr);
+    if (ca.sin4.sin_family != AF_INET) { // ComboAddress will match v6 IPs, which is not valid here
+      throw MOADNSException("Asked to decode '"+zone+"' as an APL record, but found invalid v4 IP address");
+    }
+    // Section 4.1 of RFC 3123 (don't send trailing "0" bytes)
+    // Copy data; using array of bytes since we might end up truncating them in the packet
+    bzero(ret->d_ip4, sizeof(ret->d_ip4));
+    for (int i=0; i<4; i++) {
+      ret->d_ip4[3-i] = (v4ip & 255);
+      v4ip = v4ip >> 8;
+    }
+    // Remove trailing "0" bytes from packet and calculate length
+    bytes  = 4; // Start by assuming we'll send 4 bytes
+    v4ip = ntohl(ca.sin4.sin_addr.s_addr);
+    for (int i=0; i<4; i++) {
+      if ((v4ip & 255) == 0) {
+        // trailing 0 byte, reduce length
+        bytes--;
+      } else
+      {
+        // Found non-0 byte, stop trimming
+        break;
+      }
+      v4ip = v4ip >> 8;
+    }
+
+    // Prefix length
+    if (sscanf(subnetstr.c_str(), "%u", &prefix) == 1) {
+      ret->d_prefix=prefix;
+    } else
+          throw MOADNSException("Asked to decode '"+zone+"' as an IPv4 APL record, but found invalid prefix string "+subnetstr);
+    if (prefix > 32)
+      throw MOADNSException("Asked to decode '"+zone+"' as an IPv4 APL record, but found invalid prefix value "+std::to_string(prefix));
+    ret->d_afdlength = bytes;
+
+  } else if (record.find("2:", 0) == 0) { // IPv6
+    unsigned int prefix;
+    int subnet_pos;
+    string ipstr, subnetstr;
+    ComboAddress ca;
+    int bytes;
+
+    ret->d_family = APL_FAMILY_IPV6;
+
+    // Find Netmask
+    subnet_pos = record.rfind("/");
+    if (subnet_pos < 0) {
+      throw MOADNSException("Asked to decode '"+zone+"' as an APL record, but missing subnet mask");
+    }
+
+    // Parse IPv6 string into ComboAddress
+    ipstr = record.substr(2, subnet_pos - 2);
+    subnetstr = record.substr(subnet_pos + 1, record.length() - subnet_pos - 1);
+    ca = makeComboAddress(ipstr);
+
+    // Section 4.2 of RFC 3123 (don't send trailing "0" bytes)
+    // Remove trailing "0" bytes from packet and reduce length
+    bytes = 16; // Start by assuming we'll send 16 bytes
+    for (int i=15; i>=0; i--) {
+      if (ca.sin6.sin6_addr.s6_addr[i] == 0) {
+        // trailing 0 byte, calculate length
+        bytes--;
+      } else {
+        // Found non-0 byte, stop trimming
+        break;
+      }
+    }
+    bzero(ret->d_ip6, sizeof(ret->d_ip6));
+    for (int i=0; i<bytes; i++) {
+      ret->d_ip6[i] = ca.sin6.sin6_addr.s6_addr[i];
+    }
+    ret->d_afdlength = bytes;
+
+    // Prefix length
+    prefix = 0;
+    if (sscanf(subnetstr.c_str(), "%u", &prefix) == 1) {
+      ret->d_prefix=prefix;
+    } else
+          throw MOADNSException("Asked to decode '"+zone+"' as an IPv6 APL record, but found invalid prefix string "+subnetstr);
+    if (prefix > 32)
+      throw MOADNSException("Asked to decode '"+zone+"' as an IPv6 APL record, but found invalid prefix value "+subnetstr);
+   } else {
+      throw MOADNSException("Asked to encode '"+zone+"' as an IPv6 APL record but got unknown Address Family");
+  }
+
+  return ret;
+}
+
+// DNSRecord to Packet conversion
+void APLRecordContent::toPacket(DNSPacketWriter& pw) {
+
+    pw.xfr16BitInt(d_family);
+    pw.xfr8BitInt(d_prefix);
+    pw.xfr8BitInt((d_n << 7) + d_afdlength);
+    if (d_family == APL_FAMILY_IPV4) {
+      for (int i=0; i<d_afdlength; i++) {
+        pw.xfr8BitInt(d_ip4[i]);
+      }
+    } else if (d_family == APL_FAMILY_IPV6) {
+      for (int i=0; i<d_afdlength; i++) {
+        pw.xfr8BitInt(d_ip6[i]);
+      }
+    }
+}
+
+// Decode record into string
+string APLRecordContent::getZoneRepresentation(bool noDot) const {
+  string s_n, s_family, s_ip;
+
+  // Negation flag
+  if (d_n == 1) {
+    s_n = "!";
+  } else {
+    s_n = "";
+  }
+
+  if (d_family == APL_FAMILY_IPV4) { // IPv4
+    sockaddr_in sa;
+
+    s_family = std::to_string(APL_FAMILY_IPV4);
+    s_ip = "";
+    sa.sin_family = AF_INET;
+    sa.sin_port = 0;
+    sa.sin_addr.s_addr = 0;
+    for (int i=0; i < 4; i++) {
+        sa.sin_addr.s_addr |= d_ip4[i] << (i*8);
+    }
+    ComboAddress ca = ComboAddress(&sa);
+    s_ip = ca.toString();
+
+  } else if (d_family == APL_FAMILY_IPV6) { // IPv6
+    sockaddr_in6 sa;
+    s_family = std::to_string(APL_FAMILY_IPV6);
+    s_ip = "";
+    sa.sin6_family = AF_INET6;
+    sa.sin6_port = 0;
+    sa.sin6_flowinfo = 0;
+    sa.sin6_scope_id = 0;
+    for (int i=0; i < 16; i++) {
+      if (i < d_afdlength) {
+        sa.sin6_addr.s6_addr[i] = d_ip6[i];
+      } else {
+        sa.sin6_addr.s6_addr[i] = 0;
+      }
+    }
+    ComboAddress ca = ComboAddress(&sa);
+    s_ip = ca.toString();
+  } else {
+    throw MOADNSException("Asked to decode APL record but got unknown Address Family "+d_family);
+  }
+
+  return s_n + s_family + ":" + s_ip + "/" + std::to_string(d_prefix);
+}
+
+/* APL end */
+
 boilerplate_conv(TKEY, QType::TKEY,
                  conv.xfrName(d_algo);
                  conv.xfr32BitInt(d_inception);
@@ -651,6 +887,7 @@ void reportOtherTypes()
    MINFORecordContent::report();
    URIRecordContent::report();
    CAARecordContent::report();
+   APLRecordContent::report();
 }
 
 void reportAllTypes()
index 3c2ab3d6f884345d110815ccdec4f8c84234d02f..f78cf6ab657df9c4678f00f6578d3b940fd4db90 100644 (file)
@@ -784,6 +784,28 @@ private:
  uint8_t d_eui64[8];
 };
 
+#define APL_FAMILY_IPV4 1
+#define APL_FAMILY_IPV6 2
+class APLRecordContent : public DNSRecordContent
+{
+public:
+  APLRecordContent() {};
+  static void report(void);
+  static std::shared_ptr<DNSRecordContent> make(const DNSRecord &dr, PacketReader& pr);
+  static std::shared_ptr<DNSRecordContent> make(const string& zone); // FIXME400: DNSName& zone?
+  string getZoneRepresentation(bool noDot=false) const override;
+  void toPacket(DNSPacketWriter& pw) override;
+  uint16_t getType() const override { return QType::APL; }
+private:
+  uint16_t d_family;
+  uint8_t d_prefix;
+  unsigned int d_n : 1;
+  unsigned int d_afdlength : 7;
+  uint8_t d_ip4[4];
+  unsigned char d_ip6[16];
+};
+
+
 class TKEYRecordContent : public DNSRecordContent
 {
 public:
index 03b6c8a5c78d89dfb652f7ad39e0147f98e6d0ba..5c9b80cf46a98915a016b46bfda3eea365b0f1d8 100644 (file)
@@ -95,6 +95,7 @@ public:
     A6=38,
     DNAME=39,
     OPT=41,
+    APL=42,
     DS=43,
     SSHFP=44,
     IPSECKEY=45,
@@ -194,6 +195,7 @@ private:
       qtype_insert("A6", 38);
       qtype_insert("DNAME", 39);
       qtype_insert("OPT", 41);
+      qtype_insert("APL", 42);
       qtype_insert("DS", 43);
       qtype_insert("SSHFP", 44);
       qtype_insert("IPSECKEY", 45);
index 441f6464d0cf50384aa36251c698d1ff550ed190..4cf81d427b1c6fa84a9df661dc9e323fd4a291b1 100644 (file)
@@ -132,6 +132,17 @@ BOOST_AUTO_TEST_CASE(test_record_types) {
 
 // X.509 as per PKIX
      (CASE_S(QType::CERT, "1 0 0 MIIB9DCCAV2gAwIBAgIJAKxUfFVXhw7HMA0GCSqGSIb3DQEBBQUAMBMxETAPBgNVBAMMCHJlYy50ZXN0MB4XDTEzMDUxMjE5NDgwOVoXDTEzMDYxMTE5NDgwOVowEzERMA8GA1UEAwwIcmVjLnRlc3QwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBANKCu5aN/ewOXRPfzAo27XMXhYFCThCjfInTAUIEkzs6jBFZ/eyyIa/kFoiD0tAKwfFfykYU+9XgXeLjetD7rYt3SN3bzzCznoBGbGHHM0Fecrn0LV+tC/NfBB61Yx7e0AMUxmxIeLNRQW5ca5CW8qcIiiQ4fl0BScUjc5+E9QLHAgMBAAGjUDBOMB0GA1UdDgQWBBRzcVu/2bwrgkES+FhYbxZqr7mUgjAfBgNVHSMEGDAWgBRzcVu/2bwrgkES+FhYbxZqr7mUgjAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4GBAFVQ8dZBOasOhsWzA/xpAV0WdsqVkxBxrkGIRlbHHBFqOBOOz2MFSzUNx4mDy0qDKI28gcWmWaVsxoQ9VFLD6YRJuUoM8MDNcZDJbKpfDumjvvfnUAK+SiM2c4Ur3xpf0wanCA60/q2bOtFiB0tfAH6RVuIgMC3qjHAIaKEld+fE", "\x00\x01\x00\x00\x00\x30\x82\x01\xf4\x30\x82\x01\x5d\xa0\x03\x02\x01\x02\x02\x09\x00\xac\x54\x7c\x55\x57\x87\x0e\xc7\x30\x0d\x06\x09\x2a\x86\x48\x86\xf7\x0d\x01\x01\x05\x05\x00\x30\x13\x31\x11\x30\x0f\x06\x03\x55\x04\x03\x0c\x08\x72\x65\x63\x2e\x74\x65\x73\x74\x30\x1e\x17\x0d\x31\x33\x30\x35\x31\x32\x31\x39\x34\x38\x30\x39\x5a\x17\x0d\x31\x33\x30\x36\x31\x31\x31\x39\x34\x38\x30\x39\x5a\x30\x13\x31\x11\x30\x0f\x06\x03\x55\x04\x03\x0c\x08\x72\x65\x63\x2e\x74\x65\x73\x74\x30\x81\x9f\x30\x0d\x06\x09\x2a\x86\x48\x86\xf7\x0d\x01\x01\x01\x05\x00\x03\x81\x8d\x00\x30\x81\x89\x02\x81\x81\x00\xd2\x82\xbb\x96\x8d\xfd\xec\x0e\x5d\x13\xdf\xcc\x0a\x36\xed\x73\x17\x85\x81\x42\x4e\x10\xa3\x7c\x89\xd3\x01\x42\x04\x93\x3b\x3a\x8c\x11\x59\xfd\xec\xb2\x21\xaf\xe4\x16\x88\x83\xd2\xd0\x0a\xc1\xf1\x5f\xca\x46\x14\xfb\xd5\xe0\x5d\xe2\xe3\x7a\xd0\xfb\xad\x8b\x77\x48\xdd\xdb\xcf\x30\xb3\x9e\x80\x46\x6c\x61\xc7\x33\x41\x5e\x72\xb9\xf4\x2d\x5f\xad\x0b\xf3\x5f\x04\x1e\xb5\x63\x1e\xde\xd0\x03\x14\xc6\x6c\x48\x78\xb3\x51\x41\x6e\x5c\x6b\x90\x96\xf2\xa7\x08\x8a\x24\x38\x7e\x5d\x01\x49\xc5\x23\x73\x9f\x84\xf5\x02\xc7\x02\x03\x01\x00\x01\xa3\x50\x30\x4e\x30\x1d\x06\x03\x55\x1d\x0e\x04\x16\x04\x14\x73\x71\x5b\xbf\xd9\xbc\x2b\x82\x41\x12\xf8\x58\x58\x6f\x16\x6a\xaf\xb9\x94\x82\x30\x1f\x06\x03\x55\x1d\x23\x04\x18\x30\x16\x80\x14\x73\x71\x5b\xbf\xd9\xbc\x2b\x82\x41\x12\xf8\x58\x58\x6f\x16\x6a\xaf\xb9\x94\x82\x30\x0c\x06\x03\x55\x1d\x13\x04\x05\x30\x03\x01\x01\xff\x30\x0d\x06\x09\x2a\x86\x48\x86\xf7\x0d\x01\x01\x05\x05\x00\x03\x81\x81\x00\x55\x50\xf1\xd6\x41\x39\xab\x0e\x86\xc5\xb3\x03\xfc\x69\x01\x5d\x16\x76\xca\x95\x93\x10\x71\xae\x41\x88\x46\x56\xc7\x1c\x11\x6a\x38\x13\x8e\xcf\x63\x05\x4b\x35\x0d\xc7\x89\x83\xcb\x4a\x83\x28\x8d\xbc\x81\xc5\xa6\x59\xa5\x6c\xc6\x84\x3d\x54\x52\xc3\xe9\x84\x49\xb9\x4a\x0c\xf0\xc0\xcd\x71\x90\xc9\x6c\xaa\x5f\x0e\xe9\xa3\xbe\xf7\xe7\x50\x02\xbe\x4a\x23\x36\x73\x85\x2b\xdf\x1a\x5f\xd3\x06\xa7\x08\x0e\xb4\xfe\xad\x9b\x3a\xd1\x62\x07\x4b\x5f\x00\x7e\x91\x56\xe2\x20\x30\x2d\xea\x8c\x70\x08\x68\xa1\x25\x77\xe7\xc4"))
+     (CASE_S(QType::APL,"1:10.1.1.1/32", "\x00\x01\x20\x04\x0a\x01\x01\x01"))
+     (CASE_S(QType::APL,"1:10.1.1.0/24", "\x00\x01\x18\x03\x0a\x01\x01"))
+     (CASE_S(QType::APL,"1:60.0.0.0/8", "\x00\x01\x08\x01\x3c"))
+     (CASE_S(QType::APL,"1:10.1.1.1/32", "\x00\x01\x20\x04\x0a\x01\x01\x01"))
+     (CASE_S(QType::APL,"!1:10.1.1.1/32", "\x00\x01\x20\x84\x0a\x01\x01\x01"))
+     (CASE_S(QType::APL,"1:255.255.255.255/32", "\x00\x01\x20\x04\xff\xff\xff\xff"))
+     (CASE_S(QType::APL,"2:fe80::/8", "\x00\x02\x08\x02\xfe\x80"))
+     (CASE_S(QType::APL,"2:2001::1/32", "\x00\x02\x20\x10\x20\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01"))
+     (CASE_S(QType::APL,"!2:2001::1/32", "\x00\x02\x20\x90\x20\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01"))
+     (CASE_S(QType::APL,"2:fe80:1234:5678:9910:8bc:3359:b2e8:720e/32", "\x00\x02\x20\x10\xfe\x80\x12\x34\x56\x78\x99\x10\x08\xbc\x33\x59\xb2\xe8\x72\x0e"))
+     (CASE_S(QType::APL,"2:ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/32","\x00\x02\x20\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff"))
      (CASE_L(QType::DS, "20642 8 2 04443ABE7E94C3985196BEAE5D548C727B044DDA5151E60D7CD76A9F D931D00E", "20642 8 2 04443abe7e94c3985196beae5d548c727b044dda5151e60d7cd76a9fd931d00e", "\x50\xa2\x08\x02\x04\x44\x3a\xbe\x7e\x94\xc3\x98\x51\x96\xbe\xae\x5d\x54\x8c\x72\x7b\x04\x4d\xda\x51\x51\xe6\x0d\x7c\xd7\x6a\x9f\xd9\x31\xd0\x0e"))
      (CASE_S(QType::SSHFP, "1 1 aa65e3415a50d9b3519c2b17aceb815fc2538d88", "\x01\x01\xaa\x65\xe3\x41\x5a\x50\xd9\xb3\x51\x9c\x2b\x17\xac\xeb\x81\x5f\xc2\x53\x8d\x88"))
      (CASE_L(QType::SSHFP, "1 1 aa65e3415a50d9b3519c2b17aceb815fc253 8d88", "1 1 aa65e3415a50d9b3519c2b17aceb815fc2538d88", "\x01\x01\xaa\x65\xe3\x41\x5a\x50\xd9\xb3\x51\x9c\x2b\x17\xac\xeb\x81\x5f\xc2\x53\x8d\x88"))