← Commit list

a973377cd7

Stefan Eissing
Stefan Eissing
2026-04-16
SUCCESS Duration: 244s

Subject

dnsd: implement HTTPS-RR with alpn pref and no-default-alpn

Binary

No binary (build failed)

Architecture & Compiler Flags

Architecture: x86_64-linux-gnu Compiler: gcc (Ubuntu 11.4.0-1ubuntu1~22.04.3) 11.4.0

—

Build Log

(log yok)

Commit Diff GitHub →

docs/libcurl/libcurl-env-dbg.md+5 -0
@@ -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.
lib/cf-https-connect.c+25 -5
@@ -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)
tests/http/Makefile.am+1 -0
@@ -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 \
tests/http/test_06_eyeballs.py+2 -4
@@ -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):
tests/http/test_22_httpsrr.py+134 -0
@@ -0,0 +1,134 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#***************************************************************************
+# _ _ ____ _
+# Project ___| | | | _ \| |
+# / __| | | | |_) | |
+# | (__| |_| | _ <| |___
+# \___|\___/|_| \_\_____|
+#
+# Copyright (C) Daniel Stenberg, <[email protected]>, 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}'
tests/http/testenv/dnsd.py+7 -1
@@ -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))
tests/server/dnsd.c+309 -101
@@ -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);
-
- /* 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;
+ prefix[2] = (unsigned char)(qtype >> 8);
+ prefix[3] = (unsigned char)(qtype & 0xff);
+
+ 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)) {
- logmsg("unable to handle query of length %zu", qlen);
- curlx_free(resp);
- return NULL;
- }
+ header[6] = (uint8_t)(ancount >> 8);
+ header[7] = (uint8_t)(ancount & 0xff);
- /* append query, includes QTYPE and QCLASS */
- memcpy(&bytes[12], qbuf, qlen);
+ if(blob_addn(&resp->body, header, sizeof(header)))
+ goto error;
- i = 12 + qlen;
+ if(blob_addn(&resp->body, qbuf, qlen)) {
+ logmsg("unable to handle query of length %zu", qlen);
+ goto error;
+ }
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;
+ 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;
- snprintf(file, sizeof(file), "%s/" INSTRUCTIONS, logdir);
+ 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;
}
}