From a7161af476d7300ed79ea3fcc61a4fa2288ccbad Mon Sep 17 00:00:00 2001 From: Willem Toorop Date: Thu, 16 Jul 2026 09:45:51 +0200 Subject: [PATCH 1/3] The oots SvcParam (draft-johani-dnsop-svcb-oots-00) --- include/zone.h | 2 + src/generic/svcb.h | 75 +++++++++++++++++++++++++++++++++++- tests/svcb.c | 94 ++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 161 insertions(+), 10 deletions(-) diff --git a/include/zone.h b/include/zone.h index 4f6a092..7aaee75 100644 --- a/include/zone.h +++ b/include/zone.h @@ -263,6 +263,8 @@ extern "C" { #define ZONE_SVC_PARAM_KEY_DOCPATH (10u) /** PvD configuration is available at the well-known path @draft{ietf, intarea-proxy-config} */ #define ZONE_SVC_PARAM_KEY_PVD (11u) +/** Per-transport operator confidence in serving the nameserver's query load over that transport, as a percentage @draft{johani, dnsop-svcb-oots} */ +#define ZONE_SVC_PARAM_KEY_OOTS (12u) /** Reserved ("invalid key") @rfc{9460} */ #define ZONE_SVC_PARAM_KEY_INVALID_KEY (65535u) /** @} */ diff --git a/src/generic/svcb.h b/src/generic/svcb.h index b7203c7..fa7c49a 100644 --- a/src/generic/svcb.h +++ b/src/generic/svcb.h @@ -432,6 +432,73 @@ static int32_t parse_docpath( return parse_alpn(parser, type, field, key, param, rdata, token); } +nonnull_all +static int32_t parse_oots( + parser_t *parser, + const type_info_t *type, + const rdata_info_t *field, + uint16_t key, + const svc_param_info_t *param, + rdata_t *rdata, + const token_t *token) +{ + const char *t = token->data, *te = token->data + token->length; + const uint8_t *rdata_start = rdata->octets; + + (void)field; + (void)key; + (void)param; + + while (t < te) { + const char *transport = t; + const uint8_t *transport_out = rdata->octets; + const char *colon = memchr(t, ':', te - t); + + if (!colon) + SYNTAX_ERROR(parser, "No colon found in oots DNS transport in %s", NAME(type)); + + if(colon == t) + SYNTAX_ERROR(parser, "DNS transport name must have at least 1 character in %s", NAME(type)); + + if (rdata->octets + (colon - t) + 2 > rdata->limit) + SYNTAX_ERROR(parser, "No space for oots DNS transport in %s", NAME(type)); + + *rdata->octets++ = (colon - t); + memcpy(rdata->octets, transport, colon - t); + rdata->octets += (colon - t); + + t = colon + 1; + uint64_t number = 0; + for (;; t++) { + const uint64_t digit = (uint8_t)*t - '0'; + if (digit > 9) + break; + number = number * 10 + digit; + } + if(t == colon +1) + SYNTAX_ERROR(parser, "Oots percentage missing in %s", NAME(type)); + if (number > 100) + SYNTAX_ERROR(parser, "Invalid oots percentage in %s", NAME(type)); + + *rdata->octets++ = (uint8_t)number; + + const uint8_t *g; + for (g = rdata_start; g < transport_out; g += *g + 2) { + if (memcmp(g, transport_out, *transport_out + 1) == 0) + SEMANTIC_ERROR(parser, "Duplicate DNS transport in oots in %s", NAME(type)); + } + if (*t != ',') + break; + else + t++; + } + + if (t != te || rdata->octets > rdata->limit) + SYNTAX_ERROR(parser, "Invalid oots in %s", NAME(type)); + return 0; +} + + nonnull_all static int32_t parse_mandatory_lax( parser_t *parser, @@ -498,6 +565,8 @@ static const svc_param_info_t svc_params[] = { SVC_PARAM("docpath", 10u, OPTIONAL_VALUE, parse_docpath, parse_docpath), // draft-ietf-intarea-proxy-config-13 section 2.1: SVC_PARAM("pvd", 11u, NO_VALUE, parse_unknown, parse_unknown), + // draft-johani-dnsop-svcb-oots + SVC_PARAM("oots", 12u, MANDATORY_VALUE, parse_oots, parse_oots), }; static const svc_param_info_t unknown_svc_param = @@ -559,14 +628,16 @@ static really_inline size_t scan_svc_param( return (void)(*param = &svc_params[(*key = ZONE_SVC_PARAM_KEY_IPV6HINT)]), 8; else if (memcmp(data, "dohpath", 7) == 0) return (void)(*param = &svc_params[(*key = ZONE_SVC_PARAM_KEY_DOHPATH)]), 7; - else if (memcmp(data, "docpath", 7) == 0) - return (void)(*param = &svc_params[(*key = ZONE_SVC_PARAM_KEY_DOCPATH)]), 7; else if (memcmp(data, "ohttp", 5) == 0) return (void)(*param = &svc_params[(*key = ZONE_SVC_PARAM_KEY_OHTTP)]), 5; else if (memcmp(data, "tls-supported-groups", 20) == 0) return (void)(*param = &svc_params[(*key = ZONE_SVC_PARAM_KEY_TLS_SUPPORTED_GROUPS)]), 20; + else if (memcmp(data, "docpath", 7) == 0) + return (void)(*param = &svc_params[(*key = ZONE_SVC_PARAM_KEY_DOCPATH)]), 7; else if (memcmp(data, "pvd", 3) == 0) return (void)(*param = &svc_params[(*key = ZONE_SVC_PARAM_KEY_PVD)]), 3; + else if (memcmp(data, "oots", 4) == 0) + return (void)(*param = &svc_params[(*key = ZONE_SVC_PARAM_KEY_OOTS)]), 4; else if (memcmp(data, "key", 3) == 0) return scan_unknown_svc_param_key(data, key, param); else diff --git a/tests/svcb.c b/tests/svcb.c index 442144e..109b709 100644 --- a/tests/svcb.c +++ b/tests/svcb.c @@ -812,7 +812,7 @@ static const rdata_t docpath_s4_rdata = // From the draft-ietf-intarea-proxy-config-13 Section-2.1 Discovery via HTTPS/SVCB Records: static const char pvd_s1_text[] = - PAD("pvd-s1. 3600 IN HTTPS 1 . alpn=\"h3,h2\" pvd"); + PAD("pvd-s1 3600 IN HTTPS 1 . alpn=\"h3,h2\" pvd"); static const rdata_t pvd_s1_rdata = RDATA( 0x00, 0x01, // priority @@ -822,7 +822,62 @@ static const rdata_t pvd_s1_rdata = ); static const char pvd_f1_text[] = - PAD("pvd-f1. 3600 IN HTTPS 1 . alpn=\"h3,h2\" pvd=\"something\""); + PAD("pvd-f1 3600 IN HTTPS 1 . alpn=\"h3,h2\" pvd=\"something\""); + +static const char oots_s1_text[] = + PAD("oots-s1 300 IN SVCB 1 . oots=\"do53:100,dot:10\""); + +static const rdata_t oots_s1_rdata = + RDATA( + 0x00, 0x01, // priority + 0x00, // target + 0x00, 0x0c, 0x00, 0x0b, 0x04, 'd', 'o', '5', '3', 100, 0x03, 'd', 'o', 't', 10 // outs="do53:100,dot:10" + ); + +static const char oots_s2_text[] = + PAD("oots-s2 300 IN SVCB 1 . oots=\"do53:100,dot:5,doq:5\""); + +static const rdata_t oots_s2_rdata = + RDATA( + 0x00, 0x01, // priority + 0x00, // target + 0x00, 0x0c, 0x00, 0x10, 0x04, 'd', 'o', '5', '3', 100, 0x03, 'd', 'o', 't', 5, 0x03, 'd', 'o', 'q', 5 // outs="do53:100,dot:5,doq:5" + ); + +static const char oots_s3_text[] = + PAD("oots-s3 300 IN SVCB 1 . oots=\"do53:100,dot:25,doh:10,doq:10\""); + +static const rdata_t oots_s3_rdata = + RDATA( + 0x00, 0x01, // priority + 0x00, // target + 0x00, 0x0c, 0x00, 0x15, 0x04, 'd', 'o', '5', '3', 100, 0x03, 'd', 'o', 't', 25, 0x03, 'd', 'o', 'h', 10, 0x03, 'd', 'o', 'q', 10// outs="do53:100,dot:25,doh:10,doq:10" + ); + +// Missing values +static const char oots_f1_text[] = + PAD("oots-s1 300 IN SVCB 1 . oots=\"\""); + +// Missing percentage +static const char oots_f2_text[] = + PAD("oots-s2 300 IN SVCB 1 . oots=\"do53:100,dot\""); + +// Missing transport +static const char oots_f3_text[] = + PAD("oots-s3 300 IN SVCB 1 . oots=\"do53:100,:25\""); + +// Invalid percentage +static const char oots_f4_text[] = + PAD("oots-s4 300 IN SVCB 1 . oots=\"do53:100,dot:101\""); + +// Invalid percentage +static const char oots_f5_text[] = + PAD("oots-s5 300 IN SVCB 1 . oots=\"do53:100,dot:25%\""); + +// Duplicate transport +static const char oots_f6_text[] = + PAD("oots-s6 300 IN SVCB 1 . oots=\"do53:100,dot:25,dot:10\""); + // FIXME: make a test that verifies correct behavior for no-default-alpn="some value" @@ -924,7 +979,15 @@ static const test_t tests[] = { { false, ZONE_TYPE_SVCB, 0, docpath_s3_text, &docpath_s3_rdata }, { false, ZONE_TYPE_SVCB, 0, docpath_s4_text, &docpath_s4_rdata }, { false, ZONE_TYPE_HTTPS, 0, pvd_s1_text, &pvd_s1_rdata }, - { false, ZONE_TYPE_HTTPS, ZONE_SEMANTIC_ERROR, pvd_f1_text, NULL } + { false, ZONE_TYPE_HTTPS, ZONE_SEMANTIC_ERROR, pvd_f1_text, NULL }, + { false, ZONE_TYPE_SVCB, 0, oots_s1_text, &oots_s1_rdata }, + { false, ZONE_TYPE_SVCB, 0, oots_s2_text, &oots_s2_rdata }, + { false, ZONE_TYPE_SVCB, 0, oots_s3_text, &oots_s3_rdata }, + { false, ZONE_TYPE_SVCB, ZONE_SYNTAX_ERROR, oots_f2_text, NULL }, + { false, ZONE_TYPE_SVCB, ZONE_SYNTAX_ERROR, oots_f3_text, NULL }, + { false, ZONE_TYPE_SVCB, ZONE_SYNTAX_ERROR, oots_f4_text, NULL }, + { false, ZONE_TYPE_SVCB, ZONE_SYNTAX_ERROR, oots_f5_text, NULL }, + { false, ZONE_TYPE_SVCB, ZONE_SEMANTIC_ERROR, oots_f6_text, NULL } }; static int32_t add_rr( @@ -942,15 +1005,30 @@ static int32_t add_rr( (void)owner; (void)class; (void)ttl; - if (type != test->type) + if (type != test->type) { + fprintf(stderr, "type mismatch\n"); return ZONE_SYNTAX_ERROR; + } if (test->code != ZONE_SUCCESS) return ZONE_SUCCESS; if (rdlength != test->rdata->length || !test->rdata->octets) - return ZONE_SYNTAX_ERROR; - if (memcmp(rdata, test->rdata->octets, rdlength) != 0) - return ZONE_SYNTAX_ERROR; - return ZONE_SUCCESS; + fprintf( stderr, "rdata length did not match %hu != %hu" + , rdlength, test->rdata->length); + else if (memcmp(rdata, test->rdata->octets, rdlength) != 0) + fprintf(stderr, "rdata bytes did not match"); + else + return ZONE_SUCCESS; + + size_t i; + for (i = 0; i < rdlength; i++) { + if (i % 16 == 0) + fprintf(stderr, "\n"); + else if (i % 8 == 0) + fprintf(stderr, " "); + fprintf(stderr, " %.2x", rdata[i]); + } + fprintf(stderr, "\n"); + return ZONE_SYNTAX_ERROR; } static uint8_t origin[] = From 2efe2f133d73d7a35373b66a1f60177454480eeb Mon Sep 17 00:00:00 2001 From: Willem Toorop Date: Thu, 16 Jul 2026 10:10:10 +0200 Subject: [PATCH 2/3] Improvements from github tests --- src/generic/svcb.h | 9 ++++++--- tests/svcb.c | 30 +++++++++++++----------------- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/generic/svcb.h b/src/generic/svcb.h index fa7c49a..e4daf9b 100644 --- a/src/generic/svcb.h +++ b/src/generic/svcb.h @@ -452,7 +452,7 @@ static int32_t parse_oots( while (t < te) { const char *transport = t; const uint8_t *transport_out = rdata->octets; - const char *colon = memchr(t, ':', te - t); + const char *colon = memchr(t, ':', (size_t)(te - t)); if (!colon) SYNTAX_ERROR(parser, "No colon found in oots DNS transport in %s", NAME(type)); @@ -463,8 +463,11 @@ static int32_t parse_oots( if (rdata->octets + (colon - t) + 2 > rdata->limit) SYNTAX_ERROR(parser, "No space for oots DNS transport in %s", NAME(type)); - *rdata->octets++ = (colon - t); - memcpy(rdata->octets, transport, colon - t); + if (colon - t > 255) + SYNTAX_ERROR(parser, "DNS transport name too large in %s", NAME(type)); + + *rdata->octets++ = (uint8_t)(colon - t); + memcpy(rdata->octets, transport, (size_t)(colon - t)); rdata->octets += (colon - t); t = colon + 1; diff --git a/tests/svcb.c b/tests/svcb.c index 109b709..3d2a8c3 100644 --- a/tests/svcb.c +++ b/tests/svcb.c @@ -854,29 +854,25 @@ static const rdata_t oots_s3_rdata = 0x00, 0x0c, 0x00, 0x15, 0x04, 'd', 'o', '5', '3', 100, 0x03, 'd', 'o', 't', 25, 0x03, 'd', 'o', 'h', 10, 0x03, 'd', 'o', 'q', 10// outs="do53:100,dot:25,doh:10,doq:10" ); -// Missing values +// Missing percentage static const char oots_f1_text[] = - PAD("oots-s1 300 IN SVCB 1 . oots=\"\""); + PAD("oots-f1 300 IN SVCB 1 . oots=\"do53:100,dot\""); -// Missing percentage +// Missing transport static const char oots_f2_text[] = - PAD("oots-s2 300 IN SVCB 1 . oots=\"do53:100,dot\""); + PAD("oots-f2 300 IN SVCB 1 . oots=\"do53:100,:25\""); -// Missing transport +// Invalid percentage static const char oots_f3_text[] = - PAD("oots-s3 300 IN SVCB 1 . oots=\"do53:100,:25\""); + PAD("oots-f3 300 IN SVCB 1 . oots=\"do53:100,dot:101\""); // Invalid percentage static const char oots_f4_text[] = - PAD("oots-s4 300 IN SVCB 1 . oots=\"do53:100,dot:101\""); - -// Invalid percentage -static const char oots_f5_text[] = - PAD("oots-s5 300 IN SVCB 1 . oots=\"do53:100,dot:25%\""); + PAD("oots-f4 300 IN SVCB 1 . oots=\"do53:100,dot:25%\""); // Duplicate transport -static const char oots_f6_text[] = - PAD("oots-s6 300 IN SVCB 1 . oots=\"do53:100,dot:25,dot:10\""); +static const char oots_f5_text[] = + PAD("oots-f5 300 IN SVCB 1 . oots=\"do53:100,dot:25,dot:10\""); // FIXME: make a test that verifies correct behavior for no-default-alpn="some value" @@ -983,11 +979,11 @@ static const test_t tests[] = { { false, ZONE_TYPE_SVCB, 0, oots_s1_text, &oots_s1_rdata }, { false, ZONE_TYPE_SVCB, 0, oots_s2_text, &oots_s2_rdata }, { false, ZONE_TYPE_SVCB, 0, oots_s3_text, &oots_s3_rdata }, + { false, ZONE_TYPE_SVCB, ZONE_SYNTAX_ERROR, oots_f1_text, NULL }, { false, ZONE_TYPE_SVCB, ZONE_SYNTAX_ERROR, oots_f2_text, NULL }, { false, ZONE_TYPE_SVCB, ZONE_SYNTAX_ERROR, oots_f3_text, NULL }, { false, ZONE_TYPE_SVCB, ZONE_SYNTAX_ERROR, oots_f4_text, NULL }, - { false, ZONE_TYPE_SVCB, ZONE_SYNTAX_ERROR, oots_f5_text, NULL }, - { false, ZONE_TYPE_SVCB, ZONE_SEMANTIC_ERROR, oots_f6_text, NULL } + { false, ZONE_TYPE_SVCB, ZONE_SEMANTIC_ERROR, oots_f5_text, NULL } }; static int32_t add_rr( @@ -1012,8 +1008,8 @@ static int32_t add_rr( if (test->code != ZONE_SUCCESS) return ZONE_SUCCESS; if (rdlength != test->rdata->length || !test->rdata->octets) - fprintf( stderr, "rdata length did not match %hu != %hu" - , rdlength, test->rdata->length); + fprintf( stderr, "rdata length did not match %d != %d" + , (int)rdlength, (int)test->rdata->length); else if (memcmp(rdata, test->rdata->octets, rdlength) != 0) fprintf(stderr, "rdata bytes did not match"); else From 2c4225335598fcd8bba38fe3905391aa3dae7fac Mon Sep 17 00:00:00 2001 From: Willem Toorop Date: Thu, 16 Jul 2026 11:50:02 +0200 Subject: [PATCH 3/3] Apply suggestions from code review Co-authored-by: Wouter Wijngaards --- src/generic/svcb.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/generic/svcb.h b/src/generic/svcb.h index e4daf9b..0f94f52 100644 --- a/src/generic/svcb.h +++ b/src/generic/svcb.h @@ -486,8 +486,8 @@ static int32_t parse_oots( *rdata->octets++ = (uint8_t)number; const uint8_t *g; - for (g = rdata_start; g < transport_out; g += *g + 2) { - if (memcmp(g, transport_out, *transport_out + 1) == 0) + for (g = rdata_start; g < transport_out; g += ((size_t)(*g)) + 2) { + if (memcmp(g, transport_out, ((size_t)(*transport_out)) + 1) == 0) SEMANTIC_ERROR(parser, "Duplicate DNS transport in oots in %s", NAME(type)); } if (*t != ',')