]> git.ipfire.org Git - thirdparty/curl.git/commitdiff
test1658: add unit test for the HTTPS RR decoder
authorDaniel Stenberg <daniel@haxx.se>
Fri, 4 Apr 2025 21:21:41 +0000 (23:21 +0200)
committerDaniel Stenberg <daniel@haxx.se>
Sat, 5 Apr 2025 19:03:47 +0000 (21:03 +0200)
Made the HTTPS-RR parser a little stricter while at it.

Drop the ALPN escape handling, that was not needed.

Make the hode handle (and ignore) duplicate ALPN entries.

Closes #16972

lib/doh.c
lib/httpsrr.c
lib/httpsrr.h
lib/urlapi-int.h
lib/urlapi.c
tests/data/Makefile.am
tests/data/test1658 [new file with mode: 0644]
tests/unit/Makefile.inc
tests/unit/unit1658.c [new file with mode: 0644]

index def9781e474a69e9db6f4acf01a43d7e4ba27603..fe5faccbef47275de54fd328b40ab181fe9ccbc3 100644 (file)
--- a/lib/doh.c
+++ b/lib/doh.c
 #include "connect.h"
 #include "strdup.h"
 #include "dynbuf.h"
+#include "escape.h"
+#include "urlapi-int.h"
+
 /* The last 3 #include files should be in this order */
 #include "curl_printf.h"
 #include "curl_memory.h"
 #include "memdebug.h"
-#include "escape.h"
 
 #define DNS_CLASS_IN 0x01
 
@@ -1014,10 +1016,10 @@ UNITTEST void de_cleanup(struct dohentry *d)
  * just after the end of the DNS name encoding on output. (And
  * that is why it is an "unsigned char **" :-)
  */
