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
This commit is contained in:
Stefan Eissing 2026-04-16 13:18:38 +02:00 committed by Daniel Stenberg
parent 1bf1f8ed6a
commit a973377cd7
No known key found for this signature in database
GPG key ID: 5CC908FDB71E12C2
7 changed files with 484 additions and 112 deletions

View file

@ -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.

View file

@ -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)

View file

@ -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 \

View file

@ -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):

View file

@ -0,0 +1,134 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#***************************************************************************
# _ _ ____ _
# 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
#
###########################################################################
#
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}'

View file

@ -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))

View file

@ -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;
}
}