mirror of
https://github.com/curl/curl.git
synced 2026-07-23 00:47:18 +03:00
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:
parent
1bf1f8ed6a
commit
a973377cd7
7 changed files with 484 additions and 112 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 \
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
134
tests/http/test_22_httpsrr.py
Normal file
134
tests/http/test_22_httpsrr.py
Normal 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}'
|
||||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue