From 7f6a75664f9fb45390193e492ee3361fb795d098 Mon Sep 17 00:00:00 2001 From: Stefan Eissing Date: Thu, 6 Aug 2026 13:46:41 +0200 Subject: [PATCH] dnsd: add support for DoH dnsd now opens UDP+TCP sockets and accepts http: DoH requests to obtain the same, configured answers (records, delays, error codes) as over UDP. DoH: use `async->queries_ongoing` like all other resolvers instead of the internal `pending` counter. Fixes waiting for results. Tests: in pytest, parameterize dnsd tests to use both DNS and DoH. Closes #22506 --- lib/vdns/doh.c | 18 +- lib/vdns/doh.h | 1 - tests/http/test_21_resolve.py | 87 ++- tests/http/test_22_httpsrr.py | 85 +-- tests/http/testenv/env.py | 5 + tests/server/dnsd.c | 1313 ++++++++++++++++++++++----------- tests/server/first.h | 8 + tests/server/sws.c | 225 +----- tests/server/util.c | 339 +++++++++ 9 files changed, 1357 insertions(+), 724 deletions(-) diff --git a/lib/vdns/doh.c b/lib/vdns/doh.c index 592cb07583..ccf763dc54 100644 --- a/lib/vdns/doh.c +++ b/lib/vdns/doh.c @@ -251,8 +251,8 @@ static void doh_probe_done(struct Curl_easy *doh, return; } - dohp->pending--; - infof(doh, "a DoH request is completed, %u to go", dohp->pending); + async->queries_ongoing--; + infof(doh, "a DoH request is completed, %u to go", async->queries_ongoing); dohp->probe_resp[i].result = result; /* We expect either the meta data still to exist or the sub request * to have already failed. */ @@ -267,7 +267,7 @@ static void doh_probe_done(struct Curl_easy *doh, if(result) infof(doh, "DoH request %s", curl_easy_strerror(result)); - if(!dohp->pending) { + if(!async->queries_ongoing) { /* DoH completed, run master to act on results */ Curl_multi_mark_dirty(master); } @@ -487,7 +487,7 @@ CURLcode Curl_doh(struct Curl_easy *data, &dohp->probe_resp[DOH_SLOT_IPV4].probe_mid); if(result) goto error; - dohp->pending++; + async->queries_ongoing++; } #ifdef USE_IPV6 @@ -499,7 +499,7 @@ CURLcode Curl_doh(struct Curl_easy *data, &dohp->probe_resp[DOH_SLOT_IPV6].probe_mid); if(result) goto error; - dohp->pending++; + async->queries_ongoing++; } #endif @@ -520,7 +520,7 @@ CURLcode Curl_doh(struct Curl_easy *data, curlx_free(qname); if(result) goto error; - dohp->pending++; + async->queries_ongoing++; } #endif return CURLE_OK; @@ -1211,7 +1211,7 @@ CURLcode Curl_doh_take_result(struct Curl_easy *data, return async->for_proxy ? CURLE_COULDNT_RESOLVE_PROXY : CURLE_COULDNT_RESOLVE_HOST; } - else if(!dohp->pending) { + else if(!async->queries_ongoing) { struct Curl_dns_entry *dns = NULL; DOHcode rc[DOH_SLOT_COUNT]; bool negative = TRUE; @@ -1235,7 +1235,7 @@ CURLcode Curl_doh_take_result(struct Curl_easy *data, if(rc[slot] && (rc[slot] != DOH_DNS_NXDOMAIN)) negative = FALSE; if(rc[slot]) { - CURL_TRC_DNS(data, "[%s] [DoH] error: %s type %s for %s", + CURL_TRC_DNS(data, "[%s] [DoH] error: %s of type %s for %s", Curl_resolv_query_str(async->dns_queries), doh_strerror(rc[slot]), doh_type2name(p->dnstype), async->peer->hostname); @@ -1306,7 +1306,7 @@ CURLcode Curl_doh_take_result(struct Curl_easy *data, if(dns) result = Curl_dnscache_add(data, dns); *pdns = dns; - } /* !dohp->pending */ + } /* !async->queries_ongoing */ else /* wait for pending DoH transactions to complete */ return CURLE_AGAIN; diff --git a/lib/vdns/doh.h b/lib/vdns/doh.h index 5179c28cae..1e4271796f 100644 --- a/lib/vdns/doh.h +++ b/lib/vdns/doh.h @@ -109,7 +109,6 @@ struct doh_response { * as easy meta for CURL_EZM_DOH_MASTER */ struct doh_probes { struct doh_response probe_resp[DOH_SLOT_COUNT]; - unsigned int pending; /* still outstanding probes */ }; /* diff --git a/tests/http/test_21_resolve.py b/tests/http/test_21_resolve.py index ec15de7a9c..685a9792ae 100644 --- a/tests/http/test_21_resolve.py +++ b/tests/http/test_21_resolve.py @@ -124,73 +124,84 @@ class TestResolve: r.check_stats(count=count, http_status=0, exitcode=6) assert r.duration > timedelta(milliseconds=count * delay_ms), f'{r}' - # dnsd with no answers - @pytest.mark.skipif(condition=not Env.curl_override_dns(), reason="no DNS override") - def test_21_06_dnsd_empty(self, env: Env, httpd, dnsd): - dnsd.set_answers() + def dns_settings(self, dns_method, dnsd): + xargs = [] run_env = os.environ.copy() - run_env['CURL_DNS_SERVER'] = f'127.0.0.1:{dnsd.port}' + run_env['CURL_DEBUG'] = 'dns,doh' + if dns_method == 'DoH': + if not Env.curl_can_doh(): + pytest.skip(reason="curl built without DoH") + xargs = ['--doh-insecure', '--doh-url', f'http://127.0.0.1:{dnsd.port}/'] + else: + if not Env.curl_override_dns(): + pytest.skip(reason="no DNS override") + run_env['CURL_DNS_SERVER'] = f'127.0.0.1:{dnsd.port}' + run_env['CURL_QUICK_EXIT'] = '1' + return run_env, xargs + + # dnsd with no answers + @pytest.mark.parametrize("dns_method", ["DNS", "DoH"]) + def test_21_06_dnsd_empty(self, env: Env, httpd, dnsd, dns_method): + dnsd.set_answers() + run_env, xargs = self.dns_settings(dns_method, dnsd) curl = CurlClient(env=env, run_env=run_env, force_resolv=False) url = 'https://test-dnsd.http.curl.invalid/' - r = curl.http_download(urls=[url], with_stats=True) + r = curl.http_download(urls=[url], with_stats=True, extra_args=xargs) r.check_exit_code(6) # could not resolve host r.check_stats(count=1, http_status=0, exitcode=6) # dnsd with one answer for A - @pytest.mark.skipif(condition=not Env.curl_override_dns(), reason="no DNS override") - def test_21_07_dnsd_a(self, env: Env, httpd, dnsd): + @pytest.mark.parametrize("dns_method", ["DNS", "DoH"]) + def test_21_07_dnsd_a(self, env: Env, httpd, dnsd, dns_method): dnsd.set_answers(addr_a=['127.0.0.1']) - run_env = os.environ.copy() - run_env['CURL_DNS_SERVER'] = f'127.0.0.1:{dnsd.port}' + run_env, xargs = self.dns_settings(dns_method, dnsd) 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 = curl.http_download(urls=[url], with_stats=True, extra_args=xargs) r.check_exit_code(0) r.check_stats(count=1, http_status=200, exitcode=0) assert r.stats[0]['remote_ip'] == '127.0.0.1' # dnsd with one answer for AAAA - @pytest.mark.skipif(condition=not Env.curl_override_dns(), reason="no DNS override") @pytest.mark.skipif(condition=not Env.curl_has_feature('IPv6'), reason="no IPv6") - def test_21_08_dnsd_aaaa(self, env: Env, httpd, dnsd): + @pytest.mark.parametrize("dns_method", ["DNS", "DoH"]) + def test_21_08_dnsd_aaaa(self, env: Env, httpd, dnsd, dns_method): dnsd.set_answers(addr_aaaa=['[::1]']) - run_env = os.environ.copy() - run_env['CURL_DNS_SERVER'] = f'127.0.0.1:{dnsd.port}' - run_env['CURL_QUICK_EXIT'] = '1' + run_env, xargs = self.dns_settings(dns_method, dnsd) 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 = curl.http_download(urls=[url], with_stats=True, extra_args=xargs) r.check_exit_code(0) r.check_stats(count=1, http_status=200, exitcode=0) assert r.stats[0]['remote_ip'] == '::1' # dnsd with one answer for A, delayed one for AAAA - @pytest.mark.skipif(condition=not Env.curl_override_dns(), reason="no DNS override") - def test_21_09_dnsd_a_delay(self, env: Env, httpd, dnsd): + @pytest.mark.parametrize("dns_method", ["DNS", "DoH"]) + def test_21_09_dnsd_a_delay(self, env: Env, httpd, dnsd, dns_method): + if dns_method == 'DoH': + pytest.skip(reason='DoH does not handle partial responses') dnsd.set_answers(addr_a=['127.0.0.1'], addr_aaaa=['[::1]'], delay_aaaa_ms=env.test_timeout * 1000) - run_env = os.environ.copy() - run_env['CURL_DNS_SERVER'] = f'127.0.0.1:{dnsd.port}' - run_env['CURL_QUICK_EXIT'] = '1' + run_env, xargs = self.dns_settings(dns_method, dnsd) 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 = curl.http_download(urls=[url], with_stats=True, extra_args=xargs) r.check_exit_code(0) r.check_stats(count=1, http_status=200, exitcode=0) assert r.stats[0]['remote_ip'] == '127.0.0.1' # dnsd with one answer for AAAA, delayed one for A - @pytest.mark.skipif(condition=not Env.curl_override_dns(), reason="no DNS override") @pytest.mark.skipif(condition=not Env.curl_has_feature('IPv6'), reason="no IPv6") - def test_21_10_dnsd_aaaa_delay(self, env: Env, httpd, dnsd): + @pytest.mark.parametrize("dns_method", ["DNS", "DoH"]) + def test_21_10_dnsd_aaaa_delay(self, env: Env, httpd, dnsd, dns_method): + if dns_method == 'DoH': + pytest.skip(reason='DoH does not handle partial responses') dnsd.set_answers(addr_a=['127.0.0.1'], addr_aaaa=['[::1]'], delay_a_ms=env.test_timeout * 1000) - run_env = os.environ.copy() - run_env['CURL_DNS_SERVER'] = f'127.0.0.1:{dnsd.port}' - run_env['CURL_QUICK_EXIT'] = '1' + run_env, xargs = self.dns_settings(dns_method, dnsd) 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 = curl.http_download(urls=[url], with_stats=True, extra_args=xargs) r.check_exit_code(0) r.check_stats(count=1, http_status=200, exitcode=0) assert r.stats[0]['remote_ip'] == '::1' @@ -234,15 +245,14 @@ class TestResolve: # dnsd giving NXDOMAIN for all families: the negative answer is # cached and a second lookup of the same name uses the cache - @pytest.mark.skipif(condition=not Env.curl_override_dns(), reason="no DNS override") - def test_21_13_dnsd_nxdomain_cached(self, env: Env, httpd, dnsd): + @pytest.mark.parametrize("dns_method", ["DNS", "DoH"]) + def test_21_13_dnsd_nxdomain_cached(self, env: Env, httpd, dnsd, dns_method): count = 2 dnsd.set_answers(rcode_a=3, rcode_aaaa=3) - run_env = os.environ.copy() - run_env['CURL_DNS_SERVER'] = f'127.0.0.1:{dnsd.port}' + run_env, xargs = self.dns_settings(dns_method, dnsd) curl = CurlClient(env=env, run_env=run_env, force_resolv=False) urls = [f'https://test-nx.http.curl.invalid/?id={i}' for i in range(count)] - r = curl.http_download(urls=urls, with_stats=True) + r = curl.http_download(urls=urls, with_stats=True, extra_args=xargs) r.check_exit_code(6) r.check_stats(count=count, http_status=0, exitcode=6) if env.curl_is_verbose(): @@ -250,15 +260,14 @@ class TestResolve: # dnsd failing one family with SERVFAIL: not an authoritative # negative answer, a second lookup of the same name tries again - @pytest.mark.skipif(condition=not Env.curl_override_dns(), reason="no DNS override") - def test_21_14_dnsd_servfail_uncached(self, env: Env, httpd, dnsd): + @pytest.mark.parametrize("dns_method", ["DNS", "DoH"]) + def test_21_14_dnsd_servfail_uncached(self, env: Env, httpd, dnsd, dns_method): count = 2 dnsd.set_answers(rcode_a=2, rcode_aaaa=3) - run_env = os.environ.copy() - run_env['CURL_DNS_SERVER'] = f'127.0.0.1:{dnsd.port}' + run_env, xargs = self.dns_settings(dns_method, dnsd) curl = CurlClient(env=env, run_env=run_env, force_resolv=False) urls = [f'https://test-sf.http.curl.invalid/?id={i}' for i in range(count)] - r = curl.http_download(urls=urls, with_stats=True) + r = curl.http_download(urls=urls, with_stats=True, extra_args=xargs) r.check_exit_code(6) r.check_stats(count=count, http_status=0, exitcode=6) if env.curl_is_verbose(): diff --git a/tests/http/test_22_httpsrr.py b/tests/http/test_22_httpsrr.py index 6ae5d0c728..63bb2c029e 100644 --- a/tests/http/test_22_httpsrr.py +++ b/tests/http/test_22_httpsrr.py @@ -33,7 +33,6 @@ 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 TestHTTPSRR: @@ -44,86 +43,86 @@ class TestHTTPSRR: 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']) + def dns_settings(self, dns_method, dnsd): 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' + run_env['CURL_DBG_AWAIT_HTTPSRR'] = '1' + xargs = [] + if dns_method == 'DoH': + if not Env.curl_can_doh(): + pytest.skip(reason="curl built without DoH") + xargs = ['--doh-insecure', '--doh-url', f'http://127.0.0.1:{dnsd.port}/'] + else: + if not Env.curl_override_dns(): + pytest.skip(reason="no DNS override") + run_env['CURL_DNS_SERVER'] = f'127.0.0.1:{dnsd.port}' + return run_env, xargs + + # dnsd a HTTPS-RR that prefers HTTP/1.1. + @pytest.mark.parametrize("dns_method", ["DNS", "DoH"]) + def test_22_01_httpsrr_h1(self, env: Env, httpd, dnsd, dns_method): + dnsd.set_answers(addr_a=['127.0.0.1'], + https=['10 . alpn=http/1.1']) + run_env, xargs = self.dns_settings(dns_method, dnsd) 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 = curl.http_download(urls=[url], with_stats=True, extra_args=xargs) 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.have_h3(), reason="missing HTTP/3 support") - def test_22_02_httpsrr_h3(self, env: Env, httpd, dnsd, nghttpx): + @pytest.mark.parametrize("dns_method", ["DNS", "DoH"]) + def test_22_02_httpsrr_h3(self, env: Env, httpd, dnsd, nghttpx, dns_method): 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' + run_env, xargs = self.dns_settings(dns_method, dnsd) + xargs.append('--http3') 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 = curl.http_download(urls=[url], with_stats=True, extra_args=xargs) 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): + @pytest.mark.parametrize("dns_method", ["DNS", "DoH"]) + def test_22_03_httpsrr_h3(self, env: Env, httpd, dnsd, nghttpx, dns_method): 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' + run_env, xargs = self.dns_settings(dns_method, dnsd) 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 = curl.http_download(urls=[url], with_stats=True, extra_args=xargs) 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): + @pytest.mark.parametrize("dns_method", ["DNS", "DoH"]) + def test_22_04_httpsrr_wrong_target(self, env: Env, httpd, dnsd, dns_method): 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' + run_env, xargs = self.dns_settings(dns_method, dnsd) 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 = curl.http_download(urls=[url], with_stats=True, extra_args=xargs) 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): + @pytest.mark.parametrize("dns_method", ["DNS", "DoH"]) + def test_22_05_httpsrr_no_default_alpn(self, env: Env, httpd, dnsd, dns_method): 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' + run_env, xargs = self.dns_settings(dns_method, dnsd) 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 = curl.http_download(urls=[url], with_stats=True, extra_args=xargs) r.check_exit_code(0) r.check_stats(count=1, http_status=200, exitcode=0) assert r.stats[0]['http_version'] == '2', f'{r}' @@ -132,16 +131,14 @@ class TestHTTPSRR: @pytest.mark.skipif(condition=not Env.curl_has_feature('HTTPS-proxy'), reason='curl lacks HTTPS-proxy support') @pytest.mark.skipif(condition=not Env.have_nghttpx(), reason="no nghttpx available") - def test_22_06_httpsrr_proxy(self, env: Env, httpd, nghttpx_fwd, dnsd): + @pytest.mark.parametrize("dns_method", ["DNS", "DoH"]) + def test_22_06_httpsrr_proxy(self, env: Env, httpd, nghttpx_fwd, dnsd, dns_method): 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, xargs = self.dns_settings(dns_method, dnsd) curl = CurlClient(env=env, run_env=run_env) url = f'https://localhost:{env.https_port}/data.json' - xargs = curl.get_proxy_args(tunnel=True) + xargs.extend(curl.get_proxy_args(tunnel=True)) r = curl.http_download(urls=[url], alpn_proto='h2', with_stats=True, extra_args=xargs) r.check_response(count=1, http_status=200) diff --git a/tests/http/testenv/env.py b/tests/http/testenv/env.py index 0c19d6bd16..baabbe7254 100644 --- a/tests/http/testenv/env.py +++ b/tests/http/testenv/env.py @@ -163,6 +163,7 @@ class EnvConfig: self.curl_can_cert_status = 'cert-status: ON' in p.stdout self.curl_override_dns = 'override-dns: ON' in p.stdout self.curl_resolv_threaded = 'resolv-threaded: ON' in p.stdout + self.curl_can_doh = 'DoH: ON' in p.stdout self.ports = {} @@ -590,6 +591,10 @@ class Env: def curl_resolv_threaded() -> bool: return Env.CONFIG.curl_resolv_threaded + @staticmethod + def curl_can_doh() -> bool: + return Env.CONFIG.curl_can_doh + @staticmethod def curl_can_early_data() -> bool: if Env.curl_uses_lib('gnutls'): diff --git a/tests/server/dnsd.c b/tests/server/dnsd.c index 8e195e5c61..4c512a3701 100644 --- a/tests/server/dnsd.c +++ b/tests/server/dnsd.c @@ -187,7 +187,8 @@ static const char *type2string(uint16_t qtype) * * Return query (qname + type + class), type and id. */ -static int store_incoming(int qid, const unsigned char *data, size_t size, +static int store_incoming(const char *source, int query_id, + const unsigned char *data, size_t size, unsigned char *qbuf, size_t qbuflen, size_t *qlen, uint16_t *qtype, uint16_t *idp) { @@ -214,7 +215,7 @@ static int store_incoming(int qid, const unsigned char *data, size_t size, int error = errno; logmsg("fopen() failed with error (%d) %s", error, curlx_strerror(error, errbuf, sizeof(errbuf))); - logmsg("Error opening file '%s'", dumpfile); + logmsg("[%s] Error opening file '%s'", source, dumpfile); return -1; } @@ -239,12 +240,12 @@ static int store_incoming(int qid, const unsigned char *data, size_t size, data += 2; /* skip the next 16 bits */ size -= 2; #if 0 - fprintf(server, "QR: %x\n", (*idp & 0x8000) > 15); - fprintf(server, "OPCODE: %x\n", (*idp & 0x7800) >> 11); - fprintf(server, "TC: %x\n", (*idp & 0x200) >> 9); - fprintf(server, "RD: %x\n", (*idp & 0x100) >> 8); - fprintf(server, "Z: %x\n", (*idp & 0x70) >> 4); - fprintf(server, "RCODE: %x\n", (*idp & 0x0f)); + fprintf(server, "[%s] QR: %x\n", source, (*idp & 0x8000) > 15); + fprintf(server, "[%s] OPCODE: %x\n", source, (*idp & 0x7800) >> 11); + fprintf(server, "[%s] TC: %x\n", source, (*idp & 0x200) >> 9); + fprintf(server, "[%s] RD: %x\n", source, (*idp & 0x100) >> 8); + fprintf(server, "[%s] Z: %x\n", source, (*idp & 0x70) >> 4); + fprintf(server, "[%s] RCODE: %x\n", source, (*idp & 0x0f)); #endif (void)get16bit(&data, &size); @@ -259,8 +260,8 @@ static int store_incoming(int qid, const unsigned char *data, size_t size, qd = get16bit(&data, &size); fprintf(server, "QNAME %s QTYPE %s\n", name, type2string(qd)); *qtype = qd; - logmsg("[%d] Question for '%s' type %x / %s", - qid, name, qd, type2string(qd)); + logmsg("[%d] [%s] Question for '%s' type %x / %s", + query_id, source, name, qd, type2string(qd)); (void)get16bit(&data, &size); @@ -310,256 +311,26 @@ static int add_answer(struct blob *body, return blob_addn(body, a, alen); } -#ifdef _WIN32 -#define SENDTO3 int -#else -#define SENDTO3 size_t -#endif - -#define INSTRUCTIONS "dnsd.cmd" - -static curlx_struct_stat finfo_last; -static unsigned char ipv4_pref[4]; -static unsigned char ipv6_pref[16]; -static unsigned char ancount_a; -static unsigned char ancount_aaaa; - -static timediff_t a_delay_ms; -static timediff_t aaaa_delay_ms; -static timediff_t https_delay_ms; -static unsigned char rcode_a; -static unsigned char rcode_aaaa; - -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; - struct blob body; -}; - -static struct resp *resp_queue; - -static CURLcode send_resp(curl_socket_t sock, struct resp *resp) +static void fdset_add_sock(fd_set *fds, curl_socket_t sock, int *pmaxfd) { - ssize_t rc; - int sockerr = 0; - - do { - rc = sendto(sock, (const void *)resp->body.data, (SENDTO3)resp->body.dlen, - 0, &resp->addr, resp->addrlen); - } while((rc < 0) && ((sockerr = SOCKERRNO) == SOCKEINTR)); - if(rc < 0) { - char errbuf[STRERROR_LEN]; - logmsg("failed sending %zu bytes, error: (%d) %s", resp->body.dlen, - sockerr, curlx_strerror(sockerr, errbuf, sizeof(errbuf))); - return CURLE_SEND_ERROR; - } - else if(rc != (ssize_t)resp->body.dlen) { - logmsg("failed sending %zu bytes, sent: %zd", resp->body.dlen, rc); - return CURLE_SEND_ERROR; - } - logmsg("[%d] sent response", resp->qid); - return CURLE_OK; + FD_SET(sock, fds); + if((int)sock > *pmaxfd) + *pmaxfd = (int)sock; } -static void queue_resp(struct resp *resp) +static struct curltime now_plus(timediff_t delta_ms) { - struct resp **panchor = &resp_queue; - while(*panchor) { - timediff_t ms = curlx_ptimediff_ms(&(*panchor)->send_ts, &resp->send_ts); - if(ms > 0) /* resp is to be sent before *panchor */ - break; - panchor = &(*panchor)->next; - } - resp->next = *panchor; - *panchor = resp; -} - -static timediff_t send_resp_queue(curl_socket_t sock) -{ - struct resp **panchor = &resp_queue; - struct curltime now = curlx_now(); - timediff_t timeout_ms = 0; - - while(*panchor) { - struct resp *resp = *panchor; - timediff_t ms = curlx_ptimediff_ms(&resp->send_ts, &now); - - if(ms > 0) { - timeout_ms = ms; - break; + struct curltime ts = curlx_now(); + if(delta_ms > 0) { + int usec = (int)((delta_ms % 1000) * 1000); + ts.tv_sec += (time_t)(delta_ms / 1000); + ts.tv_usec += usec; + if(ts.tv_usec >= 1000000) { + ts.tv_sec++; + ts.tv_usec -= 1000000; } - *panchor = resp->next; - send_resp(sock, resp); - curlx_free(resp); } - return timeout_ms; -} - -static void clear_resp_queue(void) -{ - while(resp_queue) { - struct resp *resp = resp_queue; - resp_queue = resp->next; - curlx_free(resp); - } -} - -/* this is an answer to a question */ -static struct resp * -create_resp(int qid, const struct sockaddr *addr, curl_socklen_t addrlen, - const unsigned char *qbuf, size_t qlen, - uint16_t qtype, uint16_t id) -{ - struct resp *resp; - int a; - timediff_t delay_ms = 0; - char addrbuf[128]; /* IP address buffer */ - uint8_t header[12] = { - 0x80, 0xea, /* ID, overwrite */ - 0x81, 0x80, - /* Flags: 0x8180 Standard query response, No error - - 1... .... .... .... = Response: Message is a response - .000 0... .... .... = Opcode: Standard query (0) - .... .0.. .... .... = Authoritative: Server is not an authority for - domain - .... ..0. .... .... = Truncated: Message is not truncated - .... ...1 .... .... = Recursion desired: Do query recursively - .... .... 1... .... = Recursion available: Server can do recursive - queries - .... .... .0.. .... = Z: reserved (0) - .... .... ..0. .... = Answer authenticated: Answer/authority portion - was not authenticated by the server - .... .... ...0 .... = Non-authenticated data: Unacceptable - .... .... .... 0000 = Reply code: No error (0) - */ - 0x0, 0x1, /* QDCOUNT a single question */ - 0x0, 0x0, /* ANCOUNT number of answers */ - 0x0, 0x0, /* NSCOUNT */ - 0x0, 0x0 /* ARCOUNT */ - }; - uint16_t ancount = 0; - unsigned char rcode = 0; - - switch(qtype) { - case QTYPE_A: - ancount = ancount_a; - delay_ms = a_delay_ms; - rcode = rcode_a; - break; - case QTYPE_AAAA: - ancount = ancount_aaaa; - delay_ms = aaaa_delay_ms; - rcode = rcode_aaaa; - break; - case QTYPE_HTTPS: - if(httpsrr.dlen) - ancount = 1; - delay_ms = https_delay_ms; - break; - } - if(rcode) - ancount = 0; - - resp = curlx_calloc(1, sizeof(*resp)); - if(!resp) - 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); - goto error; - } - memcpy(&resp->addr, CURL_UNCONST(addr), addrlen); - resp->addrlen = addrlen; - - header[0] = (uint8_t)(id >> 8); - header[1] = (uint8_t)(id & 0xff); - - if(rcode) { - header[3] = (uint8_t)((header[3] & 0xf0) | (rcode & 0x0f)); - logmsg("[%d] response rcode %u", qid, (unsigned int)rcode); - } - - 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); - goto error; - } - - switch(qtype) { - case QTYPE_A: - for(a = 0; !rcode && (a < ancount_a); a++) { - const unsigned char *store = ipv4_pref; - const char *ip; - if(add_answer(&resp->body, store, sizeof(ipv4_pref), QTYPE_A)) - goto error; - ip = curlx_inet_ntop(AF_INET, store, addrbuf, sizeof(addrbuf)); - logmsg("[%d] response A (%x) '%s'", qid, (unsigned int)QTYPE_A, - ip ? ip : "(null)"); - } - if(!ancount_a) - logmsg("[%d] response A empty", qid); - break; - case QTYPE_AAAA: - for(a = 0; !rcode && (a < ancount_aaaa); a++) { - const unsigned char *store = ipv6_pref; - const char *ip; - if(add_answer(&resp->body, store, sizeof(ipv6_pref), QTYPE_AAAA)) - goto error; - ip = curlx_inet_ntop(AF_INET6, store, addrbuf, sizeof(addrbuf)); - logmsg("[%d] response AAAA (%x) '%s'", qid, (unsigned int)QTYPE_AAAA, - ip ? ip : "(null)"); - } - if(!ancount_aaaa) - logmsg("[%d] response AAAA empty", qid); - break; - case QTYPE_HTTPS: - 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, - (unsigned int)QTYPE_HTTPS, httpsrr.dlen); - } - else - logmsg("[%d] response HTTPS, no record", qid); - break; - } - - resp->send_ts = curlx_now(); - if(delay_ms > 0) { - int usec = (int)((delay_ms % 1000) * 1000); - resp->send_ts.tv_sec += (time_t)(delay_ms / 1000); - resp->send_ts.tv_usec += usec; - if(resp->send_ts.tv_usec >= 1000000) { - 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; + return ts; } static int read_https_alpn_part(struct blob *b, struct Curl_str *str) @@ -580,6 +351,27 @@ static int read_https_alpn_part(struct blob *b, struct Curl_str *str) return 0; } +#ifdef _WIN32 +#define SENDTO3 int +#else +#define SENDTO3 size_t +#endif + +#define INSTRUCTIONS "dnsd.cmd" + +static curlx_struct_stat finfo_last; +static unsigned char ipv4_pref[4]; +static unsigned char ipv6_pref[16]; +static unsigned char ancount_a; +static unsigned char ancount_aaaa; + +static timediff_t a_delay_ms; +static timediff_t aaaa_delay_ms; +static timediff_t https_delay_ms; +static unsigned char rcode_a; +static unsigned char rcode_aaaa; +static struct blob httpsrr; + static int read_https_alpn(struct blob *b, const char **ps) { struct Curl_str word; @@ -618,7 +410,7 @@ static int read_https(struct blob *b, const char *s) curlx_str_passblanks(&s); if(curlx_str_word(&s, &word, UINT16_MAX)) { - logmsg("https: unable to read target qname, input=%s", s); + logmsg("[CONFIG] https: unable to read target qname, input=%s", s); return 1; } if(blob_add_qname(b, &word)) @@ -675,7 +467,7 @@ static void read_instructions(void) blob_reset(&httpsrr); finfo_last = finfo; - logmsg("read instructions from %s", file); + logmsg("[CONFIG] reading from %s", file); f = curlx_fopen(file, FOPEN_READTEXT); if(f) { char buf[256]; @@ -757,39 +549,804 @@ static void read_instructions(void) rc = buf[0] ? 0 : 1; } if(rc != 1) { - logmsg("Bad line in %s: '%s'", file, buf); + logmsg("[CONFIG] Bad line in %s: '%s'", file, buf); } else if(rtype) { - logmsg("added %s record via '%s'", rtype, buf); + logmsg("[CONFIG] 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", + logmsg("[CONFIG] 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 - logmsg("Error opening file '%s'", file); + logmsg("[CONFIG] Error opening file '%s'", file); +} + +static int last_query_id = -1; + +static int dnsd_make_answer(struct blob *blob, int query_id, + const uint8_t *qbuf, size_t qlen, + uint16_t qtype, uint16_t id, + timediff_t *pdelay_ms) +{ + int a; + char addrbuf[128]; /* IP address buffer */ + uint8_t header[12] = { + 0x80, 0xea, /* ID, overwrite */ + 0x81, 0x80, + /* Flags: 0x8180 Standard query response, No error + + 1... .... .... .... = Response: Message is a response + .000 0... .... .... = Opcode: Standard query (0) + .... .0.. .... .... = Authoritative: Server is not an authority for + domain + .... ..0. .... .... = Truncated: Message is not truncated + .... ...1 .... .... = Recursion desired: Do query recursively + .... .... 1... .... = Recursion available: Server can do recursive + queries + .... .... .0.. .... = Z: reserved (0) + .... .... ..0. .... = Answer authenticated: Answer/authority portion + was not authenticated by the server + .... .... ...0 .... = Non-authenticated data: Unacceptable + .... .... .... 0000 = Reply code: No error (0) + */ + 0x0, 0x1, /* QDCOUNT a single question */ + 0x0, 0x0, /* ANCOUNT number of answers */ + 0x0, 0x0, /* NSCOUNT */ + 0x0, 0x0 /* ARCOUNT */ + }; + uint16_t ancount = 0; + unsigned char rcode = 0; + + /* read once per incoming query, which is probably more than one + per test case */ + read_instructions(); + + switch(qtype) { + case QTYPE_A: + ancount = ancount_a; + *pdelay_ms = a_delay_ms; + rcode = rcode_a; + break; + case QTYPE_AAAA: + ancount = ancount_aaaa; + *pdelay_ms = aaaa_delay_ms; + rcode = rcode_aaaa; + break; + case QTYPE_HTTPS: + if(httpsrr.dlen) + ancount = 1; + *pdelay_ms = https_delay_ms; + break; + default: + *pdelay_ms = 0; + } + if(rcode) + ancount = 0; + + header[0] = (uint8_t)(id >> 8); + header[1] = (uint8_t)(id & 0xff); + + if(rcode) { + header[3] = (uint8_t)((header[3] & 0xf0) | (rcode & 0x0f)); + logmsg("[%d] response rcode %u", query_id, (unsigned int)rcode); + } + + header[6] = (uint8_t)(ancount >> 8); + header[7] = (uint8_t)(ancount & 0xff); + + if(blob_addn(blob, header, sizeof(header))) + return 1; + + if(blob_addn(blob, qbuf, qlen)) { + logmsg("unable to handle query of length %zu", qlen); + return 1; + } + + switch(qtype) { + case QTYPE_A: + for(a = 0; !rcode && (a < ancount_a); a++) { + const unsigned char *store = ipv4_pref; + const char *ip; + if(add_answer(blob, store, sizeof(ipv4_pref), QTYPE_A)) + return 1; + ip = curlx_inet_ntop(AF_INET, store, addrbuf, sizeof(addrbuf)); + logmsg("[%d] response A (%x) '%s'", query_id, (unsigned int)QTYPE_A, + ip ? ip : "(null)"); + } + if(!ancount_a) + logmsg("[%d] response A empty", query_id); + break; + case QTYPE_AAAA: + for(a = 0; !rcode && (a < ancount_aaaa); a++) { + const unsigned char *store = ipv6_pref; + const char *ip; + if(add_answer(blob, store, sizeof(ipv6_pref), QTYPE_AAAA)) + return 1; + ip = curlx_inet_ntop(AF_INET6, store, addrbuf, sizeof(addrbuf)); + logmsg("[%d] response AAAA (%x) '%s'", query_id, + (unsigned int)QTYPE_AAAA, ip ? ip : "(null)"); + } + if(!ancount_aaaa) + logmsg("[%d] response AAAA empty", query_id); + break; + case QTYPE_HTTPS: + if(httpsrr.dlen) { + if(add_answer(blob, httpsrr.data, httpsrr.dlen, QTYPE_HTTPS)) { + logmsg("[%d] error adding https %zu response bytes", query_id, + httpsrr.dlen); + return 1; + } + logmsg("[%d] response HTTPS (%x), %zu bytes", query_id, + (unsigned int)QTYPE_HTTPS, httpsrr.dlen); + } + else + logmsg("[%d] response HTTPS, no record", query_id); + break; + } + + return 0; +} + +struct udp_resp { + struct udp_resp *next; + int query_id; + struct curltime send_ts; + struct sockaddr addr; + curl_socklen_t addrlen; + struct blob body; +}; + +static struct udp_resp *udp_resp_queue; + +static CURLcode send_udp_resp(curl_socket_t sock, struct udp_resp *resp) +{ + ssize_t rc; + int sockerr = 0; + + do { + rc = sendto(sock, (const void *)resp->body.data, (SENDTO3)resp->body.dlen, + 0, &resp->addr, resp->addrlen); + } while((rc < 0) && ((sockerr = SOCKERRNO) == SOCKEINTR)); + if(rc < 0) { + char errbuf[STRERROR_LEN]; + logmsg("[%d-UDP] failed sending %zu bytes, error: (%d) %s", + resp->query_id, resp->body.dlen, + sockerr, curlx_strerror(sockerr, errbuf, sizeof(errbuf))); + return CURLE_SEND_ERROR; + } + else if(rc != (ssize_t)resp->body.dlen) { + logmsg("[%d-UDP] failed sending %zu bytes, sent: %zd", + resp->query_id, resp->body.dlen, rc); + return CURLE_SEND_ERROR; + } + logmsg("[%d-UDP] sent response", resp->query_id); + return CURLE_OK; +} + +static void queue_udp_resp(struct udp_resp *resp) +{ + struct udp_resp **panchor = &udp_resp_queue; + while(*panchor) { + timediff_t ms = curlx_ptimediff_ms(&(*panchor)->send_ts, &resp->send_ts); + if(ms > 0) /* resp is to be sent before *panchor */ + break; + panchor = &(*panchor)->next; + } + resp->next = *panchor; + *panchor = resp; +} + +static timediff_t queue_udp_next_ms(struct curltime *pnow) +{ + timediff_t next_ms = 1000; + struct udp_resp *r; + + for(r = udp_resp_queue; r; r = r->next) { + timediff_t ms = curlx_ptimediff_ms(&r->send_ts, pnow); + if((ms > 0) && (ms < next_ms)) + next_ms = ms; + else if(ms <= 0) + return 0; + } + return next_ms; +} + +static void queue_udp_send(curl_socket_t sock, struct curltime *pnow) +{ + struct udp_resp **panchor = &udp_resp_queue; + + while(*panchor) { + struct udp_resp *resp = *panchor; + timediff_t ms = curlx_ptimediff_ms(&resp->send_ts, pnow); + + /* if not due yet, break as response queue is time sorted */ + if(ms > 0) + break; + *panchor = resp->next; + send_udp_resp(sock, resp); + curlx_free(resp); + } +} + +static void queue_udp_clear(void) +{ + while(udp_resp_queue) { + struct udp_resp *resp = udp_resp_queue; + udp_resp_queue = resp->next; + curlx_free(resp); + } +} + +/* this is an answer to a question */ +static struct udp_resp * +udp_resp_create(int query_id, + const struct sockaddr *addr, curl_socklen_t addrlen, + const unsigned char *qbuf, size_t qlen, + uint16_t qtype, uint16_t id) +{ + struct udp_resp *resp; + timediff_t delay_ms = 0; + + resp = curlx_calloc(1, sizeof(*resp)); + if(!resp) + goto error; + + resp->query_id = query_id; + /* 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("[%d-UDP] unable to handle addrlen of %zu", + query_id, (size_t)addrlen); + goto error; + } + memcpy(&resp->addr, CURL_UNCONST(addr), addrlen); + resp->addrlen = addrlen; + + if(dnsd_make_answer(&resp->body, query_id, qbuf, qlen, qtype, id, &delay_ms)) + goto error; + + resp->send_ts = now_plus(delay_ms); + if(delay_ms > 0) + logmsg("[%d-UDP] delay response by %" FMT_TIMEDIFF_T "ms", + query_id, delay_ms); + return resp; + +error: + logmsg("[%d-UDP] failed to create response", query_id); + curlx_free(resp); + return NULL; +} + +static int udp_recv_req(curl_socket_t sock) +{ + srvr_sockaddr_union_t from; + curl_socklen_t fromlen; + uint8_t inbuffer[1500]; + uint8_t qbuf[256]; /* query storage */ + size_t qlen = 0; /* query size */ + struct udp_resp *resp; + uint16_t qtype = 0, id; + ssize_t n; + int result = 0; + + fromlen = sizeof(from); +#ifdef USE_IPV6 + if(socket_domain == AF_INET6) + fromlen = sizeof(from.sa6); + else +#endif + fromlen = sizeof(from.sa4); + + n = (ssize_t)recvfrom(sock, (char *)inbuffer, sizeof(inbuffer), 0, + &from.sa, &fromlen); + if(got_exit_signal) + goto out; + if(n < 0) { + logmsg("UDP, recvfrom error"); + result = 3; + goto out; + } + + ++last_query_id; + store_incoming("UDP", last_query_id, inbuffer, n, + qbuf, sizeof(qbuf), &qlen, &qtype, &id); + + set_advisor_read_lock(loglockfile); + serverlogslocked = 1; + + resp = udp_resp_create(last_query_id, &from.sa, fromlen, qbuf, + qlen, qtype, id); + if(!resp) + logmsg("[%d-UDP] error creating response", last_query_id); + else + queue_udp_resp(resp); + +out: + return result; +} + +#define MAX_DOH_CONNS 512 +#define MAX_DOH_INBUF_LEN (8 * 1024) +#define MAX_DOH_OUTBUF_LEN (8 * 1024) + +struct doh_conn { + curl_socket_t sock; + int index; + int query_id; + char inbuf[MAX_DOH_INBUF_LEN]; + size_t inblen; + size_t inbody_offset; + size_t inbody_len; + char outbuf[MAX_DOH_OUTBUF_LEN]; + size_t outblen; + struct curltime send_ts; + BIT(want_recv); + BIT(want_send); + BIT(close_pending); +}; + +static struct doh_conn doh_conns[MAX_DOH_CONNS]; + +static void doh_conns_init(void) +{ + int i; + for(i = 0; i < MAX_DOH_CONNS; ++i) { + doh_conns[i].sock = CURL_SOCKET_BAD; + doh_conns[i].index = i; + } +} + +static int doh_conns_add(curl_socket_t sock) +{ + int i; + for(i = 0; i < MAX_DOH_CONNS; ++i) { + if(doh_conns[i].sock == CURL_SOCKET_BAD) { + doh_conns[i].sock = sock; + doh_conns[i].want_recv = TRUE; + logmsg("[x-%d-DOH] accepted new connection, fd=%ld", i, (long)sock); + return 0; + } + } + logmsg("Too many open DoH connections, closing incoming."); + sclose(sock); + return 1; +} + +static void doh_conn_close(size_t i) +{ + if(i >= MAX_DOH_CONNS) + return; + if(doh_conns[i].sock != CURL_SOCKET_BAD) + sclose(doh_conns[i].sock); + doh_conns[i].sock = CURL_SOCKET_BAD; + doh_conns[i].want_recv = FALSE; + doh_conns[i].want_send = FALSE; + logmsg("[x-%d-DOH] connection closed", (int)i); +} + +static void doh_conns_close_all(void) +{ + size_t i; + for(i = 0; i < MAX_DOH_CONNS; ++i) { + doh_conn_close(i); + } +} + +static void doh_conns_fdsets(fd_set *readfds, fd_set *writefds, + struct curltime *pnow, + int *pmaxfd, + timediff_t *ptimeout_ms) +{ + size_t i; + for(i = 0; i < MAX_DOH_CONNS; ++i) { + struct doh_conn *c = &doh_conns[i]; + if((c->sock != CURL_SOCKET_BAD)) { + if(!c->want_send && c->outblen) { /* check delayed send */ + timediff_t ms = curlx_ptimediff_ms(&c->send_ts, pnow); + if(ms <= 0) + c->want_send = TRUE; + else if((ms < *ptimeout_ms) || !*ptimeout_ms) + *ptimeout_ms = ms; + } + if(c->want_send) + FD_SET(c->sock, writefds); + if(c->want_recv) + FD_SET(c->sock, readfds); + + if((FD_ISSET(c->sock, readfds) || FD_ISSET(c->sock, writefds)) && + (int)c->sock > *pmaxfd) + *pmaxfd = (int)c->sock; + } + } +} + +static int doh_conn_send(struct doh_conn *c, struct curltime *pnow) +{ + char errbuf[STRERROR_LEN]; + ssize_t rc; + int sockerr; + + if(c->outblen) { + timediff_t ms = curlx_ptimediff_ms(&c->send_ts, pnow); + size_t n; + + if(ms > 0) { /* not due yet */ + c->want_send = FALSE; + goto out; + } + + rc = swrite(c->sock, c->outbuf, c->outblen); + if(rc < 0) { + sockerr = SOCKERRNO; + if((sockerr == SOCKEINPROGRESS) || SOCK_EAGAIN(sockerr)) + return 0; + sockerr = SOCKERRNO; + logmsg("[%d-%d-DOH] swrite(%ld) failed with error (%d) %s", + c->query_id, c->index, (long)c->sock, + sockerr, curlx_strerror(sockerr, errbuf, sizeof(errbuf))); + return 1; + } + n = (size_t)rc; + if(n >= c->outblen) { /* all sent */ + c->outblen = 0; + logmsg("[%d-%d-DOH] last response byte sent", c->query_id, c->index); + } + else { + c->outblen -= n; + memmove(c->outbuf, c->outbuf + n, c->outblen); + } + } + +out: + if(!c->outblen) { + c->want_send = FALSE; + if(c->close_pending) + return 1; + else { + logmsg("[x-%d-DOH] switching to recv", c->index); + c->want_recv = TRUE; + } + } + return 0; +} + +static const char *http_status_descr(int http_status) +{ + switch(http_status) { + case 200: + return "Ok"; + case 400: + return "Bad Request"; + case 404: + return "Not Found"; + case 405: + return "Method Not Allowed"; + default: + if(http_status < 100) + return "This is wrong"; + if(http_status < 200) + return "Just Kidding"; + if(http_status < 300) + return "Lgtm"; + if(http_status < 400) + return "Follow Me For More Requests"; + if(http_status < 500) + return "You were wrong"; + return "We did something wrong"; + } +} + +static int doh_conn_send_err(struct doh_conn *c, int http_status) +{ + c->inblen = 0; + c->close_pending = TRUE; + c->want_recv = FALSE; + if(c->outblen) { + logmsg("[%d-%d-DOH] error sending response with %zu bytes still outgoing", + c->query_id, c->index, c->outblen); + return 1; + } + + memset(c->outbuf, 0, sizeof(c->outbuf)); + snprintf(c->outbuf, sizeof(c->outbuf) - 1, + "HTTP/1.1 %d %s\r\n" + "Content-Length: 0\r\n" + "Connection: close\r\n" + "\r\n", + http_status, http_status_descr(http_status)); + c->outblen = strlen(c->outbuf); + c->want_send = TRUE; + logmsg("[%d-%d-DOH] sending HTTP response %d", + c->query_id, c->index, http_status); + return 0; +} + +static int doh_conn_send_answer(struct doh_conn *c, struct blob *body, + timediff_t delay_ms) +{ + if(c->outblen) { /* Should not happen */ + logmsg("[%d-%d-DOH] trying to send an answer with outbuf still having " + "%zu bytes", c->query_id, c->index, c->outblen); + return 1; + } + memset(c->outbuf, 0, sizeof(c->outbuf)); + snprintf(c->outbuf, sizeof(c->outbuf) - 1, + "HTTP/1.1 200 %s\r\n" + "Server: curl/test-dnsd\r\n" + "Date: Thu, 06 Aug 2026 08:42:00 GMT\r\n" + "Content-Type: application/dns-message\r\n" + "Content-Length: %ld\r\n" + "\r\n", + http_status_descr(200), (long)body->dlen); + c->outblen = strlen(c->outbuf); + if((c->outblen + body->dlen) > sizeof(c->outbuf)) { + logmsg("[%d-%d-DOH] response size of %zu too large for outbuf", + c->query_id, c->index, c->outblen + body->dlen); + c->outblen = 0; + return 1; + } + memcpy(c->outbuf + c->outblen, body->data, body->dlen); + c->outblen += body->dlen; + c->send_ts = now_plus(delay_ms); + if(delay_ms > 0) + logmsg("[%d-%d-DOH] delay response by %" FMT_TIMEDIFF_T "ms", + c->query_id, c->index, delay_ms); + else + c->want_send = TRUE; + c->want_recv = FALSE; + logmsg("[%d-%d-DOH] sending HTTP response 200", + c->query_id, c->index); + return 0; +} + +static int doh_req_parse_headers(const char **pstr, + size_t *pcontent_length, + bool *pcomplete) +{ + static const struct Curl_str HD_content_length = { + STRCONST("Content-Length:") + }; + static const struct Curl_str HD_content_type = { + STRCONST("Content-Type:") + }; + static const struct Curl_str HD_wanted_type = { + STRCONST("application/dns-message") + }; + const char *p = *pstr; + bool ct_ok = FALSE; + bool cl_ok = FALSE; + bool eoh = FALSE; + + *pcomplete = FALSE; + *pcontent_length = 0; + while(p[0]) { + const char *nl, *start = p; + struct Curl_str hd_name, hd_val; + + if((p[0] == '\r') && (p[1] == '\n')) { + p += 2; + eoh = TRUE; + break; + } + nl = strchr(p, '\n'); + if(!nl) /* incomplete */ + break; + if(curlx_str_word(&p, &hd_name, 1024) || + curlx_str_singlespace(&p) || + curlx_str_untilnl(&p, &hd_val, 1024) || + curlx_str_newline(&p) || + curlx_str_newline(&p)) { + logmsg("unrecognized request header '%.*s'", + (int)(nl - start), start); + return 1; + } + if(curlx_str_case_equal(&HD_content_type, &hd_name)) { + if(!curlx_str_case_equal(&HD_wanted_type, &hd_val)) { + logmsg("wrong content-type: '%.*s'", (int)hd_val.len, hd_val.str); + return 1; + } + ct_ok = TRUE; + } + else if(curlx_str_case_equal(&HD_content_length, &hd_name)) { + const char *s = hd_val.str; + curl_off_t offt; + if(curlx_str_number(&s, &offt, 4096)) { + logmsg("wrong content-length: '%.*s'", (int)hd_val.len, hd_val.str); + return 1; + } + *pcontent_length = (size_t)offt; + cl_ok = TRUE; + } + else { + /* ignore this header */ + } + } + + *pstr = p; + if(!eoh) + return 0; /* need more */ + if(!ct_ok) { + logmsg("request missing Content-Type"); + return 1; + } + if(!cl_ok) { + logmsg("request missing Content-Length"); + return 1; + } + *pcomplete = TRUE; + return 0; +} + +static int doh_conn_do_req(struct doh_conn *c, bool eos) +{ + static const struct Curl_str DOH_PROTO = { STRCONST("HTTP/1.1") }; + static const struct Curl_str DOH_METHOD = { STRCONST("POST") }; + static const struct Curl_str DOH_PATH = { STRCONST("/") }; + const char *first_nl; + bool complete = FALSE; + + if(!c->inblen && eos) + return 1; + + if(!c->inbody_offset) { + first_nl = strchr(c->inbuf, '\n'); + if(first_nl) { + /* This is a poor man's HTTP/1.1 parser and we should rather have + * one in curlx that we can share. */ + const char *p = c->inbuf; + struct Curl_str method, path, proto; + + if(curlx_str_word(&p, &method, 1024) || curlx_str_singlespace(&p)) { + logmsg("[x-%d-DOH] unrecognized first request line method '%.*s'", + c->index, (int)(first_nl - c->inbuf), c->inbuf); + return 1; + } + if(curlx_str_word(&p, &path, 1024) || curlx_str_singlespace(&p)) { + logmsg("[x-%d-DOH] unrecognized first request line path '%.*s'", + c->index, (int)(first_nl - c->inbuf), c->inbuf); + return 1; + } + if(curlx_str_untilnl(&p, &proto, 1024) || + curlx_str_newline(&p) || + curlx_str_newline(&p)) { + logmsg("[x-%d-DOH] unrecognized first request line proto '%.*s'", + c->index, (int)(first_nl - c->inbuf), c->inbuf); + return 1; + } + if(!curlx_str_case_equal(&DOH_PROTO, &proto)) { + logmsg("[x-%d-DOH] unrecognized request protocol '%.*s'", + c->index, (int)proto.len, proto.str); + return 1; + } + if(!curlx_str_case_equal(&DOH_METHOD, &method)) { + logmsg("[x-%d-DOH] unsupported request method '%.*s'", + c->index, (int)method.len, method.str); + return doh_conn_send_err(c, 405); + } + if(!curlx_str_case_equal(&DOH_PATH, &path)) { + logmsg("[x-%d-DOH] request path not fond '%.*s'", + c->index, (int)path.len, path.str); + return doh_conn_send_err(c, 404); + } + if(doh_req_parse_headers(&p, &c->inbody_len, &complete)) { + return doh_conn_send_err(c, 400); + } + /* Looks ok, remember the start of the body bytes */ + c->inbody_offset = (p - c->inbuf); + } + if(!complete) + return eos ? 1 : 0; /* want more, error if client close */ + } + + if(c->inblen >= (c->inbody_offset + c->inbody_len)) { + /* We have all bytes for processing the request */ + uint8_t qbuf[256]; /* query storage */ + size_t qlen = 0; /* query size */ + size_t rlen = c->inbody_offset + c->inbody_len; /* request size */ + uint16_t qtype = 0, id; + struct blob blob; + timediff_t delay_ms; + + c->query_id = ++last_query_id; + if(store_incoming("DoH", c->query_id, (const uint8_t *)c->inbuf + + c->inbody_offset, c->inbody_len, + qbuf, sizeof(qbuf), &qlen, &qtype, &id)) { + logmsg("[%d-%d-DOH] error storing incoming request", + c->query_id, c->index); + return doh_conn_send_err(c, 400); + } + + /* remove handled request from inbuf */ + if(rlen >= c->inblen) + c->inblen = 0; + else { + memmove(c->inbuf, c->inbuf + rlen, c->inblen - rlen); + c->inblen -= rlen; + } + c->inbody_len = c->inbody_offset = 0; + + memset(&blob, 0, sizeof(blob)); + if(dnsd_make_answer(&blob, c->query_id, qbuf, qlen, qtype, id, + &delay_ms)) { + return doh_conn_send_err(c, 500); + } + return doh_conn_send_answer(c, &blob, delay_ms); + } + return 0; +} + +static int doh_conn_recv(struct doh_conn *c) +{ + char errbuf[STRERROR_LEN]; + ssize_t n; + int sockerr; + + n = sread(c->sock, c->inbuf + c->inblen, sizeof(c->inbuf) - 1 - c->inblen); + if(n < 0) { + sockerr = SOCKERRNO; + if((sockerr == SOCKEINPROGRESS) || SOCK_EAGAIN(sockerr)) + return 0; + sockerr = SOCKERRNO; + logmsg("[x-%d-DOH] sread() failed with error (%d) %s", c->index, + sockerr, curlx_strerror(sockerr, errbuf, sizeof(errbuf))); + return 1; + } + else if(n == 0) { + logmsg("[x-%d-DOH] sread() == 0, client closed connection", c->index); + return doh_conn_do_req(c, TRUE); + } + else { + c->inblen += (size_t)n; + c->inbuf[c->inblen] = 0; + logmsg("[x-%d-DOH] sread() == %zu, processing", c->index, (size_t)n); + return doh_conn_do_req(c, FALSE); + } +} + +static int doh_conns_serve(fd_set *readfds, fd_set *writefds) +{ + struct curltime now; + size_t i; + + for(i = 0; i < MAX_DOH_CONNS; ++i) { + struct doh_conn *c = &doh_conns[i]; + if(c->sock != CURL_SOCKET_BAD) { + if(c->want_send && FD_ISSET(c->sock, writefds)) { + now = curlx_now(); + if(doh_conn_send(c, &now)) { + doh_conn_close(i); + continue; + } + } + if(c->want_recv && FD_ISSET(c->sock, readfds)) { + if(doh_conn_recv(c)) { + doh_conn_close(i); + continue; + } + } + } + } + return 0; } static int test_dnsd(int argc, const char **argv) { - srvr_sockaddr_union_t me; - ssize_t n = 0; int arg = 1; - curl_socket_t sock = CURL_SOCKET_BAD; - int flag; - int rc; - int sockerr; + curl_socket_t sock_udp = CURL_SOCKET_BAD; + curl_socket_t sock_tcp_listen = CURL_SOCKET_BAD; char errbuf[STRERROR_LEN]; + int rc, sockerr; int result = 0; - struct resp *resp; pidname = ".dnsd.pid"; serverlogfile = "log/dnsd.log"; serverlogslocked = 0; server_port = 9123; /* UDP */ + socket_domain = AF_INET; while(argc > arg) { const char *opt; @@ -870,97 +1427,14 @@ static int test_dnsd(int argc, const char **argv) install_signal_handlers(FALSE); -#ifdef USE_IPV6 - if(socket_domain == AF_INET6) - sock = socket(AF_INET6, SOCK_DGRAM, 0); - else -#endif - sock = socket(AF_INET, SOCK_DGRAM, 0); - - if(sock == CURL_SOCKET_BAD) { - sockerr = SOCKERRNO; - logmsg("Error creating socket (%d) %s", - sockerr, curlx_strerror(sockerr, errbuf, sizeof(errbuf))); - result = 1; + result = open_stream_sock(&sock_tcp_listen, &server_port); + if(result) goto dnsd_cleanup; - } + doh_conns_init(); - flag = 1; - if(setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (void *)&flag, sizeof(flag))) { - sockerr = SOCKERRNO; - logmsg("setsockopt(SO_REUSEADDR) failed with error (%d) %s", - sockerr, curlx_strerror(sockerr, errbuf, sizeof(errbuf))); - result = 1; + result = open_udp_sock(&sock_udp, &server_port); + if(result) goto dnsd_cleanup; - } - -#ifdef USE_IPV6 - if(socket_domain == AF_INET6) { - memset(&me.sa6, 0, sizeof(me.sa6)); - me.sa6.sin6_family = AF_INET6; - me.sa6.sin6_addr = in6addr_any; - me.sa6.sin6_port = htons(server_port); - rc = bind(sock, &me.sa, sizeof(me.sa6)); - } - else -#endif - { - memset(&me.sa4, 0, sizeof(me.sa4)); - me.sa4.sin_family = AF_INET; - me.sa4.sin_addr.s_addr = INADDR_ANY; - me.sa4.sin_port = htons(server_port); - rc = bind(sock, &me.sa, sizeof(me.sa4)); - } - if(rc) { - sockerr = SOCKERRNO; - logmsg("Error binding socket on port %hu (%d) %s", server_port, - sockerr, curlx_strerror(sockerr, errbuf, sizeof(errbuf))); - result = 1; - goto dnsd_cleanup; - } - - if(!server_port) { - /* The system was supposed to choose a port number, figure out which - port we actually got and update the listener port value with it. */ - curl_socklen_t la_size; - srvr_sockaddr_union_t localaddr; - memset(&localaddr, 0, sizeof(localaddr)); -#ifdef USE_IPV6 - if(socket_domain == AF_INET6) - la_size = sizeof(localaddr.sa6); - else -#endif - la_size = sizeof(localaddr.sa4); - - if(getsockname(sock, &localaddr.sa, &la_size) < 0) { - sockerr = SOCKERRNO; - logmsg("getsockname() failed with error (%d) %s", - sockerr, curlx_strerror(sockerr, errbuf, sizeof(errbuf))); - sclose(sock); - goto dnsd_cleanup; - } - switch(localaddr.sa.sa_family) { - case AF_INET: - server_port = ntohs(localaddr.sa4.sin_port); - break; -#ifdef USE_IPV6 - case AF_INET6: - server_port = ntohs(localaddr.sa6.sin6_port); - break; -#endif - default: - break; - } - if(!server_port) { - /* Real failure, listener port shall not be zero beyond this point. */ - logmsg("Apparently getsockname() succeeded, with listener port zero."); - logmsg("A valid reason for this failure is a binary built without"); - logmsg("proper network library linkage. This might not be the only"); - logmsg("reason, but double check it before anything else."); - result = 2; - goto dnsd_cleanup; - } - } dnsd_wrotepidfile = write_pidfile(pidname); if(!dnsd_wrotepidfile) { @@ -976,75 +1450,76 @@ static int test_dnsd(int argc, const char **argv) } } - logmsg("Running %s version on port UDP/%d", socket_type, (int)server_port); - curlx_nonblock(sock, TRUE); + /* start accepting connections */ + if(listen(sock_tcp_listen, 50)) { + sockerr = SOCKERRNO; + logmsg("listen() failed with error (%d) %s", + sockerr, curlx_strerror(sockerr, errbuf, sizeof(errbuf))); + result = 1; + goto dnsd_cleanup; + } + + logmsg("Running %s on port UDP+TCP/%u", socket_type, server_port); + curlx_nonblock(sock_udp, TRUE); + curlx_nonblock(sock_tcp_listen, TRUE); for(;;) { - uint16_t id = 0; - uint8_t inbuffer[1500]; - srvr_sockaddr_union_t from; - curl_socklen_t fromlen; - uint8_t qbuf[256]; /* query storage */ - size_t qlen = 0; /* query size */ - uint16_t qtype = 0; timediff_t timeout_ms = 0; - fromlen = sizeof(from); -#ifdef USE_IPV6 - if(socket_domain == AF_INET6) - fromlen = sizeof(from.sa6); - else -#endif - fromlen = sizeof(from.sa4); + fd_set readfds, writefds; + struct timeval tv; + int maxfd = 0; + struct curltime now = curlx_now(); - timeout_ms = send_resp_queue(sock); + FD_ZERO(&readfds); + fdset_add_sock(&readfds, sock_udp, &maxfd); + fdset_add_sock(&readfds, sock_tcp_listen, &maxfd); - { - fd_set readfds; - struct timeval tv; - int maxfd = (int)sock; + FD_ZERO(&writefds); + timeout_ms = queue_udp_next_ms(&now); + if(!timeout_ms) + fdset_add_sock(&writefds, sock_udp, &maxfd); - FD_ZERO(&readfds); - FD_SET(sock, &readfds); - if(!timeout_ms || (timeout_ms > 100)) - timeout_ms = 100; + doh_conns_fdsets(&readfds, &writefds, &now, &maxfd, &timeout_ms); - rc = select(maxfd + 1, &readfds, NULL, NULL, - curlx_mstotv(&tv, timeout_ms)); + if(!timeout_ms || (timeout_ms > 100)) + timeout_ms = 100; - if(rc == -1) { - logmsg("error %d returned by select()", SOCKERRNO); - } - else if(!rc) { /* timeout */ - continue; - } + rc = select(maxfd + 1, &readfds, &writefds, NULL, + curlx_mstotv(&tv, timeout_ms)); + + if(rc == -1) { + logmsg("error %d returned by select()", SOCKERRNO); } - n = (ssize_t)recvfrom(sock, (char *)inbuffer, sizeof(inbuffer), 0, - &from.sa, &fromlen); - if(got_exit_signal) - break; - if(n < 0) { - logmsg("recvfrom"); - result = 3; - break; + else if(!rc) { /* timeout */ + continue; } - /* read once per incoming query, which is probably more than one - per test case */ - read_instructions(); + if(FD_ISSET(sock_udp, &writefds)) { + now = curlx_now(); + queue_udp_send(sock_udp, &now); + } + if(FD_ISSET(sock_udp, &readfds)) { + result = udp_recv_req(sock_udp); + if(result) + break; + } - ++query_id; - store_incoming(query_id, inbuffer, n, - qbuf, sizeof(qbuf), &qlen, &qtype, &id); + result = doh_conns_serve(&readfds, &writefds); + if(result) + break; - set_advisor_read_lock(loglockfile); - serverlogslocked = 1; - - resp = create_resp(query_id, &from.sa, fromlen, qbuf, - qlen, qtype, id); - if(!resp) - logmsg("error creating response"); - else - queue_resp(resp); + if(FD_ISSET(sock_tcp_listen, &readfds)) { + /* Service all queued connections */ + curl_socket_t sock_conn; + while(TRUE) { + sock_conn = accept_connection(sock_tcp_listen); + if(!sock_conn) /* no more connections to accept */ + break; + if(sock_conn == CURL_SOCKET_BAD) + goto dnsd_cleanup; + doh_conns_add(sock_conn); + } + } if(got_exit_signal) break; @@ -1056,14 +1531,12 @@ static int test_dnsd(int argc, const char **argv) } dnsd_cleanup: + if(sock_udp != CURL_SOCKET_BAD) + sclose(sock_udp); + if(sock_tcp_listen != CURL_SOCKET_BAD) + sclose(sock_tcp_listen); -#if 0 - if((peer != sock) && (peer != CURL_SOCKET_BAD)) - sclose(peer); -#endif - - if(sock != CURL_SOCKET_BAD) - sclose(sock); + doh_conns_close_all(); if(got_exit_signal) logmsg("signalled to die"); @@ -1078,7 +1551,7 @@ dnsd_cleanup: clear_advisor_read_lock(loglockfile); } - clear_resp_queue(); + queue_udp_clear(); restore_signal_handlers(FALSE); diff --git a/tests/server/first.h b/tests/server/first.h index aa81a5798e..1de9af74b7 100644 --- a/tests/server/first.h +++ b/tests/server/first.h @@ -159,6 +159,14 @@ extern curl_socket_t sockdaemon(curl_socket_t sock, uint16_t *listenport, const char *unix_socket, bool bind_only); +extern int open_udp_sock(curl_socket_t *psock, uint16_t *pport); +extern int open_stream_sock(curl_socket_t *psock, uint16_t *pport); +extern curl_socket_t accept_connection(curl_socket_t listen_sock); +extern bool curlx_str_case_equal(const struct Curl_str *s1, + const struct Curl_str *s2); + +/* returns true if the current socket is an IP one */ +extern bool socket_domain_is_ip(void); /* global variables */ static const char *srcpath = "."; /* pointing to the test directory */ diff --git a/tests/server/sws.c b/tests/server/sws.c index 6c18b9a399..5d7087927d 100644 --- a/tests/server/sws.c +++ b/tests/server/sws.c @@ -189,23 +189,6 @@ static char *data_to_hex(const char *data, size_t len) /* work around for handling trailing headers */ static int already_recv_zeroed_chunk = FALSE; -#if defined(TCP_NODELAY) && defined(CURL_TCP_NODELAY_SUPPORTED) -/* returns true if the current socket is an IP one */ -static bool socket_domain_is_ip(void) -{ - switch(socket_domain) { - case AF_INET: -#ifdef USE_IPV6 - case AF_INET6: -#endif - return TRUE; - default: - /* case AF_UNIX: */ - return FALSE; - } -} -#endif - /* parse the file on disk that might have a test number for us */ static int parse_cmdfile(struct sws_httprequest *req) { @@ -1749,92 +1732,6 @@ static void http_upgrade(struct sws_httprequest *req) /* left to implement */ } -/* returns a socket handle, or 0 if there are no more waiting sockets, - or < 0 if there was an error */ -static curl_socket_t accept_connection(curl_socket_t sock) -{ - curl_socket_t msgsock = CURL_SOCKET_BAD; - int sockerr; - char errbuf[STRERROR_LEN]; - int flag = 1; - - if(MAX_SOCKETS == num_sockets) { - logmsg("Too many open sockets!"); - return CURL_SOCKET_BAD; - } - - msgsock = accept(sock, NULL, NULL); - - if(got_exit_signal) { - if(msgsock != CURL_SOCKET_BAD) - sclose(msgsock); - return CURL_SOCKET_BAD; - } - - if(msgsock == CURL_SOCKET_BAD) { - sockerr = SOCKERRNO; - if(SOCK_EAGAIN(sockerr)) { - /* nothing to accept */ - return 0; - } - logmsg("MAJOR ERROR, accept() failed with error (%d) %s", - sockerr, curlx_strerror(sockerr, errbuf, sizeof(errbuf))); - return CURL_SOCKET_BAD; - } - - if(curlx_nonblock(msgsock, TRUE)) { - sockerr = SOCKERRNO; - logmsg("curlx_nonblock failed with error (%d) %s", - sockerr, curlx_strerror(sockerr, errbuf, sizeof(errbuf))); - sclose(msgsock); - return CURL_SOCKET_BAD; - } - -#if defined(_WIN32) && defined(USE_UNIX_SOCKETS) - if(socket_domain != AF_UNIX) { -#endif - if(setsockopt(msgsock, SOL_SOCKET, SO_KEEPALIVE, - (void *)&flag, sizeof(flag))) { - sockerr = SOCKERRNO; - logmsg("setsockopt(SO_KEEPALIVE) failed with error (%d) %s", - sockerr, curlx_strerror(sockerr, errbuf, sizeof(errbuf))); - sclose(msgsock); - return CURL_SOCKET_BAD; - } -#if defined(_WIN32) && defined(USE_UNIX_SOCKETS) - } -#endif - - /* - * As soon as this server accepts a connection from the test harness it - * must set the server logs advisor read lock to indicate that server - * logs should not be read until this lock is removed by this server. - */ - - if(!serverlogslocked) - set_advisor_read_lock(loglockfile); - serverlogslocked += 1; - - logmsg("====> Client connect"); - - all_sockets[num_sockets] = msgsock; - num_sockets += 1; - -#if defined(TCP_NODELAY) && defined(CURL_TCP_NODELAY_SUPPORTED) - if(socket_domain_is_ip()) { - /* - * Disable the Nagle algorithm to make it easier to send out a large - * response in many small segments to torture the clients more. - */ - if(setsockopt(msgsock, IPPROTO_TCP, TCP_NODELAY, - (void *)&flag, sizeof(flag))) - logmsg("====> TCP_NODELAY failed"); - } -#endif - - return msgsock; -} - /* returns 1 if the connection should be serviced again immediately, 0 if there is no data waiting, or < 0 if it should be closed */ static int service_connection(curl_socket_t *msgsock, @@ -1914,11 +1811,9 @@ static int service_connection(curl_socket_t *msgsock, static int test_sws(int argc, const char *argv[]) { - srvr_sockaddr_union_t me; curl_socket_t sock = CURL_SOCKET_BAD; int wrotepidfile = 0; int wroteportfile = 0; - int flag; #ifdef USE_UNIX_SOCKETS bool unlink_socket = FALSE; #endif @@ -1932,6 +1827,7 @@ static int test_sws(int argc, const char *argv[]) const char *location_str = port_str; int keepalive_secs = 5; const char *protocol_type = "HTTP"; + int result = 0; /* a default CONNECT port is pointless, but still ... */ size_t socket_idx; @@ -2005,6 +1901,7 @@ static int test_sws(int argc, const char *argv[]) arg++; if(argc > arg) { #ifdef USE_UNIX_SOCKETS + srvr_sockaddr_union_t me; server_unix_socket = argv[arg]; if(strlen(server_unix_socket) >= sizeof(me.sau.sun_path)) { fprintf(stderr, @@ -2091,115 +1988,13 @@ static int test_sws(int argc, const char *argv[]) if(!req) goto sws_cleanup; - sock = socket(socket_domain, SOCK_STREAM, 0); + result = open_stream_sock(&sock, &server_port); + if(result) + goto sws_cleanup; all_sockets[0] = sock; num_sockets = 1; - if(sock == CURL_SOCKET_BAD) { - sockerr = SOCKERRNO; - logmsg("Error creating socket (%d) %s", - sockerr, curlx_strerror(sockerr, errbuf, sizeof(errbuf))); - goto sws_cleanup; - } - -#if defined(_WIN32) && defined(USE_UNIX_SOCKETS) - if(socket_domain != AF_UNIX) { -#endif - flag = 1; - if(setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, - (void *)&flag, sizeof(flag))) { - sockerr = SOCKERRNO; - logmsg("setsockopt(SO_REUSEADDR) failed with error (%d) %s", - sockerr, curlx_strerror(sockerr, errbuf, sizeof(errbuf))); - goto sws_cleanup; - } -#if defined(_WIN32) && defined(USE_UNIX_SOCKETS) - } -#endif - if(curlx_nonblock(sock, TRUE)) { - sockerr = SOCKERRNO; - logmsg("curlx_nonblock failed with error (%d) %s", - sockerr, curlx_strerror(sockerr, errbuf, sizeof(errbuf))); - goto sws_cleanup; - } - - switch(socket_domain) { - case AF_INET: - memset(&me.sa4, 0, sizeof(me.sa4)); - me.sa4.sin_family = AF_INET; - me.sa4.sin_addr.s_addr = INADDR_ANY; - me.sa4.sin_port = htons(server_port); - rc = bind(sock, &me.sa, sizeof(me.sa4)); - break; -#ifdef USE_IPV6 - case AF_INET6: - memset(&me.sa6, 0, sizeof(me.sa6)); - me.sa6.sin6_family = AF_INET6; - me.sa6.sin6_addr = in6addr_any; - me.sa6.sin6_port = htons(server_port); - rc = bind(sock, &me.sa, sizeof(me.sa6)); - break; -#endif /* USE_IPV6 */ -#ifdef USE_UNIX_SOCKETS - case AF_UNIX: - rc = bind_unix_socket(sock, server_unix_socket, &me.sau); -#endif /* USE_UNIX_SOCKETS */ - } - if(rc) { - sockerr = SOCKERRNO; -#ifdef USE_UNIX_SOCKETS - if(socket_domain == AF_UNIX) - logmsg("Error binding socket on path %s (%d) %s", server_unix_socket, - sockerr, curlx_strerror(sockerr, errbuf, sizeof(errbuf))); - else -#endif - logmsg("Error binding socket on port %hu (%d) %s", server_port, - sockerr, curlx_strerror(sockerr, errbuf, sizeof(errbuf))); - goto sws_cleanup; - } - - if(!server_port) { - /* The system was supposed to choose a port number, figure out which - port we actually got and update the listener port value with it. */ - curl_socklen_t la_size; - srvr_sockaddr_union_t localaddr; - memset(&localaddr, 0, sizeof(localaddr)); -#ifdef USE_IPV6 - if(socket_domain == AF_INET6) - la_size = sizeof(localaddr.sa6); - else -#endif - la_size = sizeof(localaddr.sa4); - if(getsockname(sock, &localaddr.sa, &la_size) < 0) { - sockerr = SOCKERRNO; - logmsg("getsockname() failed with error (%d) %s", - sockerr, curlx_strerror(sockerr, errbuf, sizeof(errbuf))); - sclose(sock); - goto sws_cleanup; - } - switch(localaddr.sa.sa_family) { - case AF_INET: - server_port = ntohs(localaddr.sa4.sin_port); - break; -#ifdef USE_IPV6 - case AF_INET6: - server_port = ntohs(localaddr.sa6.sin6_port); - break; -#endif - default: - break; - } - if(!server_port) { - /* Real failure, listener port shall not be zero beyond this point. */ - logmsg("Apparently getsockname() succeeded, with listener port zero."); - logmsg("A valid reason for this failure is a binary built without"); - logmsg("proper network library linkage. This might not be the only"); - logmsg("reason, but double check it before anything else."); - sclose(sock); - goto sws_cleanup; - } - } #ifdef USE_UNIX_SOCKETS if(socket_domain != AF_UNIX) #endif @@ -2302,11 +2097,19 @@ static int test_sws(int argc, const char *argv[]) /* Service all queued connections */ curl_socket_t msgsock; do { + if(MAX_SOCKETS == num_sockets) { + logmsg("Too many open sockets!"); + goto sws_cleanup; + } msgsock = accept_connection(sock); + if(!msgsock) + break; logmsg("accept_connection %ld returned %ld", (long)sock, (long)msgsock); if(msgsock == CURL_SOCKET_BAD) goto sws_cleanup; + all_sockets[num_sockets] = msgsock; + num_sockets += 1; if(req->delay) curlx_wait_ms(req->delay); } while(msgsock > 0); @@ -2414,5 +2217,5 @@ sws_cleanup: restore_signal_handlers(FALSE); - return 0; + return result; } diff --git a/tests/server/util.c b/tests/server/util.c index 34d9a68587..c2e1d52f60 100644 --- a/tests/server/util.c +++ b/tests/server/util.c @@ -886,3 +886,342 @@ curl_socket_t sockdaemon(curl_socket_t sock, return sock; } + +int open_udp_sock(curl_socket_t *psock, uint16_t *pport) +{ + srvr_sockaddr_union_t me; + curl_socket_t sock = CURL_SOCKET_BAD; + uint16_t port = *pport; + int sockerr, flag, rc; + char errbuf[STRERROR_LEN]; + int result = 0; + + sock = socket(socket_domain, SOCK_DGRAM, 0); + + if(sock == CURL_SOCKET_BAD) { + sockerr = SOCKERRNO; + logmsg("Error creating socket (%d) %s", + sockerr, curlx_strerror(sockerr, errbuf, sizeof(errbuf))); + result = 1; + goto out; + } + + flag = 1; + if(setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (void *)&flag, sizeof(flag))) { + sockerr = SOCKERRNO; + logmsg("setsockopt(SO_REUSEADDR) failed with error (%d) %s", + sockerr, curlx_strerror(sockerr, errbuf, sizeof(errbuf))); + result = 1; + goto out; + } + +#ifdef USE_IPV6 + if(socket_domain == AF_INET6) { + memset(&me.sa6, 0, sizeof(me.sa6)); + me.sa6.sin6_family = AF_INET6; + me.sa6.sin6_addr = in6addr_any; + me.sa6.sin6_port = htons(port); + rc = bind(sock, &me.sa, sizeof(me.sa6)); + } + else +#endif + { + memset(&me.sa4, 0, sizeof(me.sa4)); + me.sa4.sin_family = AF_INET; + me.sa4.sin_addr.s_addr = INADDR_ANY; + me.sa4.sin_port = htons(port); + rc = bind(sock, &me.sa, sizeof(me.sa4)); + } + if(rc) { + sockerr = SOCKERRNO; + logmsg("Error binding socket on port %hu (%d) %s", port, + sockerr, curlx_strerror(sockerr, errbuf, sizeof(errbuf))); + result = 1; + goto out; + } + + if(!port) { + /* The system was supposed to choose a port number, figure out which + port we actually got and update the listener port value with it. */ + curl_socklen_t la_size; + srvr_sockaddr_union_t localaddr; + memset(&localaddr, 0, sizeof(localaddr)); +#ifdef USE_IPV6 + if(socket_domain == AF_INET6) + la_size = sizeof(localaddr.sa6); + else +#endif + la_size = sizeof(localaddr.sa4); + if(getsockname(sock, &localaddr.sa, &la_size) < 0) { + sockerr = SOCKERRNO; + logmsg("getsockname() failed with error (%d) %s", + sockerr, curlx_strerror(sockerr, errbuf, sizeof(errbuf))); + result = 1; + goto out; + } + switch(localaddr.sa.sa_family) { + case AF_INET: + port = ntohs(localaddr.sa4.sin_port); + break; +#ifdef USE_IPV6 + case AF_INET6: + port = ntohs(localaddr.sa6.sin6_port); + break; +#endif + default: + break; + } + if(!port) { + /* Real failure, listener port shall not be zero beyond this point. */ + logmsg("Apparently getsockname() succeeded, with listener port zero."); + logmsg("A valid reason for this failure is a binary built without"); + logmsg("proper network library linkage. This might not be the only"); + logmsg("reason, but double check it before anything else."); + result = 2; + goto out; + } + } + +out: + if(result) { + if(sock != CURL_SOCKET_BAD) + sclose(sock); + sock = CURL_SOCKET_BAD; + port = 0; + } + *psock = sock; + *pport = port; + return result; +} + +bool socket_domain_is_ip(void) +{ + switch(socket_domain) { + case AF_INET: +#ifdef USE_IPV6 + case AF_INET6: +#endif + return TRUE; + default: + /* case AF_UNIX: */ + return FALSE; + } +} + +int open_stream_sock(curl_socket_t *psock, uint16_t *pport) +{ + srvr_sockaddr_union_t me; + char errbuf[STRERROR_LEN]; + int flag, sockerr, rc = 0; + curl_socket_t sock = CURL_SOCKET_BAD; + uint16_t port = *pport; + int result = 0; + + sock = socket(socket_domain, SOCK_STREAM, 0); + + if(sock == CURL_SOCKET_BAD) { + sockerr = SOCKERRNO; + logmsg("Error creating socket (%d) %s", + sockerr, curlx_strerror(sockerr, errbuf, sizeof(errbuf))); + result = 1; + goto out; + } + +#if defined(_WIN32) && defined(USE_UNIX_SOCKETS) + if(socket_domain != AF_UNIX) { +#endif + flag = 1; + if(setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, + (void *)&flag, sizeof(flag))) { + sockerr = SOCKERRNO; + logmsg("setsockopt(SO_REUSEADDR) failed with error (%d) %s", + sockerr, curlx_strerror(sockerr, errbuf, sizeof(errbuf))); + result = 1; + goto out; + } +#if defined(_WIN32) && defined(USE_UNIX_SOCKETS) + } +#endif + if(curlx_nonblock(sock, TRUE)) { + sockerr = SOCKERRNO; + logmsg("curlx_nonblock failed with error (%d) %s", + sockerr, curlx_strerror(sockerr, errbuf, sizeof(errbuf))); + result = 1; + goto out; + } + + switch(socket_domain) { + case AF_INET: + memset(&me.sa4, 0, sizeof(me.sa4)); + me.sa4.sin_family = AF_INET; + me.sa4.sin_addr.s_addr = INADDR_ANY; + me.sa4.sin_port = htons(port); + rc = bind(sock, &me.sa, sizeof(me.sa4)); + break; +#ifdef USE_IPV6 + case AF_INET6: + memset(&me.sa6, 0, sizeof(me.sa6)); + me.sa6.sin6_family = AF_INET6; + me.sa6.sin6_addr = in6addr_any; + me.sa6.sin6_port = htons(port); + rc = bind(sock, &me.sa, sizeof(me.sa6)); + break; +#endif /* USE_IPV6 */ +#ifdef USE_UNIX_SOCKETS + case AF_UNIX: + rc = bind_unix_socket(sock, server_unix_socket, &me.sau); +#endif /* USE_UNIX_SOCKETS */ + } + if(rc) { + sockerr = SOCKERRNO; +#ifdef USE_UNIX_SOCKETS + if(socket_domain == AF_UNIX) + logmsg("Error binding socket on path %s (%d) %s", server_unix_socket, + sockerr, curlx_strerror(sockerr, errbuf, sizeof(errbuf))); + else +#endif + logmsg("Error binding socket on port %hu (%d) %s", port, + sockerr, curlx_strerror(sockerr, errbuf, sizeof(errbuf))); + result = 1; + goto out; + } + + if(!port) { + /* The system was supposed to choose a port number, figure out which + port we actually got and update the listener port value with it. */ + curl_socklen_t la_size; + srvr_sockaddr_union_t localaddr; + memset(&localaddr, 0, sizeof(localaddr)); +#ifdef USE_IPV6 + if(socket_domain != AF_INET6) +#endif + la_size = sizeof(localaddr.sa4); +#ifdef USE_IPV6 + else + la_size = sizeof(localaddr.sa6); +#endif + if(getsockname(sock, &localaddr.sa, &la_size) < 0) { + sockerr = SOCKERRNO; + logmsg("getsockname() failed with error (%d) %s", + sockerr, curlx_strerror(sockerr, errbuf, sizeof(errbuf))); + result = 1; + goto out; + } + switch(localaddr.sa.sa_family) { + case AF_INET: + port = ntohs(localaddr.sa4.sin_port); + break; +#ifdef USE_IPV6 + case AF_INET6: + port = ntohs(localaddr.sa6.sin6_port); + break; +#endif + default: + break; + } + if(!port) { + /* Real failure, listener port shall not be zero beyond this point. */ + logmsg("Apparently getsockname() succeeded, with listener port zero."); + logmsg("A valid reason for this failure is a binary built without"); + logmsg("proper network library linkage. This might not be the only"); + logmsg("reason, but double check it before anything else."); + result = 1; + goto out; + } + } +out: + if(result) { + if(sock != CURL_SOCKET_BAD) + sclose(sock); + sock = CURL_SOCKET_BAD; + port = 0; + } + *psock = sock; + *pport = port; + return result; +} + +/* returns a socket handle, or 0 if there are no more waiting sockets, + or < 0 if there was an error */ +curl_socket_t accept_connection(curl_socket_t listen_sock) +{ + curl_socket_t msgsock = CURL_SOCKET_BAD; + int sockerr; + char errbuf[STRERROR_LEN]; + int flag = 1; + + msgsock = accept(listen_sock, NULL, NULL); + + if(got_exit_signal) { + if(msgsock != CURL_SOCKET_BAD) + sclose(msgsock); + return CURL_SOCKET_BAD; + } + + if(msgsock == CURL_SOCKET_BAD) { + sockerr = SOCKERRNO; + if(SOCK_EAGAIN(sockerr)) { + /* nothing to accept */ + return 0; + } + logmsg("MAJOR ERROR, accept() failed with error (%d) %s", + sockerr, curlx_strerror(sockerr, errbuf, sizeof(errbuf))); + return CURL_SOCKET_BAD; + } + + if(curlx_nonblock(msgsock, TRUE)) { + sockerr = SOCKERRNO; + logmsg("curlx_nonblock failed with error (%d) %s", + sockerr, curlx_strerror(sockerr, errbuf, sizeof(errbuf))); + sclose(msgsock); + return CURL_SOCKET_BAD; + } + +#if defined(_WIN32) && defined(USE_UNIX_SOCKETS) + if(socket_domain != AF_UNIX) { +#endif + if(setsockopt(msgsock, SOL_SOCKET, SO_KEEPALIVE, + (void *)&flag, sizeof(flag))) { + sockerr = SOCKERRNO; + logmsg("setsockopt(SO_KEEPALIVE) failed with error (%d) %s", + sockerr, curlx_strerror(sockerr, errbuf, sizeof(errbuf))); + sclose(msgsock); + return CURL_SOCKET_BAD; + } +#if defined(_WIN32) && defined(USE_UNIX_SOCKETS) + } +#endif + + /* + * As soon as this server accepts a connection from the test harness it + * must set the server logs advisor read lock to indicate that server + * logs should not be read until this lock is removed by this server. + */ + + if(!serverlogslocked) + set_advisor_read_lock(loglockfile); + serverlogslocked += 1; + + logmsg("====> Client connect"); + +#if defined(TCP_NODELAY) && defined(CURL_TCP_NODELAY_SUPPORTED) + if(socket_domain_is_ip()) { + /* + * Disable the Nagle algorithm to make it easier to send out a large + * response in many small segments to torture the clients more. + */ + if(setsockopt(msgsock, IPPROTO_TCP, TCP_NODELAY, + (void *)&flag, sizeof(flag))) + logmsg("====> TCP_NODELAY failed"); + } +#endif + + return msgsock; +} + +bool curlx_str_case_equal(const struct Curl_str *s1, + const struct Curl_str *s2) +{ + return ((s1->len == s2->len) && + !CURL_STRNICMP(s1->str, s2->str, s1->len)); +}