diff --git a/src/resolve/meson.build b/src/resolve/meson.build index eeaea3411f..24f94f8c0e 100644 --- a/src/resolve/meson.build +++ b/src/resolve/meson.build @@ -70,7 +70,6 @@ systemd_resolved_sources = files(''' resolved-socket-graveyard.h resolved-varlink.c resolved-varlink.h - resolved.c '''.split()) resolvectl_sources = files(''' @@ -200,6 +199,14 @@ tests += [ [lib_openssl_or_gcrypt, libm]], + [files('test-resolved-stream.c') + + basic_dns_sources + systemd_resolved_sources, + [libshared], + [lib_openssl_or_gcrypt, + libm] + + systemd_resolved_dependencies, + resolve_includes], + [files('test-dnssec.c'), [libsystemd_resolve_core, libshared], @@ -229,3 +236,5 @@ fuzzers += [ [lib_openssl_or_gcrypt, libm]], ] + +systemd_resolved_sources += files('resolved.c') diff --git a/src/resolve/test-resolved-stream.c b/src/resolve/test-resolved-stream.c new file mode 100644 index 0000000000..fd7ade19d1 --- /dev/null +++ b/src/resolve/test-resolved-stream.c @@ -0,0 +1,342 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "fd-util.h" +#include "log.h" +#include "process-util.h" +#include "resolved-dns-packet.h" +#include "resolved-dns-question.h" +#include "resolved-dns-rr.h" +#if ENABLE_DNS_OVER_TLS +#include "resolved-dnstls.h" +#endif +#include "resolved-dns-server.h" +#include "resolved-dns-stream.h" +#include "resolved-manager.h" +#include "sd-event.h" +#include "sparse-endian.h" +#include "tests.h" + +static struct sockaddr_in SERVER_ADDRESS; + +/* Bytes of the questions & answers used in the test, including TCP DNS 2-byte length prefix */ +static const uint8_t QUESTION_A[] = { + 0x00, 0x1D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 'e', + 'x' , 'a' , 'm' , 'p' , 'l' , 'e' , 0x03, 'c' , 'o' , 'm' , 0x00, 0x00, 0x01, 0x00, 0x01 +}; +static const uint8_t QUESTION_AAAA[] = { + 0x00, 0x1D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 'e', + 'x' , 'a' , 'm' , 'p' , 'l' , 'e' , 0x03, 'c' , 'o' , 'm' , 0x00, 0x00, 0x1C, 0x00, 0x01 +}; +static const uint8_t ANSWER_A[] = { + 0x00, 0x2D, 0x00, 0x00, 0x81, 0x80, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x07, 'e', + 'x' , 'a' , 'm' , 'p' , 'l' , 'e' , 0x03, 'c' , 'o' , 'm' , 0x00, 0x00, 0x01, 0x00, 0x01, 0xC0, + 0x0C, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x52, 0x8D, 0x00, 0x04, 0x5D, 0xB8, 0xD8, 0x22, +}; +static const uint8_t ANSWER_AAAA[] = { + 0x00, 0x39, 0x00, 0x00, 0x81, 0x80, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x07, 'e', + 'x' , 'a' , 'm' , 'p' , 'l' , 'e' , 0x03, 'c' , 'o' , 'm' , 0x00, 0x00, 0x1C, 0x00, 0x01, 0xC0, + 0x0C, 0x00, 0x1C, 0x00, 0x01, 0x00, 0x00, 0x54, 0x4B, 0x00, 0x10, 0x26, 0x06, 0x28, 0x00, 0x02, + 0x20, 0x00, 0x01, 0x02, 0x48, 0x18, 0x93, 0x25, 0xC8, 0x19, 0x46, +}; + +/** + * A mock TCP DNS server that asserts certain questions are received + * and replies with the same answer every time. + */ +static void receive_and_check_question(int fd, const uint8_t *expected_question, + size_t question_size) { + uint8_t *actual_question; + size_t n_read = 0; + + actual_question = newa(uint8_t, question_size); + while (n_read < question_size) { + ssize_t r = read(fd, actual_question + n_read, question_size - n_read); + assert_se(r >= 0); + n_read += (size_t)r; + } + assert_se(n_read == question_size); + + assert_se(memcmp(expected_question, actual_question, question_size) == 0); +} + +static void send_answer(int fd, const uint8_t *answer, size_t answer_size) { + assert_se(write(fd, answer, answer_size) == (ssize_t)answer_size); +} + +/* Sends two answers together in a single write operation, + * so they hopefully end up in a single TCP packet / TLS record */ +static void send_answers_together(int fd, + const uint8_t *answer1, size_t answer1_size, + const uint8_t *answer2, size_t answer2_size) { + uint8_t *answer; + size_t answer_size = answer1_size + answer2_size; + + answer = newa(uint8_t, answer_size); + memcpy(answer, answer1, answer1_size); + memcpy(answer + answer1_size, answer2, answer2_size); + assert_se(write(fd, answer, answer_size) == (ssize_t)answer_size); +} + +static void server_handle(int fd) { + receive_and_check_question(fd, QUESTION_A, sizeof(QUESTION_A)); + send_answer(fd, ANSWER_A, sizeof(ANSWER_A)); + + receive_and_check_question(fd, QUESTION_AAAA, sizeof(QUESTION_AAAA)); + send_answer(fd, ANSWER_AAAA, sizeof(ANSWER_AAAA)); + + receive_and_check_question(fd, QUESTION_A, sizeof(QUESTION_A)); + receive_and_check_question(fd, QUESTION_AAAA, sizeof(QUESTION_AAAA)); + send_answers_together(fd, ANSWER_A, sizeof(ANSWER_A), + ANSWER_AAAA, sizeof(ANSWER_AAAA)); +} + +static void *tcp_dns_server(void *p) { + _cleanup_close_ int bindfd = -1, acceptfd = -1; + + assert_se((bindfd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0)) >= 0); + assert_se(setsockopt(bindfd, SOL_SOCKET, SO_REUSEADDR, &(int){1}, sizeof(int)) >= 0); + assert_se(bind(bindfd, &SERVER_ADDRESS, sizeof(SERVER_ADDRESS)) >= 0); + assert_se(listen(bindfd, 1) >= 0); + assert_se((acceptfd = accept(bindfd, NULL, NULL)) >= 0); + server_handle(acceptfd); + return NULL; +} + +#if ENABLE_DNS_OVER_TLS +/* + * Spawns a DNS TLS server using the command line "openssl s_server" tool. + */ +static void *tls_dns_server(void *p) { + pid_t openssl_pid; + int r; + _cleanup_close_ int fd_server = -1, fd_tls = -1; + _cleanup_free_ char *cert_path = NULL, *key_path = NULL; + _cleanup_free_ char *ip_str = NULL, *bind_str = NULL; + + assert_se(get_testdata_dir("test-resolve/selfsigned.cert", &cert_path) >= 0); + assert_se(get_testdata_dir("test-resolve/selfsigned.key", &key_path) >= 0); + + assert_se(in_addr_to_string(SERVER_ADDRESS.sin_family, + &(union in_addr_union){.in = SERVER_ADDRESS.sin_addr}, + &ip_str) >= 0); + asprintf(&bind_str, "%s:%d", ip_str, be16toh(SERVER_ADDRESS.sin_port)); + + /* We will hook one of the socketpair ends to OpenSSL's TLS server + * stdin/stdout, so we will be able to read and write plaintext + * from the other end's file descriptor like an usual TCP server */ + { + int fd[2]; + assert_se(socketpair(AF_UNIX, SOCK_STREAM, 0, fd) >= 0); + fd_server = fd[0]; + fd_tls = fd[1]; + } + + r = safe_fork_full("(test-resolved-stream-tls-openssl)", (int[]) { fd_server, fd_tls }, 2, + FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG|FORK_LOG|FORK_REOPEN_LOG, &openssl_pid); + assert(r >= 0); + if (r == 0) { + /* Child */ + assert_se(dup2(fd_tls, STDIN_FILENO) >= 0); + assert_se(dup2(fd_tls, STDOUT_FILENO) >= 0); + close(TAKE_FD(fd_server)); + close(TAKE_FD(fd_tls)); + + execlp("openssl", "openssl", "s_server", "-accept", bind_str, + "-key", key_path, "-cert", cert_path, + "-quiet", "-naccept", "1", NULL); + log_error("exec failed, is something wrong with the 'openssl' command?"); + _exit(EXIT_FAILURE); + } else { + pthread_mutex_t *server_lock = (pthread_mutex_t *)p; + + server_handle(fd_server); + + /* Once the test is done kill the TLS server to release the port */ + assert_se(pthread_mutex_lock(server_lock) == 0); + assert_se(kill(openssl_pid, SIGTERM) >= 0); + assert_se(waitpid(openssl_pid, NULL, 0) >= 0); + assert_se(pthread_mutex_unlock(server_lock) == 0); + } + + return NULL; +} +#endif + +static const char *TEST_DOMAIN = "example.com"; +static const uint64_t EVENT_TIMEOUT_USEC = 5 * 1000 * 1000; + +static void send_simple_question(DnsStream *stream, uint16_t type) { + _cleanup_(dns_packet_unrefp) DnsPacket *p = NULL; + _cleanup_(dns_resource_key_unrefp) DnsResourceKey *key = NULL; + _cleanup_(dns_question_unrefp) DnsQuestion *question = NULL; + + assert_se(dns_packet_new(&p, DNS_PROTOCOL_DNS, 0, DNS_PACKET_SIZE_MAX) >= 0); + assert_se(question = dns_question_new(1)); + assert_se(key = dns_resource_key_new(DNS_CLASS_IN, type, TEST_DOMAIN)); + assert_se(dns_question_add(question, key, 0) >= 0); + assert_se(dns_packet_append_question(p, question) >= 0); + DNS_PACKET_HEADER(p)->qdcount = htobe16(dns_question_size(question)); + assert_se(dns_stream_write_packet(stream, p) >= 0); +} + +static const size_t MAX_RECEIVED_PACKETS = 2; +static DnsPacket *received_packets[2] = {}; +static size_t n_received_packets = 0; + +static int on_stream_packet(DnsStream *stream) { + assert_se(n_received_packets < MAX_RECEIVED_PACKETS); + assert_se(received_packets[n_received_packets++] = dns_stream_take_read_packet(stream)); + return 0; +} + +static void test_dns_stream(bool tls) { + Manager manager = {}; + _cleanup_(dns_stream_unrefp) DnsStream *stream = NULL; + _cleanup_(sd_event_unrefp) sd_event *event = NULL; + _cleanup_close_ int clientfd = -1; + int r; + + void *(*server_entrypoint)(void *); + pthread_t server_thread; + pthread_mutex_t server_lock; + + log_info("test-resolved-stream: Started %s test", tls ? "TLS" : "TCP"); + +#if ENABLE_DNS_OVER_TLS + if (tls) { + /* For TLS mode, use DNS_OVER_TLS_OPPORTUNISTIC instead of + * DNS_OVER_TLS_YES, just to make certificate validation more + * lenient, allowing us to use self-signed certificates. + * We never downgrade, everything we test always goes over TLS */ + manager.dns_over_tls_mode = DNS_OVER_TLS_OPPORTUNISTIC; + } +#endif + + assert_se(sd_event_new(&event) >= 0); + manager.event = event; + + /* Set up a mock DNS (over TCP or TLS) server */ + server_entrypoint = tcp_dns_server; +#if ENABLE_DNS_OVER_TLS + if (tls) + server_entrypoint = tls_dns_server; +#endif + assert_se(pthread_mutex_init(&server_lock, NULL) == 0); + assert_se(pthread_mutex_lock(&server_lock) == 0); + assert_se(pthread_create(&server_thread, NULL, server_entrypoint, &server_lock) == 0); + + /* Create a socket client and connect to the TCP or TLS server + * The server may not be up immediately, so try to connect a few times before failing */ + assert_se((clientfd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0)) >= 0); + + for (int i = 0; i < 100; i++) { + r = connect(clientfd, &SERVER_ADDRESS, sizeof(SERVER_ADDRESS)); + if (r >= 0) + break; + usleep(EVENT_TIMEOUT_USEC / 100); + } + assert_se(r >= 0); + + /* systemd-resolved uses (and requires) the socket to be in nonblocking mode */ + assert_se(fcntl(clientfd, F_SETFL, O_NONBLOCK) >= 0); + + /* Initialize DNS stream */ + assert_se(dns_stream_new(&manager, &stream, DNS_STREAM_LOOKUP, DNS_PROTOCOL_DNS, + TAKE_FD(clientfd), NULL, DNS_STREAM_DEFAULT_TIMEOUT_USEC) >= 0); + stream->on_packet = on_stream_packet; +#if ENABLE_DNS_OVER_TLS + if (tls) { + DnsServer server = { + .manager = &manager, + .family = SERVER_ADDRESS.sin_family, + .address.in = SERVER_ADDRESS.sin_addr + }; + + assert_se(dnstls_manager_init(&manager) >= 0); + assert_se(dnstls_stream_connect_tls(stream, &server) >= 0); + } +#endif + + /* Test: Question of type A and associated answer */ + log_info("test-resolved-stream: A record"); + send_simple_question(stream, DNS_TYPE_A); + while (n_received_packets != 1) + assert_se(sd_event_run(event, EVENT_TIMEOUT_USEC) >= 1); + assert_se(DNS_PACKET_DATA(received_packets[0])); + assert_se(memcmp(DNS_PACKET_DATA(received_packets[0]), + ANSWER_A + 2, sizeof(ANSWER_A) - 2) == 0); + dns_packet_unref(TAKE_PTR(received_packets[0])); + n_received_packets = 0; + + /* Test: Question of type AAAA and associated answer */ + log_info("test-resolved-stream: AAAA record"); + send_simple_question(stream, DNS_TYPE_AAAA); + while (n_received_packets != 1) + assert_se(sd_event_run(event, EVENT_TIMEOUT_USEC) >= 1); + assert_se(DNS_PACKET_DATA(received_packets[0])); + assert_se(memcmp(DNS_PACKET_DATA(received_packets[0]), + ANSWER_AAAA + 2, sizeof(ANSWER_AAAA) - 2) == 0); + dns_packet_unref(TAKE_PTR(received_packets[0])); + n_received_packets = 0; + + /* Test: Question of type A and AAAA and associated answers + * Both answers are sent back in a single packet or TLS record + * (tests the fix of PR #22132: "Fix DoT timeout on multiple answer records") */ + log_info("test-resolved-stream: A + AAAA record"); + send_simple_question(stream, DNS_TYPE_A); + send_simple_question(stream, DNS_TYPE_AAAA); + + while (n_received_packets != 2) + assert_se(sd_event_run(event, EVENT_TIMEOUT_USEC) >= 1); + assert_se(DNS_PACKET_DATA(received_packets[0])); + assert_se(DNS_PACKET_DATA(received_packets[1])); + assert_se(memcmp(DNS_PACKET_DATA(received_packets[0]), + ANSWER_A + 2, sizeof(ANSWER_A) - 2) == 0); + assert_se(memcmp(DNS_PACKET_DATA(received_packets[1]), + ANSWER_AAAA + 2, sizeof(ANSWER_AAAA) - 2) == 0); + dns_packet_unref(TAKE_PTR(received_packets[0])); + dns_packet_unref(TAKE_PTR(received_packets[1])); + n_received_packets = 0; + +#if ENABLE_DNS_OVER_TLS + if (tls) + dnstls_manager_free(&manager); +#endif + + /* Stop the DNS server */ + assert_se(pthread_mutex_unlock(&server_lock) == 0); + assert_se(pthread_join(server_thread, NULL) == 0); + assert_se(pthread_mutex_destroy(&server_lock) == 0); + + log_info("test-resolved-stream: Finished %s test", tls ? "TLS" : "TCP"); +} + +int main(int argc, char **argv) { + SERVER_ADDRESS = (struct sockaddr_in) { + .sin_family = AF_INET, + .sin_port = htobe16(12345), + .sin_addr.s_addr = htobe32(INADDR_LOOPBACK) + }; + + test_setup_logging(LOG_DEBUG); + + test_dns_stream(false); +#if ENABLE_DNS_OVER_TLS + if (system("openssl version >/dev/null 2>&1") != 0) + return log_tests_skipped("Skipping TLS test since the 'openssl' command does not seem to be available"); + test_dns_stream(true); +#endif + + return 0; +} diff --git a/test/test-resolve/selfsigned.cert b/test/test-resolve/selfsigned.cert new file mode 100644 index 0000000000..456862c220 --- /dev/null +++ b/test/test-resolve/selfsigned.cert @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIIFmzCCA4OgAwIBAgIUZlvbV3+2YGjHJDTW+u0XL/ypvsowDQYJKoZIhvcNAQEL +BQAwXDELMAkGA1UEBhMCVVMxDzANBgNVBAgMBkRlbmlhbDEUMBIGA1UEBwwLU3By +aW5nZmllbGQxDDAKBgNVBAoMA0RpczEYMBYGA1UEAwwPd3d3LmV4YW1wbGUuY29t +MCAXDTIyMDEyMzE0MjYyMloYDzMwMjEwNTI2MTQyNjIyWjBcMQswCQYDVQQGEwJV +UzEPMA0GA1UECAwGRGVuaWFsMRQwEgYDVQQHDAtTcHJpbmdmaWVsZDEMMAoGA1UE +CgwDRGlzMRgwFgYDVQQDDA93d3cuZXhhbXBsZS5jb20wggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQC14826tEEHKICM/AKOKsyBUDaa6Z6KqS927ifb43LJ +fxtg8vW+vX9OGtje2qVAoI1UMSu4yttItSd9cjrnAFLPFvQGC8dFhn436ehLWiKP +AP5KvhIQ3equ5fTicn+Hxdm7C3Um2SEEE347I/vArfzuNE7PIJ57sh7KeBHGCrU3 +6iPl1DkkUilbqJAcgFoepozx1SbPq4h8LsdqJDKg+XUtvtuUS850D5Hb5ErTc8NL +VrA+urSIr+yIX4jAeLXbktNLGuAc3+cJTjwuWdDvP51yS0qpj459dFaRWQE9gu0b +DkRwYLF7Mpel66B8TBkHAWBhSs+oLNnv//Zv945wbUkTK29N2VtSEI4pd/47nTX9 +MwGn4q/ZAjhI7JUN3LcRDsrLdVAUabbK/U+xkL/lOlRRBK/1iuLELkaJlMUiuqZh +q3DvNjqeT5yY8GTU5iXoBcvY0lac3+zYaemTgD5cZfF4gpTflGfc5Gf+he6U3Dol +TT+4JfMrw0YdbqsH4oyEtmLBfMvvp+PQysiOELSFbSAphZOOcy8QSzoRrniNynPd +kM7kIM+0t2XUaz0lKtNuZSo9DnhTMvTLPnnbk5aJt5nPxPprcdqhcJhrHw7gVhBo +EceYJmXGiJJMLYuBNymZ4u7YrBg0e0qO+Fi9a4Kfh/QNMq/6VrpWvycb9LtCLhU+ +qQIDAQABo1MwUTAdBgNVHQ4EFgQU3ugK1HtfPaq90JC5Qf5ekrn4uUcwHwYDVR0j +BBgwFoAU3ugK1HtfPaq90JC5Qf5ekrn4uUcwDwYDVR0TAQH/BAUwAwEB/zANBgkq +hkiG9w0BAQsFAAOCAgEATzNvQP+VNLY0qK5b/8j8C5PHB0WKsTHSVNOhITr0Bq67 +AeOq5Mh2qZvJd/QAPTb/Cg60802RLQxp6uhCcfMdxTXkz1mxq6dKEeDAu/zAZzXk +WSpJl/VORnibjvXf2OS6ucb4KPOxfkYiD328CdSYBJapmQbnUmwZph2SO0bpY7K3 +EbTY9fIyabGMrjbXL5EGRvqA0NSnJHVUYx1h3b32PYKHrQKu6syCE4OrMY0yjdLH +1WnHeC3iB02AFy7TTfmeUiMTxaiPXAPjBDDIQtv1GHt8GR7WHSD3seIhAu6Lzbyr +0zrxk52C9v0YP1lgOwnvmQfbUSpWc29yhrXFkqkZToqbmYjNO55gPN8JA/2GrWan +s8gQwQ8z+yWAqNJQA5S+9+hNlBlcq659gCjIxoyCmkol4EepwR1WWdZjs2I00FHk +mQL1ig6H81rg/Bh2SraKR1tGdmjCNFi4RfWHsxCBcd1cGFeUIN+ygNmjXmzXJDFP +5vUXL9J5iu+WD1rnwB2gPRSvZUrmKUZnOGk0/kt1RpgbcFdOza+6vZmB51fXZYpD +YyvXHTbuHVOyXA160/Fmg6BNy5BfrTuXaZ3YVeZmvDf+ywVl2BFDQZDoLLQMIHzl +L2DdMuhVmgITqx8ZtrSxqBxW0DQXFZiMT+sv81+o2SO5nDzSYjoXfQv/Xkgpx44= +-----END CERTIFICATE----- diff --git a/test/test-resolve/selfsigned.key b/test/test-resolve/selfsigned.key new file mode 100644 index 0000000000..44a09829ef --- /dev/null +++ b/test/test-resolve/selfsigned.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +VULYeUwXocBKk6rBSgDlsFF5LW22 +-----END PRIVATE KEY-----