-static CURLcode doh_decode_rdata_name(unsigned char **buf, size_t *remaining,
-                                      char **dnsname)
+static CURLcode doh_decode_rdata_name(const unsigned char **buf,
+                                      size_t *remaining, char **dnsname)
 {
-  unsigned char *cp = NULL;
+  const unsigned char *cp = NULL;
   size_t rem = 0;
   unsigned char clen = 0; /* chunk len */
   struct dynbuf thename;
@@ -1057,43 +1059,23 @@ static CURLcode doh_decode_rdata_name(unsigned char **buf, size_t *remaining,
   return CURLE_OK;
 }
 
-#ifdef DEBUGBUILD
-static CURLcode doh_test_alpn_escapes(void)
-{
-  /* we will use an example from draft-ietf-dnsop-svcb, figure 10 */
-  static unsigned char example[] = {
-    0x08,                                           /* length 8 */
-    0x66, 0x5c, 0x6f, 0x6f, 0x2c, 0x62, 0x61, 0x72, /* value "f\\oo,bar" */
-    0x02,                                           /* length 2 */
-    0x68, 0x32                                      /* value "h2" */
-  };
-  size_t example_len = sizeof(example);
-  unsigned char aval[MAX_HTTPSRR_ALPNS] = { 0 };
-  static const char expected[2] = { ALPN_h2, ALPN_none };
-
-  if(Curl_httpsrr_decode_alpn(example, example_len, aval) != CURLE_OK)
-    return CURLE_BAD_CONTENT_ENCODING;
-  if(memcmp(aval, expected, sizeof(expected)))
-    return CURLE_BAD_CONTENT_ENCODING;
-  return CURLE_OK;
-}
-#endif
+UNITTEST CURLcode doh_resp_decode_httpsrr(struct Curl_easy *data,
+                                          const unsigned char *cp, size_t len,
+                                          struct Curl_https_rrinfo **hrr);
 
-static CURLcode doh_resp_decode_httpsrr(struct Curl_easy *data,
-                                        unsigned char *cp, size_t len,
-                                        struct Curl_https_rrinfo **hrr)
+/* @unittest 1658 */
+UNITTEST CURLcode doh_resp_decode_httpsrr(struct Curl_easy *data,
+                                          const unsigned char *cp, size_t len,
+                                          struct Curl_https_rrinfo **hrr)
 {
   uint16_t pcode = 0, plen = 0;
   uint32_t expected_min_pcode = 0;
   struct Curl_https_rrinfo *lhrr = NULL;
   char *dnsname = NULL;
   CURLcode result = CURLE_OUT_OF_MEMORY;
+  size_t olen;
 
-#ifdef DEBUGBUILD
-  /* a few tests of escaping, should not be here but ok for now */
-  if(doh_test_alpn_escapes() != CURLE_OK)
-    return CURLE_OUT_OF_MEMORY;
-#endif
+  *hrr = NULL;
   if(len <= 2)
     return CURLE_BAD_FUNCTION_ARGUMENT;
   lhrr = calloc(1, sizeof(struct Curl_https_rrinfo));
@@ -1105,6 +1087,11 @@ static CURLcode doh_resp_decode_httpsrr(struct Curl_easy *data,
   if(doh_decode_rdata_name(&cp, &len, &dnsname) != CURLE_OK)
     goto err;
   lhrr->target = dnsname;
+  if(Curl_junkscan(dnsname, &olen, FALSE)) {
+    /* unacceptable hostname content */
+    result = CURLE_WEIRD_SERVER_REPLY;
+    goto err;
+  }
   lhrr->port = -1; /* until set */
   while(len >= 4) {
     pcode = doh_get16bit(cp, 0);
@@ -1131,9 +1118,12 @@ err:
   return result;
 }
 
-# ifdef DEBUGBUILD
-static void doh_print_httpsrr(struct Curl_easy *data,
-                              struct Curl_https_rrinfo *hrr)
+#ifdef DEBUGBUILD
+UNITTEST void doh_print_httpsrr(struct Curl_easy *data,
+                                struct Curl_https_rrinfo *hrr);
+
+UNITTEST void doh_print_httpsrr(struct Curl_easy *data,
+                                struct Curl_https_rrinfo *hrr)
 {
   DEBUGASSERT(hrr);
   infof(data, "HTTPS RR: priority %d, target: %s",
index 9a0bc827ef8156c8e15703ae1d4e92055dae4302..003790bcbc482f14358881a7949aad2bc525cfb9 100644 (file)
 #include "curl_memory.h"
 #include "memdebug.h"
 
-CURLcode Curl_httpsrr_decode_alpn(const unsigned char *cp, size_t len,
-                                  unsigned char *alpns)
+#define MAX_ALPN_LENGTH 255
+
+static CURLcode httpsrr_decode_alpn(const char *cp, size_t len,
+                                    unsigned char *alpns)
 {
   /*
-   * spec here is as per RFC 9460, section-7.1.1
-   * encoding is a concatenated list of strings each preceded by a one
-   * octet length
-   * output is comma-sep list of the strings
-   * implementations may or may not handle quoting of comma within
-   * string values, so we might see a comma within the wire format
-   * version of a string, in which case we will precede that by a
-   * backslash - same goes for a backslash character, and of course
-   * we need to use two backslashes in strings when we mean one;-)
+   * The wire-format value for "alpn" consists of at least one alpn-id
+   * prefixed by its length as a single octet, and these length-value pairs
+   * are concatenated to form the SvcParamValue. These pairs MUST exactly fill
+   * the SvcParamValue; otherwise, the SvcParamValue is malformed.
    */
-  struct dynbuf dval;
   int idnum = 0;
 
-  Curl_dyn_init(&dval, DYN_DOH_RESPONSE);
   while(len > 0) {
     size_t tlen = (size_t) *cp++;
-    size_t i;
     enum alpnid id;
     len--;
     if(tlen > len)
-      goto err;
-    /* add escape char if needed, clunky but easier to read */
-    for(i = 0; i != tlen; i++) {
-      if('\\' == *cp || ',' == *cp) {
-        if(Curl_dyn_addn(&dval, "\\", 1))
-          goto err;
-      }
-      if(Curl_dyn_addn(&dval, cp++, 1))
-        goto err;
-    }
-    len -= tlen;
+      return CURLE_BAD_CONTENT_ENCODING;
 
     /* we only store ALPN ids we know about */
-    id = Curl_alpn2alpnid(Curl_dyn_ptr(&dval), Curl_dyn_len(&dval));
+    id = Curl_alpn2alpnid(cp, tlen);
     if(id != ALPN_none) {
       if(idnum == MAX_HTTPSRR_ALPNS)
         break;
-      alpns[idnum++] = (unsigned char)id;
+      if(idnum && memchr(alpns, id, idnum))
+        /* this ALPN id is already stored */
+        ;
+      else
+        alpns[idnum++] = (unsigned char)id;
     }
-    Curl_dyn_reset(&dval);
+    cp += tlen;
+    len -= tlen;
   }
-  Curl_dyn_free(&dval);
   if(idnum < MAX_HTTPSRR_ALPNS)
     alpns[idnum] = ALPN_none; /* terminate the list */
   return CURLE_OK;
-err:
-  Curl_dyn_free(&dval);
-  return CURLE_BAD_CONTENT_ENCODING;
 }
 
 CURLcode Curl_httpsrr_set(struct Curl_easy *data,
                           struct Curl_https_rrinfo *hi,
                           uint16_t rrkey, const uint8_t *val, size_t vlen)
 {
+  CURLcode result = CURLE_OK;
   switch(rrkey) {
+  case HTTPS_RR_CODE_MANDATORY:
+    CURL_TRC_DNS(data, "HTTPS RR MANDATORY left to implement");
+    break;
   case HTTPS_RR_CODE_ALPN: /* str_list */
-    Curl_httpsrr_decode_alpn(val, vlen, hi->alpns);
+    result = httpsrr_decode_alpn((const char *)val, vlen, hi->alpns);
     CURL_TRC_DNS(data, "HTTPS RR ALPN: %u %u %u %u",
                  hi->alpns[0], hi->alpns[1], hi->alpns[2], hi->alpns[3]);
     break;
   case HTTPS_RR_CODE_NO_DEF_ALPN:
+    if(vlen) /* no data */
+      return CURLE_BAD_FUNCTION_ARGUMENT;
     hi->no_def_alpn = TRUE;
     CURL_TRC_DNS(data, "HTTPS RR no-def-alpn");
     break;
   case HTTPS_RR_CODE_IPV4: /* addr4 list */
-    if(!vlen)
+    if(!vlen || (vlen & 3)) /* the size must be 4-byte aligned */
       return CURLE_BAD_FUNCTION_ARGUMENT;
     hi->ipv4hints = Curl_memdup(val, vlen);
     if(!hi->ipv4hints)
@@ -125,7 +116,7 @@ CURLcode Curl_httpsrr_set(struct Curl_easy *data,
     CURL_TRC_DNS(data, "HTTPS RR ECH");
     break;
   case HTTPS_RR_CODE_IPV6: /* addr6 list */
-    if(!vlen)
+    if(!vlen || (vlen & 15)) /* the size must be 16-byte aligned */
       return CURLE_BAD_FUNCTION_ARGUMENT;
     hi->ipv6hints = Curl_memdup(val, vlen);
     if(!hi->ipv6hints)
@@ -143,7 +134,7 @@ CURLcode Curl_httpsrr_set(struct Curl_easy *data,
     CURL_TRC_DNS(data, "HTTPS RR unknown code");
     break;
   }
-  return CURLE_OK;
+  return result;
 }
 
 struct Curl_https_rrinfo *
index 847aa9abf637fc8c5962fee756592f96d2884e9a..1d71a57433654f62265cdd5d8ead7746795d5b5b 100644 (file)
@@ -68,6 +68,7 @@ void Curl_httpsrr_cleanup(struct Curl_https_rrinfo *rrinfo);
 /*
  * Code points for DNS wire format SvcParams as per RFC 9460
  */
+#define HTTPS_RR_CODE_MANDATORY       0x00
 #define HTTPS_RR_CODE_ALPN            0x01
 #define HTTPS_RR_CODE_NO_DEF_ALPN     0x02
 #define HTTPS_RR_CODE_PORT            0x03
@@ -75,9 +76,6 @@ void Curl_httpsrr_cleanup(struct Curl_https_rrinfo *rrinfo);
 #define HTTPS_RR_CODE_ECH             0x05
 #define HTTPS_RR_CODE_IPV6            0x06
 
-CURLcode Curl_httpsrr_decode_alpn(const unsigned char *cp, size_t len,
-                                  unsigned char *alpns);
-
 #if defined(USE_ARES)
 void Curl_dnsrec_done_cb(void *arg, ares_status_t status,
                          size_t timeouts,
index fcffab2e9542207d9311a5a70733c1d26eeda461..fbce1837ff94fb6c74d47847f687a53332edfeca 100644 (file)
@@ -30,6 +30,8 @@ size_t Curl_is_absolute_url(const char *url, char *buf, size_t buflen,
 
 CURLUcode Curl_url_set_authority(CURLU *u, const char *authority);
 
+CURLUcode Curl_junkscan(const char *url, size_t *urllen, bool allowspace);
+
 #ifdef UNITTESTS
 UNITTEST CURLUcode Curl_parse_port(struct Curl_URL *u, struct dynbuf *host,
                                    bool has_scheme);
index bbf7947c2a9af73cdfc495652e459c0e2dbe514d..88cfa824183e4b4b80032587dc5b94ce2f5837f5 100644 (file)
@@ -303,7 +303,7 @@ static CURLUcode redirect_url(const char *base, const char *relurl,
 }
 
 /* scan for byte values <= 31, 127 and sometimes space */
-static CURLUcode junkscan(const char *url, size_t *urllen, bool allowspace)
+CURLUcode Curl_junkscan(const char *url, size_t *urllen, bool allowspace)
 {
   size_t n = strlen(url);
   size_t i;
@@ -918,7 +918,7 @@ static CURLUcode parseurl(const char *url, CURLU *u, unsigned int flags)
 
   Curl_dyn_init(&host, CURL_MAX_INPUT_LENGTH);
 
-  result = junkscan(url, &urllen, !!(flags & CURLU_ALLOW_SPACE));
+  result = Curl_junkscan(url, &urllen, !!(flags & CURLU_ALLOW_SPACE));
   if(result)
     goto fail;
 
index 02c3cbe5d5f1dc08b948928d0964c6e68b071f1a..00f5b659c7e50b808445bb44630f6f3f17400958 100644 (file)
@@ -220,6 +220,7 @@ test1620 test1621 \
 test1630 test1631 test1632 test1633 test1634 test1635 \
 \
 test1650 test1651 test1652 test1653 test1654 test1655 test1656 test1657 \
+test1658 \
 test1660 test1661 test1662 test1663 test1664 \
 \
 test1670 test1671 \
diff --git a/tests/data/test1658 b/tests/data/test1658
new file mode 100644 (file)
index 0000000..96e247c
--- /dev/null
@@ -0,0 +1,30 @@
+<testcase>
+<info>
+<keywords>
+unittest
+doh
+httpsrr
+</keywords>
+</info>
+
+#
+# Client-side
+<client>
+<server>
+none
+</server>
+<features>
+unittest
+DoH
+</features>
+<name>
+unit test for doh_resp_decode_httpsrr
+</name>
+</client>
+<verify>
+<stderr mode="text">
+URL: -
+Test ended with result 0
+</stderr>
+</verify>
+</testcase>
index 4a9369326eeb8bb6f947752ca98c15439a559e83..0c13da94c93c272c7c0c75c7ec03070839429eaa 100644 (file)
@@ -39,6 +39,7 @@ UNITPROGS = unit1300          unit1302 unit1303 unit1304 unit1305 unit1307 \
  unit1608 unit1609 unit1610 unit1611 unit1612 unit1614 unit1615 unit1616 \
  unit1620 unit1621 \
  unit1650 unit1651 unit1652 unit1653 unit1654 unit1655 unit1656 unit1657 \
+ unit1658 \
  unit1660 unit1661 unit1663 unit1664 \
  unit2600 unit2601 unit2602 unit2603 unit2604 \
  unit3200 \
@@ -126,6 +127,8 @@ unit1656_SOURCES = unit1656.c $(UNITFILES)
 
 unit1657_SOURCES = unit1657.c $(UNITFILES)
 
+unit1658_SOURCES = unit1658.c $(UNITFILES)
+
 unit1660_SOURCES = unit1660.c $(UNITFILES)
 
 unit1661_SOURCES = unit1661.c $(UNITFILES)
diff --git a/tests/unit/unit1658.c b/tests/unit/unit1658.c
new file mode 100644 (file)
index 0000000..c6f28b2
--- /dev/null
@@ -0,0 +1,553 @@
+/***************************************************************************
+ *                                  _   _ ____  _
+ *  Project                     ___| | | |  _ \| |
+ *                             / __| | | | |_) | |
+ *                            | (__| |_| |  _ <| |___
+ *                             \___|\___/|_| \_\_____|
+ *
+ * Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
+ *
+ * This software is licensed as described in the file COPYING, which
+ * you should have received as part of this distribution. The terms
+ * are also available at https://curl.se/docs/copyright.html.
+ *
+ * You may opt to use, copy, modify, merge, publish, distribute and/or sell
+ * copies of the Software, and permit persons to whom the Software is
+ * furnished to do so, under the terms of the COPYING file.
+ *
+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+ * KIND, either express or implied.
+ *
+ * SPDX-License-Identifier: curl
+ *
+ ***************************************************************************/
+#include "curlcheck.h"
+
+#include "doh.h" /* from the lib dir */
+
+static CURLcode unit_setup(void)
+{
+  /* whatever you want done first */
+  curl_global_init(CURL_GLOBAL_ALL);
+  return CURLE_OK;
+}
+
+static void unit_stop(void)
+{
+  curl_global_cleanup();
+  /* done before shutting down and exiting */
+}
+
+/* DoH + HTTPSRR are required */
+#if !defined(CURL_DISABLE_DOH) && defined(USE_HTTPSRR)
+
+extern CURLcode doh_resp_decode_httpsrr(struct Curl_easy *data,
+                                        const unsigned char *cp, size_t len,
+                                        struct Curl_https_rrinfo **hrr);
+extern void doh_print_httpsrr(struct Curl_easy *data,
+                              struct Curl_https_rrinfo *hrr);
+
+struct test {
+  const char *name;
+  const unsigned char *dns;
+  size_t len; /* size of the dns packet */
+  const char *expect;
+};
+
+/*
+ * The idea here is that we pass one DNS packet at the time to the decoder. we
+ * then generate a string output with the results and compare if it matches
+ * the expected. One by one.
+ */
+
+static char rrbuffer[256];
+static void rrresults(struct Curl_https_rrinfo *rr, CURLcode result)
+{
+  char *p = rrbuffer;
+  char *pend = rrbuffer + sizeof(rrbuffer);
+  msnprintf(rrbuffer, sizeof(rrbuffer), "r:%d|", (int)result);
+  p += strlen(rrbuffer);
+
+  if(rr) {
+    unsigned int i;
+    msnprintf(p, pend - p, "p:%d|", rr->priority);
+    p += strlen(p);
+
+    msnprintf(p, pend - p, "%s|", rr->target ? rr->target : "-");
+    p += strlen(p);
+
+    for(i = 0; i < MAX_HTTPSRR_ALPNS && rr->alpns[i] != ALPN_none; i++) {
+      msnprintf(p, pend - p, "alpn:%x|", rr->alpns[i]);
+      p += strlen(p);
+    }
+    if(rr->no_def_alpn) {
+      msnprintf(p, pend - p, "no-def-alpn|");
+      p += strlen(p);
+    }
+    if(rr->port >= 0) {
+      msnprintf(p, pend - p, "port:%d|", rr->port);
+      p += strlen(p);
+    }
+    if(rr->ipv4hints) {
+      for(i = 0; i < rr->ipv4hints_len; i += 4) {
+        msnprintf(p, pend - p, "ipv4:%d.%d.%d.%d|",
+                  rr->ipv4hints[i],
+                  rr->ipv4hints[i + 1],
+                  rr->ipv4hints[i + 2],
+                  rr->ipv4hints[i + 3]);
+        p += strlen(p);
+      }
+    }
+    if(rr->echconfiglist) {
+      msnprintf(p, pend - p, "ech:");
+      p += strlen(p);
+      for(i = 0; i < rr->echconfiglist_len; i++) {
+        msnprintf(p, pend - p, "%02x", rr->echconfiglist[i]);
+        p += strlen(p);
+      }
+      msnprintf(p, pend - p, "|");
+      p += strlen(p);
+    }
+    if(rr->ipv6hints) {
+      for(i = 0; i < rr->ipv6hints_len; i += 16) {
+        int x;
+        msnprintf(p, pend - p, "ipv6:");
+        p += strlen(p);
+        for(x = 0; x < 16; x += 2) {
+          msnprintf(p, pend - p, "%s%02x%02x",
+                    x ? ":" : "",
+                    rr->ipv6hints[i + x],
+                    rr->ipv6hints[i + x + 1]);
+          p += strlen(p);
+        }
+        msnprintf(p, pend - p, "|");
+        p += strlen(p);
+      }
+    }
+  }
+}
+
+UNITTEST_START
+{
+  /* The "SvcParamKeys" specified within the HTTPS RR packet *must* be
+     provided in numerical order. */
+
+  static struct test t[] = {
+    {
+      "single h2 alpn",
+      (const unsigned char *)"\x00\x00" /* 16-bit prio */
+      "\x04name\x00" /* RNAME */
+      "\x00\x01" /* RR (1 == ALPN) */
+      "\x00\x03" /* data size */
+      "\x02" /* length byte */
+      "h2",
+      15,
+      "r:0|p:0|name.|alpn:10|"
+    },
+    {
+      "single h2 alpn missing last byte",
+      (const unsigned char *)"\x00\x00" /* 16-bit prio */
+      "\x04name\x00" /* RNAME */
+      "\x00\x01" /* RR (1 == ALPN) */
+      "\x00\x03" /* data size */
+      "\x02" /* length byte */
+      "h", /* missing byte */
+      14,
+      "r:8|"
+    },
+    {
+      "two alpns",
+      (const unsigned char *)"\x00\x00" /* 16-bit prio */
+      "\x04name\x04some\x00" /* RNAME */
+      "\x00\x01" /* RR (1 == ALPN) */
+      "\x00\x06" /* data size */
+      "\x02" /* ALPN length byte */
+      "h2"
+      "\x02" /* APLN length byte */
+      "h3",
+      23,
+      "r:0|p:0|name.some.|alpn:10|alpn:20|"
+    },
+    {
+      "wrong syntax alpns",
+      (const unsigned char *)"\x00\x00" /* 16-bit prio */
+      "\x04name\x04some\x00" /* RNAME */
+      "\x00\x01" /* RR (1 == ALPN) */
+      "\x00\x06" /* data size */
+      "\x02" /* ALPN length byte */
+      "h2"
+      "\x03" /* APLN length byte (WRONG) */
+      "h3",
+      23,
+      "r:61|"
+    },
+    {
+      "five alpns (ignore dupes)", /* we only support four */
+      (const unsigned char *)"\x00\x00" /* 16-bit prio */
+      "\x04name\x04some\x00" /* RNAME */
+      "\x00\x01" /* RR (1 == ALPN) */
+      "\x00\x0f" /* data size */
+      "\x02" /* ALPN length byte */
+      "h2"
+      "\x02" /* ALPN length byte */
+      "h2"
+      "\x02" /* ALPN length byte */
+      "h2"
+      "\x02" /* ALPN length byte */
+      "h2"
+      "\x02" /* APLN length byte */
+      "h3",
+      32,
+      "r:0|p:0|name.some.|alpn:10|alpn:20|"
+    },
+    {
+      "rname only",
+      (const unsigned char *)"\x00\x00" /* 16-bit prio */
+      "\x04name\x04some\x00", /* RNAME */
+      13,
+      "r:0|p:0|name.some.|"
+    },
+    {
+      "rname with low ascii byte",
+      (const unsigned char *)"\x00\x00" /* 16-bit prio */
+      "\x04name\x04som\x03\x00", /* RNAME */
+      13,
+      "r:8|"
+    },
+    {
+      "rname with null byte",
+      (const unsigned char *)"\x00\x00" /* 16-bit prio */
+      "\x04sa\x00e\x04some\x00", /* RNAME */
+      13,
+      "r:27|"
+    },
+    {
+      "rname only (missing byte)",
+      (const unsigned char *)"\x00\x00" /* 16-bit prio */
+      "\x04name\x05some\x00", /* RNAME */
+      /* it lacks a byte */
+      13,
+      "r:27|"
+    },
+    {
+      "unrecognized alpn",
+      (const unsigned char *)"\x00\x00" /* 16-bit prio */
+      "\x04name\x04some\x00" /* RNAME */
+      "\x00\x01" /* RR (1 == ALPN) */
+      "\x00\x06" /* data size */
+      "\x02" /* ALPN length byte */
+      "h8" /* unrecognized */
+      "\x02" /* APLN length byte */
+      "h1",
+      23,
+      "r:0|p:0|name.some.|alpn:8|"
+    },
+    {
+      "alnt + no-default-alpn",
+      (const unsigned char *)"\x00\x00" /* 16-bit prio */
+      "\x04name\x04some\x00" /* RNAME */
+      "\x00\x01" /* RR (1 == ALPN) */
+      "\x00\x03" /* data size */
+      "\x02" /* ALPN length byte */
+      "h2"
+      "\x00\x02" /* RR (2 == NO DEFALT ALPN) */
+      "\x00\x00", /* must be zero */
+      24,
+      "r:0|p:0|name.some.|alpn:10|no-def-alpn|"
+    },
+    {
+      "alnt + no-default-alpn with size",
+      (const unsigned char *)"\x00\x00" /* 16-bit prio */
+      "\x04name\x04some\x00" /* RNAME */
+      "\x00\x01" /* RR (1 == ALPN) */
+      "\x00\x03" /* data size */
+      "\x02" /* ALPN length byte */
+      "h2"
+      "\x00\x02" /* RR (2 == NO DEFALT ALPN) */
+      "\x00\x01" /* must be zero */
+      "\xff",
+      25,
+      "r:43|"
+    },
+    {
+      "alnt + no-default-alpn with size too short package",
+      (const unsigned char *)"\x00\x00" /* 16-bit prio */
+      "\x04name\x04some\x00" /* RNAME */
+      "\x00\x01" /* RR (1 == ALPN) */
+      "\x00\x03" /* data size */
+      "\x02" /* ALPN length byte */
+      "h2"
+      "\x00\x02" /* RR (2 == NO DEFALT ALPN) */
+      "\x00\x01", /* must be zero */
+      /* missing last byte in the packet */
+      24,
+      "r:8|"
+    },
+    {
+      "rname + blank alpn field",
+      (const unsigned char *)"\x11\x11" /* 16-bit prio */
+      "\x04name\x04some\x00" /* RNAME */
+      "\x00\x01" /* RR (1 == ALPN) */
+      "\x00\x00", /* data size, strictly speaking this is illegal:
+                     "one or more alpn-ids" */
+      17,
+      "r:0|p:4369|name.some.|"
+    },
+    {
+      "no rname + blank alpn",
+      (const unsigned char *)"\x00\x11" /* 16-bit prio */
+      "\x00" /* no RNAME */
+      "\x00\x01" /* RR (1 == ALPN) */
+      "\x00\x00", /* data size */
+      7,
+      "r:0|p:17|.|"
+    },
+    {
+      "unsupported field",
+      (const unsigned char *)"\xff\xff" /* 16-bit prio */
+      "\x00" /* no RNAME */
+      "\x00\x07" /* RR (7 == not a supported data) */
+      "\x00\x02" /* data size */
+      "FF", /* unknown to curl */
+      9,
+      "r:0|p:65535|.|"
+    },
+    {
+      "unsupported field (wrong size)",
+      (const unsigned char *)"\xff\xff" /* 16-bit prio */
+      "\x00" /* no RNAME */
+      "\x00\x07" /* RR (7 == not a supported data) */
+      "\x00\x02" /* data size */
+      "F", /* unknown to curl */
+      8,
+      "r:8|"
+    },
+    {
+      "port number",
+      (const unsigned char *)"\x00\x10" /* 16-bit prio */
+      "\x00" /* no RNAME */
+      "\x00\x01" /* RR (1 == ALPN) */
+      "\x00\x03" /* data size */
+      "\x02" /* ALPN length byte */
+      "h2"
+      "\x00\x03" /* RR (3 == PORT) */
+      "\x00\x02" /* data size */
+      "\x12\x34", /* port number */
+      16,
+      "r:0|p:16|.|alpn:10|port:4660|"
+    },
+    {
+      "port number with wrong size (3 bytes)",
+      (const unsigned char *)"\x00\x10" /* 16-bit prio */
+      "\x00" /* no RNAME */
+      "\x00\x01" /* RR (1 == ALPN) */
+      "\x00\x03" /* data size */
+      "\x02" /* ALPN length byte */
+      "h2"
+      "\x00\x03" /* RR (3 == PORT) */
+      "\x00\x03" /* data size */
+      "\x12\x34\x00", /* 24 bit port number! */
+      17,
+      "r:43|"
+    },
+    {
+      "port number with wrong size (1 byte)",
+      (const unsigned char *)"\x00\x10" /* 16-bit prio */
+      "\x00" /* no RNAME */
+      "\x00\x01" /* RR (1 == ALPN) */
+      "\x00\x03" /* data size */
+      "\x02" /* ALPN length byte */
+      "h2"
+      "\x00\x03" /* RR (3 == PORT) */
+      "\x00\x01" /* data size */
+      "\x12", /* 8 bit port number! */
+      15,
+      "r:43|"
+    },
+    {
+      "alpn + two ipv4 addreses",
+      (const unsigned char *)"\x00\x10" /* 16-bit prio */
+      "\x00" /* no RNAME */
+      "\x00\x01" /* RR (1 == ALPN) */
+      "\x00\x03" /* data size */
+      "\x02" /* ALPN length byte */
+      "h2"
+      "\x00\x04" /* RR (4 == Ipv4hints) */
+      "\x00\x08" /* data size */
+      "\xc0\xa8\x00\x01"  /* 32 bits */
+      "\xc0\xa8\x00\x02", /* 32 bits */
+      22,
+      "r:0|p:16|.|alpn:10|ipv4:192.168.0.1|ipv4:192.168.0.2|"
+    },
+    {
+      "alpn + two ipv4 addreses in wrong order",
+      (const unsigned char *)"\x00\x10" /* 16-bit prio */
+      "\x00" /* no RNAME */
+      "\x00\x04" /* RR (4 == Ipv4hints) */
+      "\x00\x08" /* data size */
+      "\xc0\xa8\x00\x01"  /* 32 bits */
+      "\xc0\xa8\x00\x02" /* 32 bits */
+      "\x00\x01" /* RR (1 == ALPN) */
+      "\x00\x03" /* data size */
+      "\x02" /* ALPN length byte */
+      "h2",
+      22,
+      "r:8|"
+    },
+    {
+      "alpn + ipv4 address with wrong size",
+      (const unsigned char *)"\x00\x10" /* 16-bit prio */
+      "\x00" /* no RNAME */
+      "\x00\x01" /* RR (1 == ALPN) */
+      "\x00\x03" /* data size */
+      "\x02" /* ALPN length byte */
+      "h2"
+      "\x00\x04" /* RR (4 == Ipv4hints) */
+      "\x00\x05" /* data size */
+      "\xc0\xa8\x00\x01\xff",  /* 32 + 8 bits */
+      19,
+      "r:43|"
+    },
+    {
+      "alpn + one ipv6 address",
+      (const unsigned char *)"\x00\x10" /* 16-bit prio */
+      "\x00" /* no RNAME */
+      "\x00\x01" /* RR (1 == ALPN) */
+      "\x00\x03" /* data size */
+      "\x02" /* ALPN length byte */
+      "h2"
+      "\x00\x06" /* RR (6 == Ipv6hints) */
+      "\x00\x10" /* data size */
+      "\xfe\x80\xda\xbb\xc1\xff\xfe\xa3\x8a\x22\x12\x34\x56\x78\x91\x23",
+      30,
+      "r:0|p:16|.|alpn:10|ipv6:fe80:dabb:c1ff:fea3:8a22:1234:5678:9123|"
+    },
+    {
+      "alpn + one ipv6 address with wrong size",
+      (const unsigned char *)"\x00\x10" /* 16-bit prio */
+      "\x00" /* no RNAME */
+      "\x00\x01" /* RR (1 == ALPN) */
+      "\x00\x03" /* data size */
+      "\x02" /* ALPN length byte */
+      "h2"
+      "\x00\x06" /* RR (6 == Ipv6hints) */
+      "\x00\x11" /* data size */
+      "\xfe\x80\xda\xbb\xc1\xff\xfe\xa3\x8a\x22\x12\x34\x56\x78\x91\x23\x45",
+      31,
+      "r:43|"
+    },
+    {
+      "alpn + two ipv6 addresses",
+      (const unsigned char *)"\x00\x10" /* 16-bit prio */
+      "\x00" /* no RNAME */
+      "\x00\x01" /* RR (1 == ALPN) */
+      "\x00\x03" /* data size */
+      "\x02" /* ALPN length byte */
+      "h2"
+      "\x00\x06" /* RR (6 == Ipv6hints) */
+      "\x00\x20" /* data size */
+      "\xfe\x80\xda\xbb\xc1\xff\xfe\xa3\x8a\x22\x12\x34\x56\x78\x91\x23"
+      "\xee\x80\xda\xbb\xc1\xff\xfe\xa3\x8a\x22\x12\x34\x56\x78\x91\x25",
+      46,
+      "r:0|p:16|.|alpn:10|ipv6:fe80:dabb:c1ff:fea3:8a22:1234:5678:9123|"
+      "ipv6:ee80:dabb:c1ff:fea3:8a22:1234:5678:9125|"
+    },
+    {
+      "alpn + ech",
+      (const unsigned char *)"\x00\x10" /* 16-bit prio */
+      "\x00" /* no RNAME */
+      "\x00\x01" /* RR (1 == ALPN) */
+      "\x00\x03" /* data size */
+      "\x02" /* ALPN length byte */
+      "h2"
+      "\x00\x05" /* RR (5 == ECH) */
+      "\x00\x10" /* data size */
+      "\xfe\x80\xda\xbb\xc1\xff\xfe\xa3\x8a\x22\x12\x34\x56\x78\x91\x23",
+      30,
+      "r:0|p:16|.|alpn:10|ech:fe80dabbc1fffea38a22123456789123|"
+    },
+    {
+      "fully packed",
+      (const unsigned char *)"\xa0\x0b" /* 16-bit prio */
+      "\x00" /* no RNAME */
+      "\x00\x00" /* RR (0 == MANDATORY) */
+      "\x00\x00" /* data size */
+      "\x00\x01" /* RR (1 == ALPN) */
+      "\x00\x06" /* data size */
+      "\x02" /* ALPN length byte */
+      "h2"
+      "\x02" /* ALPN length byte */
+      "h1"
+      "\x00\x02" /* RR (2 == NO DEFALT ALPN) */
+      "\x00\x00" /* must be zero */
+      "\x00\x03" /* RR (3 == PORT) */
+      "\x00\x02" /* data size */
+      "\xbc\x71" /* port number */
+      "\x00\x04" /* RR (4 == Ipv4hints) */
+      "\x00\x08" /* data size */
+      "\xc0\xa8\x00\x01" /* 32 bits */
+      "\xc0\xa8\x00\x02" /* 32 bits */
+      "\x00\x05" /* RR (5 == ECH) */
+      "\x00\x10" /* data size */
+      "\xfe\x80\xda\xbb\xc1\xff\x7e\xb3\x8a\x22\x12\x34\x56\x78\x91\x23"
+      "\x00\x06" /* RR (6 == Ipv6hints) */
+      "\x00\x20" /* data size */
+      "\xfe\x80\xda\xbb\xc1\xff\xfe\xa3\x8a\x22\x12\x34\x56\x78\x91\x23"
+      "\xee\x80\xda\xbb\xc1\xff\xfe\xa3\x8a\x22\x12\x34\x56\x78\x91\x25"
+      "\x01\x07" /* RR (263 == not supported) */
+      "\x00\x04" /* data size */
+      "FFAA", /* unknown to the world */
+      103,
+      "r:0|p:40971|.|alpn:10|alpn:8|no-def-alpn|port:48241|"
+      "ipv4:192.168.0.1|ipv4:192.168.0.2|"
+      "ech:fe80dabbc1ff7eb38a22123456789123|"
+      "ipv6:fe80:dabb:c1ff:fea3:8a22:1234:5678:9123|"
+      "ipv6:ee80:dabb:c1ff:fea3:8a22:1234:5678:9125|"
+    }
+  };
+
+  CURLcode result = CURLE_OUT_OF_MEMORY;
+  CURL *easy;
+
+  easy = curl_easy_init();
+  /* so that we get the log output: */
+  curl_easy_setopt(easy, CURLOPT_VERBOSE, 1L);
+  if(easy) {
+    unsigned int i;
+
+    for(i = 0; i < CURL_ARRAYSIZE(t); i++) {
+      struct Curl_https_rrinfo *hrr;
+
+      printf("test %i: %s\n", i, t[i].name);
+
+      result = doh_resp_decode_httpsrr(easy, t[i].dns, t[i].len, &hrr);
+
+      /* create an output */
+      rrresults(hrr, result);
+
+      /* is the output the expected? */
+      if(strcmp(rrbuffer, t[i].expect)) {
+        fprintf(stderr, "Test %s (%i) failed\n"
+                "Expected: %s\n"
+                "Received: %s\n", t[i].name, i, t[i].expect, rrbuffer);
+        unitfail++;
+      }
+
+      /* free the generated struct */
+      if(hrr) {
+        Curl_httpsrr_cleanup(hrr);
+        curl_free(hrr);
+      }
+    }
+    curl_easy_cleanup(easy);
+  }
+}
+UNITTEST_STOP
+
+#else /* CURL_DISABLE_DOH or not HTTPSRR enabled */
+
+UNITTEST_START
+/* nothing to do, just succeed */
+UNITTEST_STOP
+
+#endif