From a973377cd7836313728195db3826acbc98cc0098 Mon Sep 17 00:00:00 2001 From: Stefan Eissing Date: Thu, 16 Apr 2026 13:18:38 +0200 Subject: [PATCH] dnsd: implement HTTPS-RR with alpn pref and no-default-alpn Support HTTPS records in dnsd that have ALPN preferences. Add pytest test_22_* to verify that HTTPS record ALPN preferencces take effect if the HTTPS resolve arrives in time. Fix HTTPS eyeballing use of timeouts for second attempt. Also, make an initial HTTP/1.1 attempt switch off HTTP/2. Closes #21329 --- docs/libcurl/libcurl-env-dbg.md | 5 + lib/cf-https-connect.c | 30 ++- tests/http/Makefile.am | 1 + tests/http/test_06_eyeballs.py | 6 +- tests/http/test_22_httpsrr.py | 134 +++++++++++ tests/http/testenv/dnsd.py | 8 +- tests/server/dnsd.c | 412 ++++++++++++++++++++++++-------- 7 files changed, 484 insertions(+), 112 deletions(-) create mode 100644 tests/http/test_22_httpsrr.py diff --git a/docs/libcurl/libcurl-env-dbg.md b/docs/libcurl/libcurl-env-dbg.md index 2471855bcc..9fa9c069d1 100644 --- a/docs/libcurl/libcurl-env-dbg.md +++ b/docs/libcurl/libcurl-env-dbg.md @@ -194,3 +194,8 @@ Make libcurl fail a resolve for IPv6 only. ## `CURL_QUICK_EXIT` Make `curl` use the quick exit option, even when built in debug mode. + +## `CURL_DBG_AWAIT_HTTPSRR` + +When happy eyeballing for https: wait for the HTTPS-RR resolve +answer to arrive before starting any connect attempt. diff --git a/lib/cf-https-connect.c b/lib/cf-https-connect.c index 00a5882370..5407dc73a6 100644 --- a/lib/cf-https-connect.c +++ b/lib/cf-https-connect.c @@ -378,16 +378,17 @@ static enum alpnid cf_hc_get_first_alpn(struct Curl_cfilter *cf, http_majors choices, enum alpnid not_this_one) { + /* When told to not try h2, we also do not try h1 and vice versa */ + bool allow_h1_or_h2 = (not_this_one != ALPN_h1) && + (not_this_one != ALPN_h2); if((ALPN_h3 != not_this_one) && (choices & CURL_HTTP_V3x) && cf_hc_may_h3(cf, data)) { return ALPN_h3; } - if((ALPN_h2 != not_this_one) && (choices & CURL_HTTP_V2x)) { + if(allow_h1_or_h2 && (choices & CURL_HTTP_V2x)) { return ALPN_h2; } - /* If we are trying h2 already, h1 is already used as fallback */ - if((ALPN_h1 != not_this_one) && (ALPN_h2 != not_this_one) && - (choices & CURL_HTTP_V1x)) { + if(allow_h1_or_h2 && (choices & CURL_HTTP_V1x)) { return ALPN_h1; } return ALPN_none; @@ -435,6 +436,16 @@ static CURLcode cf_hc_set_baller1(struct Curl_cfilter *cf, ctx->baller_count = 1; CURL_TRC_CF(data, cf, "1st attempt uses %s from %s", ctx->ballers[0].name, source); + + switch(alpn1) { + case ALPN_h1: + /* We really want h1, switch off h2 to make it disappear in ALPN */ + data->state.http_neg.wanted &= (uint8_t)~CURL_HTTP_V2x; + break; + default: + break; + } + return CURLE_OK; } @@ -486,8 +497,15 @@ static CURLcode cf_hc_connect(struct Curl_cfilter *cf, *done = FALSE; - if(!ctx->httpsrr_resolved) + if(!ctx->httpsrr_resolved) { ctx->httpsrr_resolved = Curl_conn_dns_resolved_https(data, cf->sockindex); +#ifdef DEBUGBUILD + if(!ctx->httpsrr_resolved && getenv("CURL_DBG_AWAIT_HTTPSRR")) { + CURL_TRC_CF(data, cf, "awaiting HTTPS-RR"); + return CURLE_OK; + } +#endif + } switch(ctx->state) { case CF_HC_RESOLV: @@ -773,6 +791,8 @@ static CURLcode cf_hc_create(struct Curl_cfilter **pcf, goto out; } ctx->def_transport = def_transport; + ctx->hard_eyeballs_timeout_ms = data->set.happy_eyeballs_timeout; + ctx->soft_eyeballs_timeout_ms = data->set.happy_eyeballs_timeout / 2; result = Curl_cf_create(&cf, &Curl_cft_http_connect, ctx); if(result) diff --git a/tests/http/Makefile.am b/tests/http/Makefile.am index 507bce7b50..3c24146e89 100644 --- a/tests/http/Makefile.am +++ b/tests/http/Makefile.am @@ -65,6 +65,7 @@ EXTRA_DIST = \ test_19_shutdown.py \ test_20_websockets.py \ test_21_resolve.py \ + test_22_httpsrr.py \ test_30_vsftpd.py \ test_31_vsftpds.py \ test_32_ftps_vsftpd.py \ diff --git a/tests/http/test_06_eyeballs.py b/tests/http/test_06_eyeballs.py index df547c8662..fb9df11d2f 100644 --- a/tests/http/test_06_eyeballs.py +++ b/tests/http/test_06_eyeballs.py @@ -164,11 +164,9 @@ class TestEyeballs: '--alt-svc', f'{asfile}', '--http3' ]) r.check_response(count=1, http_status=200) - # We expect the connection to be preferring HTTP/1.1 in the ALPN + # We expect the connection to use HTTP/1.1 assert r.total_connects == 1, f'{r.dump_logs()}' - re_m = re.compile(r'.* ALPN: curl offers http/1.1,h2') - lines = [line for line in r.trace_lines if re_m.match(line)] - assert len(lines), f'{r.dump_logs()}' + assert r.stats[0]['http_version'] == '1.1', f'{r}' @pytest.mark.skipif(condition=not Env.have_h3(), reason="h3 not supported") def test_06_22_as_ignore_h3h1(self, env: Env, httpd, configures_httpd, nghttpx): diff --git a/tests/http/test_22_httpsrr.py b/tests/http/test_22_httpsrr.py new file mode 100644 index 0000000000..07214c5b80 --- /dev/null +++ b/tests/http/test_22_httpsrr.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +#*************************************************************************** +# _ _ ____ _ +# Project ___| | | | _ \| | +# / __| | | | |_) | | +# | (__| |_| | _ <| |___ +# \___|\___/|_| \_\_____| +# +# Copyright (C) Daniel Stenberg, , 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 +# +########################################################################### +# +import logging +import os +from datetime import timedelta +from typing import Generator + +import pytest +from testenv import CurlClient, Env, LocalClient, Dnsd + +log = logging.getLogger(__name__) + + +@pytest.mark.skipif(condition=not Env.curl_is_debug(), reason="needs curl debug") +@pytest.mark.skipif(condition=not Env.curl_override_dns(), reason="no DNS override") +@pytest.mark.skipif(condition=not Env.curl_has_feature('HTTPSRR'), reason="no HTTPSRR support") +class TestResolve: + + @pytest.fixture(scope='class') + def dnsd(self, env: Env) -> Generator[Dnsd, None, None]: + dnsd = Dnsd(env=env) + assert dnsd.initial_start() + yield dnsd + dnsd.stop() + + # dnsd a HTTPS-RR that prefers HTTP/1.1. + def test_22_01_httpsrr_h1(self, env: Env, httpd, dnsd): + dnsd.set_answers(addr_a=['127.0.0.1'], + https=['10 . alpn=http/1.1']) + run_env = os.environ.copy() + run_env['CURL_DNS_SERVER'] = f'127.0.0.1:{dnsd.port}' + run_env['CURL_DBG_AWAIT_HTTPSRR'] = '1' + run_env['CURL_QUICK_EXIT'] = '1' + run_env['CURL_DEBUG'] = 'dns,https-connect' + curl = CurlClient(env=env, run_env=run_env, force_resolv=False) + url = f'https://{env.authority_for(env.domain1, "http/1.1")}/data.json' + r = curl.http_download(urls=[url], with_stats=True) + r.check_exit_code(0) + r.check_stats(count=1, http_status=200, exitcode=0) + assert r.stats[0]['http_version'] == '1.1', f'{r}' + + # dnsd a HTTPS-RR that prefers HTTP/2, this overrides the --http3 option. + @pytest.mark.skipif(condition=not Env.curl_override_dns(), reason="no DNS override") + @pytest.mark.skipif(condition=not Env.curl_has_feature('HTTPSRR'), reason="no HTTPSRR support") + @pytest.mark.skipif(condition=not Env.have_h3(), reason="missing HTTP/3 support") + def test_22_02_httpsrr_h3(self, env: Env, httpd, dnsd, nghttpx): + dnsd.set_answers(addr_a=['127.0.0.1'], + https=['10 . alpn=h2']) + run_env = os.environ.copy() + run_env['CURL_DNS_SERVER'] = f'127.0.0.1:{dnsd.port}' + run_env['CURL_DBG_AWAIT_HTTPSRR'] = '1' + run_env['CURL_QUICK_EXIT'] = '1' + run_env['CURL_DEBUG'] = 'dns,https-connect' + curl = CurlClient(env=env, run_env=run_env, force_resolv=False) + url = f'https://{env.authority_for(env.domain1, "http/1.1")}/data.json' + r = curl.http_download(urls=[url], with_stats=True, extra_args=[ + '--http3' + ]) + r.check_exit_code(0) + r.check_stats(count=1, http_status=200, exitcode=0) + assert r.stats[0]['http_version'] == '2', f'{r}' + + # dnsd a HTTPS-RR that prefers HTTP/3. + @pytest.mark.skipif(condition=not Env.have_h3(), reason="missing HTTP/3 support") + def test_22_03_httpsrr_h3(self, env: Env, httpd, dnsd, nghttpx): + dnsd.set_answers(addr_a=['127.0.0.1'], + https=['10 . alpn=h3,h2']) + run_env = os.environ.copy() + run_env['CURL_DNS_SERVER'] = f'127.0.0.1:{dnsd.port}' + run_env['CURL_DBG_AWAIT_HTTPSRR'] = '1' + run_env['CURL_QUICK_EXIT'] = '1' + run_env['CURL_DEBUG'] = 'dns,https-connect' + curl = CurlClient(env=env, run_env=run_env, force_resolv=False) + url = f'https://{env.authority_for(env.domain1, "http/1.1")}/data.json' + r = curl.http_download(urls=[url], with_stats=True) + r.check_exit_code(0) + r.check_stats(count=1, http_status=200, exitcode=0) + assert r.stats[0]['http_version'] == '3', f'{r}' + + # dnsd a HTTPS-RR that prefers HTTP/1.1 for another target, so ignored. + def test_22_04_httpsrr_wrong_target(self, env: Env, httpd, dnsd): + dnsd.set_answers(addr_a=['127.0.0.1'], + https=['10 another alpn=http/1.1']) + run_env = os.environ.copy() + run_env['CURL_DNS_SERVER'] = f'127.0.0.1:{dnsd.port}' + run_env['CURL_DBG_AWAIT_HTTPSRR'] = '1' + run_env['CURL_QUICK_EXIT'] = '1' + run_env['CURL_DEBUG'] = 'dns,https-connect' + curl = CurlClient(env=env, run_env=run_env, force_resolv=False) + url = f'https://{env.authority_for(env.domain1, "http/1.1")}/data.json' + r = curl.http_download(urls=[url], with_stats=True) + r.check_exit_code(0) + r.check_stats(count=1, http_status=200, exitcode=0) + assert r.stats[0]['http_version'] == '2', f'{r}' + + # dnsd a HTTPS-RR with no-default-alpn, ignored by curl for now + def test_22_05_httpsrr_no_default_alpn(self, env: Env, httpd, dnsd): + dnsd.set_answers(addr_a=['127.0.0.1'], + https=['10 . no-default-alpn alpn=http/1.1']) + run_env = os.environ.copy() + run_env['CURL_DNS_SERVER'] = f'127.0.0.1:{dnsd.port}' + run_env['CURL_DBG_AWAIT_HTTPSRR'] = '1' + run_env['CURL_QUICK_EXIT'] = '1' + run_env['CURL_DEBUG'] = 'dns,https-connect' + curl = CurlClient(env=env, run_env=run_env, force_resolv=False) + url = f'https://{env.authority_for(env.domain1, "http/1.1")}/data.json' + r = curl.http_download(urls=[url], with_stats=True) + r.check_exit_code(0) + r.check_stats(count=1, http_status=200, exitcode=0) + assert r.stats[0]['http_version'] == '2', f'{r}' diff --git a/tests/http/testenv/dnsd.py b/tests/http/testenv/dnsd.py index 830d02491a..b8ff097280 100644 --- a/tests/http/testenv/dnsd.py +++ b/tests/http/testenv/dnsd.py @@ -147,17 +147,23 @@ class Dnsd: def set_answers(self, addr_a: Optional[List[str]] = None, addr_aaaa: Optional[List[str]] = None, + https: Optional[List[str]] = None, delay_a_ms: int = 0, - delay_aaaa_ms: int = 0): + delay_aaaa_ms: int = 0, + delay_https_ms: int = 0): conf = [] if addr_a: conf.extend([f'A: {addr}' for addr in addr_a]) if addr_aaaa: conf.extend([f'AAAA: {addr}' for addr in addr_aaaa]) + if https: + conf.extend([f'HTTPS: {hline}' for hline in https]) if delay_a_ms: conf.append(f'Delay-A: {delay_a_ms}') if delay_aaaa_ms: conf.append(f'Delay-AAAA: {delay_aaaa_ms}') + if delay_https_ms: + conf.append(f'Delay-HTTPS: {delay_https_ms}') conf.append('\n') with open(self._conf_file, 'w') as fd: fd.write("\n".join(conf)) diff --git a/tests/server/dnsd.c b/tests/server/dnsd.c index 485be0d573..9da4eefce8 100644 --- a/tests/server/dnsd.c +++ b/tests/server/dnsd.c @@ -34,21 +34,67 @@ static int dnsd_wroteportfile = 0; #error building dnsd on AMIGA os is unsupported #endif -static unsigned short get16bit(const unsigned char **pkt, size_t *size) +static uint16_t get16bit(const unsigned char **pkt, size_t *size) { const unsigned char *p = *pkt; (*pkt) += 2; *size -= 2; - return (unsigned short)((p[0] << 8) | p[1]); + return (uint16_t)((p[0] << 8) | p[1]); } -static char name[256]; +#define BLOB_MAX_LEN 4096 -static int qname(const unsigned char **pkt, size_t *size) +struct blob { + uint8_t data[BLOB_MAX_LEN]; + size_t dlen; +}; + +static void blob_reset(struct blob *b) +{ + memset(b->data, 0, sizeof(b->data)); + b->dlen = 0; +} + +static int blob_add(struct blob *b, uint8_t n) +{ + if(b->dlen + 1 > BLOB_MAX_LEN) + return 1; + b->data[b->dlen] = n; + b->dlen += 1; + return 0; +} + +static int blob_addn(struct blob *b, const uint8_t *data, size_t n) +{ + if(b->dlen + n > BLOB_MAX_LEN) + return 1; + memcpy(&b->data[b->dlen], data, n); + b->dlen += n; + return 0; +} + +static int blob_add_uint16(struct blob *b, uint16_t n) +{ + if(b->dlen + 2 > BLOB_MAX_LEN) + return 1; + b->data[b->dlen] = (n >> 8) & 0xffU; + b->data[b->dlen + 1] = n & 0xffU; + b->dlen += 2; + return 0; +} + +static int blob_addchars(struct blob *b, const char *data, size_t n) +{ + return blob_addn(b, (const uint8_t *)data, n); +} + +static int qname2str(const unsigned char **pkt, size_t *size, + char *name, size_t name_max) { unsigned char length; - int o = 0; + size_t o = 0; const unsigned char *p = *pkt; + do { int i; length = *p++; @@ -57,21 +103,71 @@ static int qname(const unsigned char **pkt, size_t *size) return 1; if(length && o) name[o++] = '.'; + if(o + length >= name_max - 1) + return 1; for(i = 0; i < length; i++) { name[o++] = *p++; } } while(length); *size -= (p - *pkt); *pkt = p; - name[o++] = '\0'; + name[o] = '\0'; return 0; } +static int blob_add_qname_part(struct blob *b, struct Curl_str *str) +{ + size_t dot, skip; + + for(dot = 0; dot < str->len; ++dot) { + if(str->str[dot] == '.') + break; + } + if(!dot || (dot > 63)) /* RFC 1035, ch. 3.1 */ + return 1; + if(blob_add(b, (uint8_t)dot) || + (dot && blob_addchars(b, str->str, dot))) + return 1; + + skip = dot; + if(dot < str->len) + skip += 1; + str->str += skip; + str->len -= skip; + return 0; +} + +static int blob_add_qname(struct blob *b, const struct Curl_str *str) +{ + struct Curl_str s = *str; + + while(s.len) { + if(s.str[0] == '.') { + if(s.len != 1) + return 1; + break; + } + else { + if(blob_add_qname_part(b, &s)) + return 1; + } + } + return blob_add(b, 0); +} + #define QTYPE_A 1 #define QTYPE_AAAA 28 #define QTYPE_HTTPS 0x41 -static const char *type2string(unsigned short qtype) +#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 +#define HTTPS_RR_CODE_IPV4 0x04 +#define HTTPS_RR_CODE_ECH 0x05 +#define HTTPS_RR_CODE_IPV6 0x06 + +static const char *type2string(uint16_t qtype) { switch(qtype) { case QTYPE_A: @@ -91,15 +187,16 @@ static const char *type2string(unsigned short qtype) */ static int store_incoming(int qid, const unsigned char *data, size_t size, unsigned char *qbuf, size_t qbuflen, size_t *qlen, - unsigned short *qtype, unsigned short *idp) + uint16_t *qtype, uint16_t *idp) { FILE *server; char dumpfile[256]; #if 0 size_t i; #endif - unsigned short qd; - const unsigned char *qptr; + uint16_t qd; + const uint8_t *qptr; + char name[256]; size_t qsize; *qlen = 0; @@ -156,7 +253,7 @@ static int store_incoming(int qid, const unsigned char *data, size_t size, qsize = size; qptr = data; - if(!qname(&data, &size)) { + if(!qname2str(&data, &size, name, sizeof(name))) { qd = get16bit(&data, &size); fprintf(server, "QNAME %s QTYPE %s\n", name, type2string(qd)); *qtype = qd; @@ -188,38 +285,27 @@ static int store_incoming(int qid, const unsigned char *data, size_t size, return 0; } -static void add_answer(unsigned char *bytes, size_t *w, - const unsigned char *a, size_t alen, - unsigned short qtype) +static int add_answer(struct blob *body, + const unsigned char *a, size_t alen, + uint16_t qtype) { - size_t i = *w; - - /* add answer */ - bytes[i++] = 0xc0; - bytes[i++] = 0x0c; /* points to the query at this fixed packet index */ + uint8_t prefix[10] = { + 0xc0, 0x0c, /* points to the query at this fixed packet index */ + 0x00, 0x00, + 0x00, 0x01, /* QCLASS IN */ + 0x00, 0x00, + 0x0a, 0x14, /* TTL, Time to live: 2580 (43 minutes) */ + }; /* QTYPE */ - bytes[i++] = (unsigned char)(qtype >> 8); - bytes[i++] = (unsigned char)(qtype & 0xff); + prefix[2] = (unsigned char)(qtype >> 8); + prefix[3] = (unsigned char)(qtype & 0xff); - /* QCLASS IN */ - bytes[i++] = 0x00; - bytes[i++] = 0x01; - - /* TTL, Time to live: 2580 (43 minutes) */ - bytes[i++] = 0x00; - bytes[i++] = 0x00; - bytes[i++] = 0x0a; - bytes[i++] = 0x14; - - /* QTYPE size */ - bytes[i++] = (unsigned char)(alen >> 8); - bytes[i++] = (unsigned char)(alen & 0xff); - - memcpy(&bytes[i], a, alen); - i += alen; - - *w = i; + if(blob_addn(body, prefix, sizeof(prefix))) + return 1; + if((alen > UINT16_MAX) || blob_add_uint16(body, (uint16_t)alen)) + return 1; + return blob_addn(body, a, alen); } #ifdef _WIN32 @@ -230,12 +316,9 @@ static void add_answer(unsigned char *bytes, size_t *w, #define INSTRUCTIONS "dnsd.cmd" -#define MAX_ALPN 5 - +static curlx_struct_stat finfo_last; static unsigned char ipv4_pref[4]; static unsigned char ipv6_pref[16]; -static unsigned char alpn_pref[MAX_ALPN]; -static int alpn_count; static unsigned char ancount_a; static unsigned char ancount_aaaa; @@ -245,14 +328,15 @@ static timediff_t https_delay_ms; static int query_id = -1; +static struct blob httpsrr; + struct resp { struct resp *next; int qid; struct curltime send_ts; struct sockaddr addr; curl_socklen_t addrlen; - char body[256]; - size_t blen; + struct blob body; }; static struct resp *resp_queue; @@ -262,12 +346,13 @@ static CURLcode send_resp(curl_socket_t sock, struct resp *resp) ssize_t rc; sending: - rc = sendto(sock, (const void *)resp->body, (SENDTO3)resp->blen, 0, + rc = sendto(sock, (const void *)resp->body.data, (SENDTO3)resp->body.dlen, 0, &resp->addr, resp->addrlen); if((rc < 0) && (SOCKERRNO == SOCKEINTR)) goto sending; - if(rc != (ssize_t)resp->blen) { - logmsg("failed sending %d bytes, errno=%d\n", (int)resp->blen, SOCKERRNO); + if(rc != (ssize_t)resp->body.dlen) { + logmsg("failed sending %d bytes, errno=%d\n", + (int)resp->body.dlen, SOCKERRNO); return CURLE_SEND_ERROR; } logmsg("[%d] sent response", resp->qid); @@ -321,14 +406,13 @@ static void clear_resp_queue(void) static struct resp * create_resp(int qid, const struct sockaddr *addr, curl_socklen_t addrlen, const unsigned char *qbuf, size_t qlen, - unsigned short qtype, unsigned short id) + uint16_t qtype, uint16_t id) { struct resp *resp; - size_t i; int a; timediff_t delay_ms = 0; char addrbuf[128]; /* IP address buffer */ - unsigned char bytes[256] = { + uint8_t header[12] = { 0x80, 0xea, /* ID, overwrite */ 0x81, 0x80, /* @@ -352,70 +436,90 @@ create_resp(int qid, const struct sockaddr *addr, curl_socklen_t addrlen, 0x0, 0x0, /* NSCOUNT */ 0x0, 0x0 /* ARCOUNT */ }; + uint16_t ancount = 0; + + switch(qtype) { + case QTYPE_A: + ancount = ancount_a; + delay_ms = a_delay_ms; + break; + case QTYPE_AAAA: + ancount = ancount_aaaa; + delay_ms = aaaa_delay_ms; + break; + case QTYPE_HTTPS: + if(httpsrr.dlen) + ancount = 1; + delay_ms = https_delay_ms; + break; + } resp = curlx_calloc(1, sizeof(*resp)); if(!resp) - return NULL; + goto error; resp->qid = qid; /* on some platforms `curl_socklen_t` is an `int`. Casting might * wrap this, but then it still has to fit our record size. */ if((size_t)addrlen > sizeof(resp->addr)) { logmsg("unable to handle addrlen of %zu", (size_t)addrlen); - curlx_free(resp); - return NULL; + goto error; } memcpy(&resp->addr, CURL_UNCONST(addr), addrlen); resp->addrlen = addrlen; - bytes[0] = (unsigned char)(id >> 8); - bytes[1] = (unsigned char)(id & 0xff); + header[0] = (uint8_t)(id >> 8); + header[1] = (uint8_t)(id & 0xff); - if(qlen > (sizeof(bytes) - 12)) { + header[6] = (uint8_t)(ancount >> 8); + header[7] = (uint8_t)(ancount & 0xff); + + if(blob_addn(&resp->body, header, sizeof(header))) + goto error; + + if(blob_addn(&resp->body, qbuf, qlen)) { logmsg("unable to handle query of length %zu", qlen); - curlx_free(resp); - return NULL; + goto error; } - /* append query, includes QTYPE and QCLASS */ - memcpy(&bytes[12], qbuf, qlen); - - i = 12 + qlen; - switch(qtype) { case QTYPE_A: - bytes[7] = ancount_a; for(a = 0; a < ancount_a; a++) { const unsigned char *store = ipv4_pref; - add_answer(bytes, &i, store, sizeof(ipv4_pref), QTYPE_A); + if(add_answer(&resp->body, store, sizeof(ipv4_pref), QTYPE_A)) + goto error; logmsg("[%d] response A (%x) '%s'", qid, QTYPE_A, curlx_inet_ntop(AF_INET, store, addrbuf, sizeof(addrbuf))); } if(!ancount_a) logmsg("[%d] response A empty", qid); - delay_ms = a_delay_ms; break; case QTYPE_AAAA: - bytes[7] = ancount_aaaa; for(a = 0; a < ancount_aaaa; a++) { const unsigned char *store = ipv6_pref; - add_answer(bytes, &i, store, sizeof(ipv6_pref), QTYPE_AAAA); + if(add_answer(&resp->body, store, sizeof(ipv6_pref), QTYPE_AAAA)) + goto error; logmsg("[%d] response AAAA (%x) '%s'", qid, QTYPE_AAAA, curlx_inet_ntop(AF_INET6, store, addrbuf, sizeof(addrbuf))); } if(!ancount_aaaa) logmsg("[%d] response AAAA empty", qid); - delay_ms = aaaa_delay_ms; break; case QTYPE_HTTPS: - logmsg("[%d] response HTTPS (empty, so far)", qid); - bytes[7] = 0; /* no answer so far */ - delay_ms = https_delay_ms; + if(httpsrr.dlen) { + if(add_answer(&resp->body, httpsrr.data, httpsrr.dlen, QTYPE_HTTPS)) { + logmsg("[%d] error adding https %zu response bytes", qid, + httpsrr.dlen); + goto error; + } + logmsg("[%d] response HTTPS (%x), %zu bytes", qid, QTYPE_HTTPS, + httpsrr.dlen); + } + else + logmsg("[%d] response HTTPS, no record", qid); break; } - memcpy(&resp->body, bytes, i); - resp->blen = i; resp->send_ts = curlx_now(); if(delay_ms > 0) { int usec = (int)((delay_ms % 1000) * 1000); @@ -425,25 +529,135 @@ create_resp(int qid, const struct sockaddr *addr, curl_socklen_t addrlen, resp->send_ts.tv_sec++; resp->send_ts.tv_usec -= 1000000; } + logmsg("[%d] delay response by %" FMT_TIMEDIFF_T "ms", qid, delay_ms); } return resp; + +error: + logmsg("[%d] failed to create response", qid); + curlx_free(resp); + return NULL; +} + +static int read_https_alpn_part(struct blob *b, struct Curl_str *str) +{ + size_t i, skip; + + for(i = 0; i < str->len; ++i) { + if(str->str[i] == ',') + break; + } + if(i > 256) + return 1; + if(blob_add(b, (uint8_t)i) || blob_addchars(b, str->str, i)) + return 1; + skip = i + ((i < str->len) ? 1 : 0); + str->str += skip; + str->len -= skip; + return 0; +} + +static int read_https_alpn(struct blob *b, const char **ps) +{ + struct Curl_str word; + struct blob tmp; + + blob_reset(&tmp); + if(curlx_str_word(ps, &word, UINT16_MAX)) + return 1; + while(word.len) { + if(read_https_alpn_part(&tmp, &word)) + return 1; + } + + if(tmp.dlen > UINT16_MAX) + return 1; + + if(blob_add_uint16(b, HTTPS_RR_CODE_ALPN) || + blob_add_uint16(b, (uint16_t)tmp.dlen) || + blob_addn(b, tmp.data, tmp.dlen)) + return 1; + return 0; +} + +static int read_https(struct blob *b, const char *s) +{ + struct Curl_str word; + curl_off_t n; + + blob_reset(b); + /* Parse a HTTPS textual representation inspired by RFC 9460 */ + curlx_str_passblanks(&s); + if(curlx_str_number(&s, &n, UINT16_MAX)) + return 1; + if(blob_add_uint16(b, (uint16_t)n)) + return 1; + + curlx_str_passblanks(&s); + if(curlx_str_word(&s, &word, UINT16_MAX)) { + logmsg("https: unable to read target qname, input=%s", s); + return 1; + } + if(blob_add_qname(b, &word)) + return 1; + + while(*s) { + curlx_str_passblanks(&s); + if(!*s) + break; + if(!strncmp("alpn=", s, 5)) { + s += 5; + if(read_https_alpn(b, &s)) + return 1; + } + else if(!strncmp("no-default-alpn", s, 15)) { + s += 15; + if(blob_add_uint16(b, HTTPS_RR_CODE_NO_DEF_ALPN) || + blob_add_uint16(b, 0)) + return 1; + } + else + return 1; + } + return 0; } static void read_instructions(void) { char file[256]; FILE *f; - - /* reset defaults */ - a_delay_ms = aaaa_delay_ms = https_delay_ms = 0; + curlx_struct_stat finfo; snprintf(file, sizeof(file), "%s/" INSTRUCTIONS, logdir); + if((curlx_stat(file, &finfo) == 0) && + (finfo.st_mtime == finfo_last.st_mtime) && + (finfo.st_size == finfo_last.st_size) +#ifndef _WIN32 + && (finfo.st_ino == finfo_last.st_ino) +#endif +#ifdef __APPLE__ + && (finfo.st_mtimespec.tv_nsec == finfo_last.st_mtimespec.tv_nsec) +#elif defined(_POSIX_C_SOURCE) +#if _POSIX_C_SOURCE >= 200809L + && (finfo.st_mtim.tv_nsec == finfo_last.st_mtim.tv_nsec) +#endif +#endif + ) { + /* looks the same as before, skip reading it again */ + return; + } + /* reset defaults */ + a_delay_ms = aaaa_delay_ms = https_delay_ms = 0; + blob_reset(&httpsrr); + finfo_last = finfo; + + logmsg("read instructions from %s", file); f = curlx_fopen(file, FOPEN_READTEXT); if(f) { char buf[256]; ancount_aaaa = ancount_a = 0; - alpn_count = 0; while(fgets(buf, sizeof(buf), f)) { + const char *rtype = NULL; char *p = strchr(buf, '\n'); if(p) { int rc; @@ -451,6 +665,7 @@ static void read_instructions(void) if(!strncmp("A: ", buf, 3)) { rc = curlx_inet_pton(AF_INET, &buf[3], ipv4_pref); ancount_a = (rc == 1); + rtype = "A"; } else if(!strncmp("AAAA: ", buf, 6)) { char *p6 = &buf[6]; @@ -462,23 +677,11 @@ static void read_instructions(void) } rc = curlx_inet_pton(AF_INET6, p6, ipv6_pref); ancount_aaaa = (rc == 1); + rtype = "AAAA"; } - else if(!strncmp("ALPN: ", buf, 6)) { - char *ap = &buf[6]; - rc = 0; - while(*ap) { - if('h' == *ap) { - ap++; - if(*ap >= '1' && *ap <= '3') { - if(alpn_count < MAX_ALPN) - alpn_pref[alpn_count++] = *ap; - } - else - break; - } - else - break; - } + else if(!strncmp("HTTPS: ", buf, 7)) { + rc = read_https(&httpsrr, &buf[7]) ? 0 : 1; + rtype = "HTTPS"; } else if(!strncmp("Delay-A: ", buf, 9)) { curl_off_t ms; @@ -514,8 +717,14 @@ static void read_instructions(void) if(rc != 1) { logmsg("Bad line in %s: '%s'\n", file, buf); } + else if(rtype) { + logmsg("added %s record via '%s'", rtype, buf); + } } } + logmsg("set delays: A=%" FMT_TIMEDIFF_T "ms AAAA=%" FMT_TIMEDIFF_T + "ms HTTPS=%" FMT_TIMEDIFF_T "ms", + a_delay_ms, aaaa_delay_ms, https_delay_ms); curlx_fclose(f); } else @@ -527,7 +736,7 @@ static int test_dnsd(int argc, const char **argv) srvr_sockaddr_union_t me; ssize_t n = 0; int arg = 1; - unsigned short port = 9123; /* UDP */ + uint16_t port = 9123; /* UDP */ curl_socket_t sock = CURL_SOCKET_BAD; int flag; int rc; @@ -596,7 +805,7 @@ static int test_dnsd(int argc, const char **argv) if(argc > arg) { opt = argv[arg]; if(!curlx_str_number(&opt, &num, 0xffff)) - port = (unsigned short)num; + port = (uint16_t)num; arg++; } } @@ -733,13 +942,13 @@ static int test_dnsd(int argc, const char **argv) curlx_nonblock(sock, TRUE); for(;;) { - unsigned short id = 0; - unsigned char inbuffer[1500]; + uint16_t id = 0; + uint8_t inbuffer[1500]; srvr_sockaddr_union_t from; curl_socklen_t fromlen; - unsigned char qbuf[256]; /* query storage */ + uint8_t qbuf[256]; /* query storage */ size_t qlen = 0; /* query size */ - unsigned short qtype = 0; + uint16_t qtype = 0; timediff_t timeout_ms = 0; fromlen = sizeof(from); #ifdef USE_IPV6 @@ -770,7 +979,6 @@ static int test_dnsd(int argc, const char **argv) logmsg("error %d returned by select()", SOCKERRNO); } else if(!rc) { /* timeout */ - logmsg("select timeout, run again"); continue; } }