* [musl] Adding dns/resolver tests to libc-test
@ 2024-08-18 2:03 Rich Felker
2024-08-18 9:38 ` Szabolcs Nagy
0 siblings, 1 reply; 7+ messages in thread
From: Rich Felker @ 2024-08-18 2:03 UTC (permalink / raw)
To: musl
[-- Attachment #1: Type: text/plain, Size: 2547 bytes --]
I've been working on a framework to allow testing of libc resolver/dns
functionality in libc-test, on Linux-based hosts, provided they have
user-namespace functionality. The intent is that these tests would be
made conditional on __linux__ or similar, with the freedom to add
equivalent setup for other systems in the future if desired.
I'm attaching a semi-polished (fallback and error handling should be
mostly right) helper module that sets up the namespaced environment
for the test in-process. The intended usage pattern would be putting a
number of resolver tests in a single file, which would first call the
enter_dns_test_ns() setup function, then sequentially run each test.
For tests looking for crashes from malformed responses, it would make
sense to fork a child for the test to run in, and wait in the parent.
Each test would:
1. Truncate and rewrite /etc/resolv.conf and /etc/hosts (these are
virtual replacements which only appear in the namespace) with
desired contents. This would involve putting "nameserver 127.0.0.1"
(or ::1 to test IPv6 nameserver support) in resolv.conf, and may
also need options to disable functionality like edns0 that might
not match the test payload.
2. Create a thread to listen on localhost:53 and respond with the
packet contents to test the resolver against. Normally these would
be hard-coded packets except for rewriting the query id to match
(or mismatch, if testing rejection of mismatched id) the query, and
selecting a reply based on the requested RR type. Should listen on
both UDP and TCP unless testing the behavior when one or the other
is not available.
3. From the main thread, call getaddrinfo, getnameinfo, get*by*[_r],
res_query, res_send, etc. as desired to test against the payload.
4. Evaluate that the results match the expectation.
A smart direction to go from here, I think, would be looking back over
DNS-related bugs and limitations (like CNAME chain length) we've had
in the past and collecting or constructing test payloads to trigger
them.
It should be possible to capture responses from the real world with a
simple program using res_query, and incorporate them into tests. If
doing so, my leaning would be to anonymize the actual contents
(replace domains with example.com, IPs with private-use ranges), but
maybe that doesn't matter? Using the arpa/nameser.h interfaces in libc
it's probably easy enough to find all the data one may want to replace
in a packet and replace it.
Anyone up for fleshing some of this out?
Rich
[-- Attachment #2: unshare-ns.c --]
[-- Type: text/plain, Size: 3136 bytes --]
#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <sched.h>
#include <sys/mount.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <net/if.h>
#include <netinet/in.h>
#include <string.h>
#include <dirent.h>
#include <sys/stat.h>
static int symlink_clone_dir(const char *pathname)
{
int ret = -1;
char tmp[] = "/tmp/cloneXXXXXX";
char buf[sizeof tmp + 100];
if (!mkdtemp(tmp)) return -1;
if (mount("none", tmp, "tmpfs", 0, "mode=0755") < 0)
goto out1;
int fd = open(tmp, O_RDONLY|O_DIRECTORY|O_CLOEXEC);
if (fd < 0) goto out2;
if (mkdirat(fd, ".orig", 0700) < 0)
goto out3;
snprintf(buf, sizeof buf, "%s/%s", tmp, ".orig");
if (mount(pathname, buf, 0, MS_BIND, 0) < 0)
goto out3;
DIR *dir = opendir(buf);
if (!dir) goto out3;
struct dirent *de;
while ((de = readdir(dir))) {
char lnk[sizeof ".orig/" + strlen(de->d_name)];
snprintf(lnk, sizeof lnk, ".orig/%s", de->d_name);
symlinkat(lnk, fd, de->d_name);
}
closedir(dir);
if (mount(tmp, pathname, 0, MS_MOVE, 0) < 0)
goto out3;
ret = 0;
out3:
close(fd);
out2:
if (ret) umount2(tmp, MNT_DETACH);
out1:
rmdir(tmp);
return ret;
}
static int make_empty(const char *pathname)
{
int fd = open(pathname, O_RDWR|O_CREAT|O_EXCL|O_CLOEXEC, 0644);
if (fd < 0) return -1;
close(fd);
return 0;
}
static int bindover(const char *pathname)
{
char tmp[] = "/tmp/bindoverXXXXXX";
int fd = mkostemp(tmp, O_CLOEXEC);
if (fd < 0) return -1;
int r = mount(tmp, pathname, 0, MS_BIND, 0);
unlink(tmp);
close(fd);
return r;
}
static int writeproc(const char *pathname, const char *contents)
{
int fd = open(pathname, O_RDWR|O_CLOEXEC);
if (fd < 0) return -1;
if (write(fd, contents, strlen(contents)) != strlen(contents))
return -1;
close(fd);
return 0;
}
static int new_user_ns(int flags)
{
uid_t uid = getuid();
gid_t gid = getgid();
if (unshare(CLONE_NEWUSER|flags) < 0)
return -1;
char buf[3*sizeof(uintmax_t) + 10];
snprintf(buf, sizeof buf, "0 %ju 1", (uintmax_t)uid);
if (writeproc("/proc/self/uid_map", buf) < 0)
return -1;
if (writeproc("/proc/self/setgroups", "deny") < 0)
return -1;
snprintf(buf, sizeof buf, "0 %ju 1", (uintmax_t)gid);
if (writeproc("/proc/self/gid_map", buf) < 0)
return -1;
return 0;
}
int enter_dns_test_ns()
{
if (new_user_ns(CLONE_NEWNS|CLONE_NEWNET) < 0) return -1;
int s = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
if (s < 0) return -1;
struct ifreq ifr = { .ifr_name = "lo", };
if (ioctl(s, SIOCGIFFLAGS, &ifr) < 0) return -1;
ifr.ifr_flags |= IFF_UP;
if (ioctl(s, SIOCSIFFLAGS, &ifr) < 0) return -1;
close(s);
if (access("/etc/resolv.conf", F_OK) || access("/etc/hosts", F_OK)) {
if (symlink_clone_dir("/etc") < 0)
return -1;
unlink("/etc/resolv.conf");
unlink("/etc/hosts");
if (make_empty("/etc/resolv.conf") || make_empty("/etc/hosts"))
return -1;
} else {
if (bindover("/etc/resolv.conf"))
return -1;
if (bindover("/etc/hosts"))
return -1;
}
return 0;
}
int main(int argc, char **argv)
{
if (enter_dns_test_ns()) {
perror(0);
return 1;
}
if (argc>1) execv(argv[1], argv+1);
}
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [musl] Adding dns/resolver tests to libc-test
2024-08-18 2:03 [musl] Adding dns/resolver tests to libc-test Rich Felker
@ 2024-08-18 9:38 ` Szabolcs Nagy
2024-08-27 15:30 ` Ryan Ward
0 siblings, 1 reply; 7+ messages in thread
From: Szabolcs Nagy @ 2024-08-18 9:38 UTC (permalink / raw)
To: Rich Felker; +Cc: musl
* Rich Felker <dalias@libc.org> [2024-08-17 22:03:28 -0400]:
> I've been working on a framework to allow testing of libc resolver/dns
> functionality in libc-test, on Linux-based hosts, provided they have
> user-namespace functionality. The intent is that these tests would be
> made conditional on __linux__ or similar, with the freedom to add
> equivalent setup for other systems in the future if desired.
thanks. im travelling now will look at it in a few days
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [musl] Adding dns/resolver tests to libc-test
2024-08-18 9:38 ` Szabolcs Nagy
@ 2024-08-27 15:30 ` Ryan Ward
2024-08-27 21:34 ` Rich Felker
0 siblings, 1 reply; 7+ messages in thread
From: Ryan Ward @ 2024-08-27 15:30 UTC (permalink / raw)
To: Szabolcs Nagy, Rich Felker; +Cc: musl
[-- Attachment #1.1: Type: text/plain, Size: 1286 bytes --]
Hello,
I have made an attempt at fleshing out the resolver tests, please see the file attached. It still needs some polishing, and I am keen to keep building out the necessary tests, attached is a simple rudimentary res_query() test. I need to improve the packet and RR parsing, but sending this out to get some early feedback before I build out more tests.
I have just been building the test and the unshare-ns.c framework with the libc-test build system, executing unshare-ns.exe and passing in resolv_query.exe as intended.
Thanks,
rw
________________________________
From: Szabolcs Nagy <nsz@port70.net>
Sent: Sunday, 18 August 2024 9:38 AM
To: Rich Felker <dalias@libc.org>
Cc: musl@lists.openwall.com <musl@lists.openwall.com>
Subject: Re: [musl] Adding dns/resolver tests to libc-test
* Rich Felker <dalias@libc.org> [2024-08-17 22:03:28 -0400]:
> I've been working on a framework to allow testing of libc resolver/dns
> functionality in libc-test, on Linux-based hosts, provided they have
> user-namespace functionality. The intent is that these tests would be
> made conditional on __linux__ or similar, with the freedom to add
> equivalent setup for other systems in the future if desired.
thanks. im travelling now will look at it in a few days
[-- Attachment #1.2: Type: text/html, Size: 3689 bytes --]
[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: resolv_query.c --]
[-- Type: text/x-csrc; name="resolv_query.c", Size: 4297 bytes --]
#include <stdio.h>
#include <unistd.h>
#include <sched.h>
#include <sys/mount.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <sys/time.h>
#include <net/if.h>
#include <netinet/in.h>
#include <string.h>
#include <dirent.h>
#include <sys/stat.h>
#include <pthread.h>
#include <resolv.h>
#include <netdb.h>
#include "test.h"
#define TEST(c, ...) \
( (c) || (t_error(#c " failed: " __VA_ARGS__),0) )
// Example IP
#define EXPECTED_IP_ADDRESS "192.168.17.1"
// Simple answer resource record
#define EXAMPLE_ANSWER_RR "\xc0\x0c" \
"\x00\x01" \
"\x00\x01" \
"\x00\x00\x02\x58" \
"\x00\x04" \
"\xc0\xa8\x11\x01" // IPv4 192.168.17.1
// Wait until serving thread is ready to receive
pthread_barrier_t sync_barrier;
static size_t construct_response(uint16_t id, unsigned char *question, unsigned char *response)
{
HEADER dns_header;
const size_t dns_header_size = sizeof(dns_header);
const size_t expected_question_size = 17U;
memset(&dns_header, 0, dns_header_size);
dns_header.id = id;
dns_header.qr = 0x01U;
dns_header.rd = 0x01U;
dns_header.ra = 0x01U;
dns_header.qdcount = 0x0100U; // 1 question
dns_header.ancount = 0x0100U; // 1 answer
memcpy(response, &dns_header, sizeof(dns_header));
memcpy(&response[dns_header_size], &question[dns_header_size], expected_question_size);
unsigned char answer_buffer[] = EXAMPLE_ANSWER_RR;
memcpy(&response[dns_header_size + expected_question_size], &answer_buffer[0], sizeof(answer_buffer) - 1);
return dns_header_size + expected_question_size + sizeof(answer_buffer) - 1; //ignore null terminator
}
static int bind_to_socket(int s)
{
struct sockaddr_in dns_server;
memset(&dns_server, 0, sizeof(dns_server));
dns_server.sin_addr.s_addr = inet_addr("127.0.0.1");
dns_server.sin_family = AF_INET;
dns_server.sin_port = htons(53);
return bind(s, (struct sockaddr*)&dns_server, sizeof(dns_server));
}
static void set_environment(void)
{
FILE *ft = fopen("/etc/resolv.conf", "w");
if (ft == NULL) t_error("unable to open namespaced resolv.conf\n");
fprintf(ft, "nameserver 127.0.0.1");
fclose(ft);
ft = fopen("/etc/hosts", "w");
if (ft == NULL) t_error("unable to open namespaced resolv.conf\n");
fprintf(ft, "127.0.0.1 localhost");
fclose(ft);
}
void *dns_server(void *arguments)
{
int s = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
int status = bind_to_socket(s);
struct sockaddr_in from = {0};
socklen_t from_length = sizeof(from);
unsigned char packet_buffer[NS_PACKETSZ];
pthread_barrier_wait(&sync_barrier);
int packet_size = recvfrom(s, packet_buffer, NS_PACKETSZ, 0,
(struct sockaddr*)&from, &from_length);
unsigned char response_buffer[NS_PACKETSZ] = {0};
const uint16_t response_id = (packet_buffer[1] << 8) | packet_buffer[0];
size_t response_size = construct_response(response_id, packet_buffer, response_buffer);
status = sendto(s, response_buffer, response_size, 0, (struct sockaddr*)&from, from_length);
return 0;
}
int main(int argc, char **argv)
{
pthread_barrier_init(&sync_barrier, NULL, 2);
set_environment();
pthread_t dns_thread;
int status = pthread_create(&dns_thread, 0, dns_server, 0);
pthread_barrier_wait(&sync_barrier);
unsigned char res_buffer[NS_PACKETSZ] = {0};
char *query = "example.com";
int length = res_query(query, C_ANY, T_PTR, res_buffer, sizeof(res_buffer));
// Simple test, expect the resulting IP to be in the last four bytes of
// the buffer
struct in_addr returned_address = {
.s_addr = res_buffer[length - 1] << 24
| res_buffer[length - 2] << 16
| res_buffer[length - 3] << 8
| res_buffer[length - 4]
};
TEST(!strcmp(inet_ntoa(returned_address), EXPECTED_IP_ADDRESS),
"Expected ip address %s, got %s\n", EXPECTED_IP_ADDRESS, inet_ntoa(returned_address));
void *thread_return;
pthread_join(dns_thread, &thread_return);
return t_status;
}
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [musl] Adding dns/resolver tests to libc-test
2024-08-27 15:30 ` Ryan Ward
@ 2024-08-27 21:34 ` Rich Felker
2024-09-09 14:46 ` Ryan Ward
0 siblings, 1 reply; 7+ messages in thread
From: Rich Felker @ 2024-08-27 21:34 UTC (permalink / raw)
To: Ryan Ward; +Cc: Szabolcs Nagy, musl
On Tue, Aug 27, 2024 at 03:30:37PM +0000, Ryan Ward wrote:
> Hello,
>
> I have made an attempt at fleshing out the resolver tests, please
> see the file attached. It still needs some polishing, and I am keen
> to keep building out the necessary tests, attached is a simple
> rudimentary res_query() test.. I need to improve the packet and RR
> parsing, but sending this out to get some early feedback before I
> build out more tests.
>
> I have just been building the test and the unshare-ns.c framework
> with the libc-test build system, executing unshare-ns.exe and
> passing in resolv_query.exe as intended.
My intent was that you call enter_dns_test_ns from the test process
itself, not from a separate wrapper to exec it. This is so you don't
end up having a program in the tests dir that, when executed
independently as root, clobbers the host system's resolv.conf or hosts
file (which would be really really bad). By entering the namespace in
the same process and testing for error, you can bail out before doing
anything if the namespace setup failed. This also avoids the need to
add extra control machinery to run the tests.
Rich
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [musl] Adding dns/resolver tests to libc-test
2024-08-27 21:34 ` Rich Felker
@ 2024-09-09 14:46 ` Ryan Ward
2024-09-11 20:38 ` Szabolcs Nagy
0 siblings, 1 reply; 7+ messages in thread
From: Ryan Ward @ 2024-09-09 14:46 UTC (permalink / raw)
To: Rich Felker; +Cc: Szabolcs Nagy, musl
[-- Attachment #1: Type: text/plain, Size: 1443 bytes --]
> My intent was that you call enter_dns_test_ns from the test process
> itself, not from a separate wrapper to exec it. This is so you don't
> end up having a program in the tests dir that, when executed
> independently as root, clobbers the host system's resolv.conf or hosts
> file (which would be really really bad). By entering the namespace in
> the same process and testing for error, you can bail out before doing
> anything if the namespace setup failed. This also avoids the need to
> add extra control machinery to run the tests.
No problems, understood, I saw the exec call in the unshare-ns.c file
and got confused. I have refactored the test attached, and added the
unshare-ns.c file to the src/common/ directory in libc-test, and exposed
the enter_dns_test_ns method in the test.h header file. Is this an
appropriate solution?
The attached res_query test tests for expected domain names, classes,
types and response data from given requests. I'm yet to implement TCP
or IPV6, and wanted to ask if v4/v6 should be in separate test files,
as to ensure the test files aren't too long-winded. I've tried to structure
the test so it's as simple as specifying a domain name, and its expected RR
data. The test just then iterates through the domains to test, the server returns
the hardcoded packets, and checks are performed.
Is this somewhat along the lines of what you are looking for?
Thanks,
rw
[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: resolv_query.c --]
[-- Type: text/x-csrc; name="resolv_query.c", Size: 9256 bytes --]
#include "test.h"
#include <arpa/inet.h>
#include <arpa/nameser.h>
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <net/if.h>
#include <netdb.h>
#include <netinet/in.h>
#include <pthread.h>
#include <resolv.h>
#include <sched.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mount.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <unistd.h>
#define TEST(c, ...) ((c) || (t_error(#c " failed: " __VA_ARGS__), 0))
#define DNS_HEADER_OFFSET 12
#define DNS_FIELD_SIZES 12
typedef struct {
char *domain_name;
uint16_t type;
uint16_t class;
char *expected_response_data;
size_t num_answers;
size_t response_size;
} test_dns_packet;
#define DNS_PACKET(domain, _type, _class, response, _num_answers, size) \
(test_dns_packet) \
{ \
.domain_name = domain, .type = _type, .class = _class, \
.expected_response_data = response, \
.num_answers = _num_answers, .response_size = size \
}
// Simple answer resource record
#define EXAMPLE_ANSWER_RR \
"\xc0\x0c" \
"\x00\x01" \
"\x00\x01" \
"\x00\x00\x02\x58" \
"\x00\x04" \
"\xc0\xa8\x11\x01" // IPv4 192.168.17.1
// Extended answer resource record
#define EXTENDED_ANSWER_RR \
"\xc0\x0c" \
"\x00\x01" \
"\x00\x01" \
"\x00\x00\x02\x58" \
"\x00\x04" \
"\xc0\xa8\x11\x02" \
"\xc0\x1c" \
"\x00\x01" \
"\x00\x01" \
"\x00\x00\x02\x58" \
"\x00\x04" \
"\xc0\xa8\x11\x03"
#define TEST_IPV4 \
DNS_PACKET("example.com", 0x01, 0x01, EXAMPLE_ANSWER_RR, 0x01, \
sizeof(EXAMPLE_ANSWER_RR))
#define TEST_LONG_DOMAIN \
DNS_PACKET("foo.bar.example.com", 0x01, 0x01, EXAMPLE_ANSWER_RR, 0x01, \
sizeof(EXAMPLE_ANSWER_RR))
#define TEST_EXTENDED_RESPONSE \
DNS_PACKET("fizz.buzz.com", 0x01, 0x01, EXTENDED_ANSWER_RR, 0x02, \
sizeof(EXTENDED_ANSWER_RR))
static test_dns_packet dns_tests[] = {TEST_IPV4, TEST_LONG_DOMAIN,
TEST_EXTENDED_RESPONSE};
static const size_t dns_test_count = sizeof(dns_tests) / sizeof(*dns_tests);
// Wait until serving thread is ready to receive
pthread_barrier_t sync_barrier;
static size_t construct_response(uint16_t id, unsigned char *question,
unsigned char *response, int response_index)
{
HEADER dns_header;
const size_t dns_header_offset = sizeof(dns_header);
const size_t expected_question_size =
strlen(dns_tests[response_index].domain_name) + 6;
memset(&dns_header, 0, dns_header_offset);
dns_header.id = id;
dns_header.qr = 0x01U;
dns_header.rd = 0x01U;
dns_header.ra = 0x01U;
dns_header.qdcount = 0x0100U; // 1 question
dns_header.ancount = htons(dns_tests[response_index].num_answers);
memcpy(response, &dns_header, sizeof(dns_header));
memcpy(&response[dns_header_offset], &question[dns_header_offset],
expected_question_size);
char *answer_buffer = dns_tests[response_index].expected_response_data;
memcpy(&response[dns_header_offset + expected_question_size],
&answer_buffer[0], dns_tests[response_index].response_size);
return dns_header_offset + expected_question_size +
dns_tests[response_index].response_size -
1; // ignore null terminator
}
static int bind_to_socket(int s)
{
struct sockaddr_in dns_server;
memset(&dns_server, 0, sizeof(dns_server));
dns_server.sin_addr.s_addr = inet_addr("127.0.0.1");
dns_server.sin_family = AF_INET;
dns_server.sin_port = htons(53);
return bind(s, (struct sockaddr *)&dns_server, sizeof(dns_server));
}
static int set_environment(void)
{
FILE *ft = fopen("/etc/resolv.conf", "w");
if (ft == NULL) {
t_error("unable to open namespaced resolv.conf\n");
return -1;
}
fprintf(ft, "nameserver 127.0.0.1");
fclose(ft);
ft = fopen("/etc/hosts", "w");
if (ft == NULL) {
t_error("unable to open namespaced resolv.conf\n");
return -1;
}
fprintf(ft, "127.0.0.1 localhost");
fclose(ft);
return 0;
}
void *dns_server(void *arguments)
{
int s = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
int status = bind_to_socket(s);
struct sockaddr_in from = {0};
socklen_t from_length = sizeof(from);
unsigned char packet_buffer[NS_PACKETSZ];
pthread_barrier_wait(&sync_barrier);
int packets_received = 0;
while (packets_received != dns_test_count) {
int packet_size =
recvfrom(s, packet_buffer, NS_PACKETSZ, 0,
(struct sockaddr *)&from, &from_length);
unsigned char response_buffer[NS_PACKETSZ] = {0};
const uint16_t response_id =
(packet_buffer[1] << 8) | packet_buffer[0];
size_t response_size =
construct_response(response_id, packet_buffer,
response_buffer, packets_received);
status = sendto(s, response_buffer, response_size, 0,
(struct sockaddr *)&from, from_length);
packets_received++;
}
return 0;
}
static unsigned char *check_domain_name(const char *domain_name,
unsigned char *buffer, unsigned *length)
{
unsigned name_location = 0;
while (*buffer != 0x00) {
TEST(!memcmp(buffer + 1, domain_name + name_location, *buffer),
"Expected domain name %s, got %s\n",
domain_name + name_location, buffer + 1);
name_location += *buffer + 1;
buffer += *buffer + 1;
}
*length = name_location;
return buffer;
}
static unsigned char *check_dns_questions(unsigned char *buffer,
test_dns_packet *packet)
{
// Check the question is returned properly
unsigned char *x = buffer + DNS_HEADER_OFFSET;
unsigned name_length = 0;
x = check_domain_name(packet->domain_name, x, &name_length);
// Increment to check returned class
x++;
uint16_t class = ntohs(*(uint16_t *)x);
TEST(class == packet->class, "Expected class 0x%04x, got 0x%04x\n",
packet->class, class);
// Increment to check returned type
x += sizeof(uint16_t);
uint16_t type = ntohs(*(uint16_t *)x);
TEST(type == packet->type, "Expected type 0x%04x, got 0x%04x\n",
packet->class, type);
x += sizeof(uint16_t);
return x;
}
static unsigned char *check_dns_answers(unsigned char *buffer,
test_dns_packet *packet,
int *previous_answer_length)
{
unsigned additional_offset = 0;
if (ntohs(*(uint16_t *)buffer) != 0xc00c + *previous_answer_length) {
buffer = check_domain_name(packet->domain_name, buffer,
&additional_offset);
buffer++;
} else {
buffer += sizeof(uint16_t);
}
uint16_t class = ntohs(*(uint16_t *)buffer);
TEST(class == packet->class, "Expected class 0x%04x, got 0x%04x\n",
packet->class, class);
buffer += sizeof(uint16_t);
uint16_t type = ntohs(*(uint16_t *)buffer);
TEST(type == packet->type, "Expected type 0x%04x, got 0x%04x\n",
packet->class, type);
buffer += sizeof(uint16_t);
uint32_t ttl = ntohl(*(uint32_t *)buffer);
TEST(ttl > 0, "Expected TTL %ld to be greater than 0\n", ttl);
buffer += sizeof(uint32_t);
uint16_t resource_length = ntohs(*(uint16_t *)buffer);
TEST(resource_length > 0,
"Expected resource length %d to be greater than 0\n",
resource_length);
buffer += sizeof(uint16_t);
uint32_t expected_ip =
*(uint32_t *)&packet
->expected_response_data[DNS_FIELD_SIZES +
*previous_answer_length +
additional_offset];
TEST(!memcmp(buffer, &expected_ip, resource_length),
"Expected IPv4 addresses to match: 0x%08x, 0x%08x\n",
ntohl(*(uint32_t *)buffer), ntohl(expected_ip));
*previous_answer_length =
additional_offset + resource_length + DNS_FIELD_SIZES;
buffer += resource_length;
return buffer;
}
static void dns_test(test_dns_packet *test)
{
unsigned char res_buffer[NS_PACKETSZ] = {0};
int length = res_query(test->domain_name, test->class, test->type,
res_buffer, sizeof(res_buffer));
size_t num_answers = ntohs(*(uint16_t *)(res_buffer + 6));
unsigned char *answer_buffer = check_dns_questions(res_buffer, test);
int previous_answer_length = 0;
for (size_t answer = 0; answer < num_answers; ++answer) {
answer_buffer = check_dns_answers(answer_buffer, test,
&previous_answer_length);
}
TEST(*answer_buffer == 0x00,
"Expected end of DNS packet to equal 0x00\n");
}
int main(void)
{
if (t_enter_dns_ns() < 0) {
t_error("Failed to enter test namespace: %s\n",
strerror(errno));
return t_status;
}
if (set_environment() < 0) {
t_error("Failed to set environment\n");
return t_status;
}
pthread_barrier_init(&sync_barrier, NULL, 2);
pthread_t dns_thread;
int status = pthread_create(&dns_thread, 0, dns_server, 0);
pthread_barrier_wait(&sync_barrier);
for (int test_index = 0; test_index < dns_test_count; ++test_index) {
dns_test(&dns_tests[test_index]);
}
void *thread_return;
pthread_join(dns_thread, &thread_return);
return t_status;
}
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [musl] Adding dns/resolver tests to libc-test
2024-09-09 14:46 ` Ryan Ward
@ 2024-09-11 20:38 ` Szabolcs Nagy
2024-09-11 21:33 ` Rich Felker
0 siblings, 1 reply; 7+ messages in thread
From: Szabolcs Nagy @ 2024-09-11 20:38 UTC (permalink / raw)
To: Ryan Ward; +Cc: Rich Felker, musl
* Ryan Ward <rwardd@outlook.com.au> [2024-09-09 14:46:22 +0000]:
> > My intent was that you call enter_dns_test_ns from the test process
> > itself, not from a separate wrapper to exec it. This is so you don't
> > end up having a program in the tests dir that, when executed
> > independently as root, clobbers the host system's resolv.conf or hosts
> > file (which would be really really bad). By entering the namespace in
> > the same process and testing for error, you can bail out before doing
> > anything if the namespace setup failed. This also avoids the need to
> > add extra control machinery to run the tests.
>
> No problems, understood, I saw the exec call in the unshare-ns.c file
> and got confused. I have refactored the test attached, and added the
> unshare-ns.c file to the src/common/ directory in libc-test, and exposed
> the enter_dns_test_ns method in the test.h header file. Is this an
> appropriate solution?
it's ok
> The attached res_query test tests for expected domain names, classes,
> types and response data from given requests. I'm yet to implement TCP
> or IPV6, and wanted to ask if v4/v6 should be in separate test files,
> as to ensure the test files aren't too long-winded. I've tried to structure
> the test so it's as simple as specifying a domain name, and its expected RR
> data. The test just then iterates through the domains to test, the server returns
> the hardcoded packets, and checks are performed.
>
> Is this somewhat along the lines of what you are looking for?
i'm not yet sure if specifying static req - resp pairs
will be enough. (e.g. an answer callback would be more
flexible, so a series of add_stuff_to_answer(ctx, stuff)
and then return make_answer(ctx) or similar to take
care of large responses with many addresses or if some
bits are set in unusual ways that could be spelled out
instead of having to catch that in a long hex string)
if we support tcp servers then it would be nice to
have separate tcp/udp server code. and i think we
can support ipv4 and ipv6 in the same code too, but
i'm not experienced in networking code.
other than deciding the right level of flexibility for
the test cases, the code looks reasonable. it will
need more error checking i think (pthread_* and
recvfrom return values etc).
> #include "test.h"
> #include <arpa/inet.h>
> #include <arpa/nameser.h>
> #include <dirent.h>
> #include <errno.h>
> #include <fcntl.h>
> #include <net/if.h>
> #include <netdb.h>
> #include <netinet/in.h>
> #include <pthread.h>
> #include <resolv.h>
> #include <sched.h>
> #include <stdint.h>
> #include <stdio.h>
> #include <stdlib.h>
> #include <string.h>
> #include <sys/ioctl.h>
> #include <sys/mount.h>
> #include <sys/socket.h>
> #include <sys/stat.h>
> #include <sys/time.h>
> #include <unistd.h>
>
> #define TEST(c, ...) ((c) || (t_error(#c " failed: " __VA_ARGS__), 0))
> #define DNS_HEADER_OFFSET 12
> #define DNS_FIELD_SIZES 12
>
> typedef struct {
> char *domain_name;
> uint16_t type;
> uint16_t class;
> char *expected_response_data;
> size_t num_answers;
> size_t response_size;
> } test_dns_packet;
>
> #define DNS_PACKET(domain, _type, _class, response, _num_answers, size) \
> (test_dns_packet) \
> { \
> .domain_name = domain, .type = _type, .class = _class, \
> .expected_response_data = response, \
> .num_answers = _num_answers, .response_size = size \
> }
>
> // Simple answer resource record
> #define EXAMPLE_ANSWER_RR \
> "\xc0\x0c" \
> "\x00\x01" \
> "\x00\x01" \
> "\x00\x00\x02\x58" \
> "\x00\x04" \
> "\xc0\xa8\x11\x01" // IPv4 192.168.17.1
>
> // Extended answer resource record
> #define EXTENDED_ANSWER_RR \
> "\xc0\x0c" \
> "\x00\x01" \
> "\x00\x01" \
> "\x00\x00\x02\x58" \
> "\x00\x04" \
> "\xc0\xa8\x11\x02" \
> "\xc0\x1c" \
> "\x00\x01" \
> "\x00\x01" \
> "\x00\x00\x02\x58" \
> "\x00\x04" \
> "\xc0\xa8\x11\x03"
>
> #define TEST_IPV4 \
> DNS_PACKET("example.com", 0x01, 0x01, EXAMPLE_ANSWER_RR, 0x01, \
> sizeof(EXAMPLE_ANSWER_RR))
>
> #define TEST_LONG_DOMAIN \
> DNS_PACKET("foo.bar.example.com", 0x01, 0x01, EXAMPLE_ANSWER_RR, 0x01, \
> sizeof(EXAMPLE_ANSWER_RR))
>
> #define TEST_EXTENDED_RESPONSE \
> DNS_PACKET("fizz.buzz.com", 0x01, 0x01, EXTENDED_ANSWER_RR, 0x02, \
> sizeof(EXTENDED_ANSWER_RR))
>
> static test_dns_packet dns_tests[] = {TEST_IPV4, TEST_LONG_DOMAIN,
> TEST_EXTENDED_RESPONSE};
> static const size_t dns_test_count = sizeof(dns_tests) / sizeof(*dns_tests);
>
> // Wait until serving thread is ready to receive
> pthread_barrier_t sync_barrier;
>
> static size_t construct_response(uint16_t id, unsigned char *question,
> unsigned char *response, int response_index)
> {
> HEADER dns_header;
> const size_t dns_header_offset = sizeof(dns_header);
> const size_t expected_question_size =
> strlen(dns_tests[response_index].domain_name) + 6;
> memset(&dns_header, 0, dns_header_offset);
>
> dns_header.id = id;
> dns_header.qr = 0x01U;
> dns_header.rd = 0x01U;
> dns_header.ra = 0x01U;
> dns_header.qdcount = 0x0100U; // 1 question
> dns_header.ancount = htons(dns_tests[response_index].num_answers);
>
> memcpy(response, &dns_header, sizeof(dns_header));
> memcpy(&response[dns_header_offset], &question[dns_header_offset],
> expected_question_size);
>
> char *answer_buffer = dns_tests[response_index].expected_response_data;
> memcpy(&response[dns_header_offset + expected_question_size],
> &answer_buffer[0], dns_tests[response_index].response_size);
>
> return dns_header_offset + expected_question_size +
> dns_tests[response_index].response_size -
> 1; // ignore null terminator
> }
>
> static int bind_to_socket(int s)
> {
> struct sockaddr_in dns_server;
>
> memset(&dns_server, 0, sizeof(dns_server));
> dns_server.sin_addr.s_addr = inet_addr("127.0.0.1");
> dns_server.sin_family = AF_INET;
> dns_server.sin_port = htons(53);
>
> return bind(s, (struct sockaddr *)&dns_server, sizeof(dns_server));
> }
>
> static int set_environment(void)
> {
> FILE *ft = fopen("/etc/resolv.conf", "w");
> if (ft == NULL) {
> t_error("unable to open namespaced resolv.conf\n");
> return -1;
> }
> fprintf(ft, "nameserver 127.0.0.1");
> fclose(ft);
>
> ft = fopen("/etc/hosts", "w");
> if (ft == NULL) {
> t_error("unable to open namespaced resolv.conf\n");
> return -1;
> }
> fprintf(ft, "127.0.0.1 localhost");
> fclose(ft);
>
> return 0;
> }
>
> void *dns_server(void *arguments)
> {
> int s = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
> int status = bind_to_socket(s);
>
> struct sockaddr_in from = {0};
> socklen_t from_length = sizeof(from);
> unsigned char packet_buffer[NS_PACKETSZ];
> pthread_barrier_wait(&sync_barrier);
>
> int packets_received = 0;
> while (packets_received != dns_test_count) {
>
> int packet_size =
> recvfrom(s, packet_buffer, NS_PACKETSZ, 0,
> (struct sockaddr *)&from, &from_length);
>
> unsigned char response_buffer[NS_PACKETSZ] = {0};
> const uint16_t response_id =
> (packet_buffer[1] << 8) | packet_buffer[0];
> size_t response_size =
> construct_response(response_id, packet_buffer,
> response_buffer, packets_received);
>
> status = sendto(s, response_buffer, response_size, 0,
> (struct sockaddr *)&from, from_length);
>
> packets_received++;
> }
>
> return 0;
> }
>
> static unsigned char *check_domain_name(const char *domain_name,
> unsigned char *buffer, unsigned *length)
> {
> unsigned name_location = 0;
> while (*buffer != 0x00) {
> TEST(!memcmp(buffer + 1, domain_name + name_location, *buffer),
> "Expected domain name %s, got %s\n",
> domain_name + name_location, buffer + 1);
>
> name_location += *buffer + 1;
> buffer += *buffer + 1;
> }
> *length = name_location;
> return buffer;
> }
>
> static unsigned char *check_dns_questions(unsigned char *buffer,
> test_dns_packet *packet)
> {
> // Check the question is returned properly
> unsigned char *x = buffer + DNS_HEADER_OFFSET;
> unsigned name_length = 0;
> x = check_domain_name(packet->domain_name, x, &name_length);
>
> // Increment to check returned class
> x++;
> uint16_t class = ntohs(*(uint16_t *)x);
> TEST(class == packet->class, "Expected class 0x%04x, got 0x%04x\n",
> packet->class, class);
>
> // Increment to check returned type
> x += sizeof(uint16_t);
> uint16_t type = ntohs(*(uint16_t *)x);
> TEST(type == packet->type, "Expected type 0x%04x, got 0x%04x\n",
> packet->class, type);
>
> x += sizeof(uint16_t);
> return x;
> }
>
> static unsigned char *check_dns_answers(unsigned char *buffer,
> test_dns_packet *packet,
> int *previous_answer_length)
> {
> unsigned additional_offset = 0;
> if (ntohs(*(uint16_t *)buffer) != 0xc00c + *previous_answer_length) {
> buffer = check_domain_name(packet->domain_name, buffer,
> &additional_offset);
> buffer++;
> } else {
> buffer += sizeof(uint16_t);
> }
>
> uint16_t class = ntohs(*(uint16_t *)buffer);
> TEST(class == packet->class, "Expected class 0x%04x, got 0x%04x\n",
> packet->class, class);
>
> buffer += sizeof(uint16_t);
> uint16_t type = ntohs(*(uint16_t *)buffer);
> TEST(type == packet->type, "Expected type 0x%04x, got 0x%04x\n",
> packet->class, type);
>
> buffer += sizeof(uint16_t);
> uint32_t ttl = ntohl(*(uint32_t *)buffer);
> TEST(ttl > 0, "Expected TTL %ld to be greater than 0\n", ttl);
>
> buffer += sizeof(uint32_t);
> uint16_t resource_length = ntohs(*(uint16_t *)buffer);
> TEST(resource_length > 0,
> "Expected resource length %d to be greater than 0\n",
> resource_length);
> buffer += sizeof(uint16_t);
>
> uint32_t expected_ip =
> *(uint32_t *)&packet
> ->expected_response_data[DNS_FIELD_SIZES +
> *previous_answer_length +
> additional_offset];
>
> TEST(!memcmp(buffer, &expected_ip, resource_length),
> "Expected IPv4 addresses to match: 0x%08x, 0x%08x\n",
> ntohl(*(uint32_t *)buffer), ntohl(expected_ip));
>
> *previous_answer_length =
> additional_offset + resource_length + DNS_FIELD_SIZES;
> buffer += resource_length;
>
> return buffer;
> }
>
> static void dns_test(test_dns_packet *test)
> {
> unsigned char res_buffer[NS_PACKETSZ] = {0};
> int length = res_query(test->domain_name, test->class, test->type,
> res_buffer, sizeof(res_buffer));
>
> size_t num_answers = ntohs(*(uint16_t *)(res_buffer + 6));
> unsigned char *answer_buffer = check_dns_questions(res_buffer, test);
>
> int previous_answer_length = 0;
> for (size_t answer = 0; answer < num_answers; ++answer) {
> answer_buffer = check_dns_answers(answer_buffer, test,
> &previous_answer_length);
> }
>
> TEST(*answer_buffer == 0x00,
> "Expected end of DNS packet to equal 0x00\n");
> }
>
> int main(void)
> {
> if (t_enter_dns_ns() < 0) {
> t_error("Failed to enter test namespace: %s\n",
> strerror(errno));
> return t_status;
> }
>
> if (set_environment() < 0) {
> t_error("Failed to set environment\n");
> return t_status;
> }
>
> pthread_barrier_init(&sync_barrier, NULL, 2);
> pthread_t dns_thread;
> int status = pthread_create(&dns_thread, 0, dns_server, 0);
> pthread_barrier_wait(&sync_barrier);
>
> for (int test_index = 0; test_index < dns_test_count; ++test_index) {
> dns_test(&dns_tests[test_index]);
> }
>
> void *thread_return;
> pthread_join(dns_thread, &thread_return);
>
> return t_status;
> }
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [musl] Adding dns/resolver tests to libc-test
2024-09-11 20:38 ` Szabolcs Nagy
@ 2024-09-11 21:33 ` Rich Felker
0 siblings, 0 replies; 7+ messages in thread
From: Rich Felker @ 2024-09-11 21:33 UTC (permalink / raw)
To: Ryan Ward, musl
On Wed, Sep 11, 2024 at 10:38:18PM +0200, Szabolcs Nagy wrote:
> * Ryan Ward <rwardd@outlook.com.au> [2024-09-09 14:46:22 +0000]:
> > > My intent was that you call enter_dns_test_ns from the test process
> > > itself, not from a separate wrapper to exec it. This is so you don't
> > > end up having a program in the tests dir that, when executed
> > > independently as root, clobbers the host system's resolv.conf or hosts
> > > file (which would be really really bad). By entering the namespace in
> > > the same process and testing for error, you can bail out before doing
> > > anything if the namespace setup failed. This also avoids the need to
> > > add extra control machinery to run the tests.
> >
> > No problems, understood, I saw the exec call in the unshare-ns.c file
> > and got confused. I have refactored the test attached, and added the
> > unshare-ns.c file to the src/common/ directory in libc-test, and exposed
> > the enter_dns_test_ns method in the test.h header file. Is this an
> > appropriate solution?
>
> it's ok
>
> > The attached res_query test tests for expected domain names, classes,
> > types and response data from given requests. I'm yet to implement TCP
> > or IPV6, and wanted to ask if v4/v6 should be in separate test files,
> > as to ensure the test files aren't too long-winded. I've tried to structure
> > the test so it's as simple as specifying a domain name, and its expected RR
> > data. The test just then iterates through the domains to test, the server returns
> > the hardcoded packets, and checks are performed.
> >
> > Is this somewhat along the lines of what you are looking for?
>
> i'm not yet sure if specifying static req - resp pairs
> will be enough. (e.g. an answer callback would be more
> flexible, so a series of add_stuff_to_answer(ctx, stuff)
> and then return make_answer(ctx) or similar to take
> care of large responses with many addresses or if some
> bits are set in unusual ways that could be spelled out
> instead of having to catch that in a long hex string)
I think a lot of valuable tests will be static answers, and probably
all of them *can be*. That's because, if you're testing the behavior
of the resolver interfaces with a particular input, there are
particular queries you expect it to make, and aside from query id,
they admit hard-coded answers.
The most interesting tests are probably things like safe handling of
malformed/malicious responses, which are necessarily hand-crafted, not
something that general-purpose code for constructing well-formed DNS
responses would ever emit.
Rich
^ permalink raw reply [flat|nested] 7+ messages in thread
end of thread, other threads:[~2024-09-11 21:34 UTC | newest]
Thread overview: 7+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2024-08-18 2:03 [musl] Adding dns/resolver tests to libc-test Rich Felker
2024-08-18 9:38 ` Szabolcs Nagy
2024-08-27 15:30 ` Ryan Ward
2024-08-27 21:34 ` Rich Felker
2024-09-09 14:46 ` Ryan Ward
2024-09-11 20:38 ` Szabolcs Nagy
2024-09-11 21:33 ` Rich Felker
Code repositories for project(s) associated with this public inbox
https://git.vuxu.org/mirror/musl/
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).