mailing list of musl libc
 help / color / mirror / code / Atom feed
From: Rich Felker <dalias@libc.org>
To: musl@lists.openwall.com
Subject: [musl] Adding dns/resolver tests to libc-test
Date: Sat, 17 Aug 2024 22:03:28 -0400	[thread overview]
Message-ID: <20240818020328.GU10433@brightrain.aerifal.cx> (raw)

[-- 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);
}

             reply	other threads:[~2024-08-18  2:03 UTC|newest]

Thread overview: 7+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2024-08-18  2:03 Rich Felker [this message]
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

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20240818020328.GU10433@brightrain.aerifal.cx \
    --to=dalias@libc.org \
    --cc=musl@lists.openwall.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
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).