* [PATCH v2 1/2] conf: Follow changes to the host nameserver used for DNS forwarding
2026-11-13 11:21 [PATCH v2 0/2] Follow changes to the host nameserver for DNS forwarding Aditya Merchant
@ 2026-11-13 11:21 ` Aditya Merchant
2026-11-13 11:21 ` [PATCH v2 2/2] pasta: Terminate inotify buffer, skip nameless events in netns watch Aditya Merchant
1 sibling, 0 replies; 3+ messages in thread
From: Aditya Merchant @ 2026-11-13 11:21 UTC (permalink / raw)
To: passt-dev; +Cc: Stefano Brivio, David Gibson, Paul Holzinger
We read the host's /etc/resolv.conf once, at start, and forward queries
sent to the --dns-forward address to the first nameserver listed there.
If the host then joins another network, connects to a VPN, or pasta
starts before the network is configured, that nameserver goes stale and
name resolution in the namespace stops working until pasta restarts.
With rootless Podman, that means stopping every container using the
rootless network namespace.
In pasta mode, if we forward DNS queries for an IP version, and the host
nameserver for it comes from resolv.conf (no --dns-host or --dns), watch
resolv.conf with inotify and read it again as it changes, so that new
DNS flows go to its current first nameserver. Watch the directory, not
the file, as NetworkManager and others replace resolv.conf by renaming
a new file over it. If resolv.conf is a symbolic link, watch the
directory of its target, as long as that's also called resolv.conf:
the other usual target, systemd-resolved's stub-resolv.conf, always
points to its local stub resolver anyway.
Once sandboxed, we can't open files by path, and keeping a descriptor
for the directory would undo the filesystem isolation: lookups of ".."
from it reach the rest of the host filesystem. Instead, right after
isolate_prefork() unshares our mount namespace, and before mounts become
unbindable, take a detached copy of the directory with open_tree() and
OPEN_TREE_CLONE: lookups from its root can't go above it, so the
directory holding resolv.conf is all we can still see. Make that copy
read-only, and only ever open resolv.conf from it. This needs Linux 5.12
or later: if any step fails, or if we can't read resolv.conf through the
copy, we don't follow changes.
AppArmor resolves paths of files on a detached mount relative to their
filesystem, so allow the usual locations of resolv.conf under /run with
paths relative to /run, too. For SELinux, allow watching /etc, and the
directories under /run used by NetworkManager and systemd-resolved.
Flows that already exist keep their target, so a client reusing the
same source port keeps reaching the old nameserver until its flow
expires.
The new test replaces resolv.conf on an overlay mounted on /etc, in a
separate mount namespace, and checks that we pick up the new nameserver.
Link: https://github.com/podman-container-tools/podman/issues/29130
Link: https://archives.passt.top/passt-dev/20230121104703.3ebcc753@elisabeth/
Signed-off-by: Aditya Merchant <adityamerchant08@gmail.com>
---
conf.c | 259 ++++++++++++++++++++++++++--
conf.h | 2 +
contrib/apparmor/abstractions/pasta | 4 +
contrib/selinux/pasta.te | 4 +
epoll_type.h | 2 +
isolation.c | 6 +-
isolation.h | 2 +-
linux_dep.h | 30 ++++
passt.1 | 5 +-
passt.c | 5 +
passt.h | 7 +
test/pasta_options/dns_host_follow | 28 +++
test/run | 1 +
13 files changed, 342 insertions(+), 13 deletions(-)
create mode 100644 test/pasta_options/dns_host_follow
diff --git a/conf.c b/conf.c
index 7be0c79..59096de 100644
--- a/conf.c
+++ b/conf.c
@@ -19,6 +19,9 @@
#include <libgen.h>
#include <string.h>
#include <sched.h>
+#include <sys/inotify.h>
+#include <sys/mount.h>
+#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <limits.h>
@@ -36,6 +39,7 @@
#include <netinet/if_ether.h>
#include "util.h"
+#include "linux_dep.h"
#include "bitmap.h"
#include "ip.h"
#include "passt.h"
@@ -200,6 +204,31 @@ static void add_dns_resolv(struct ctx *c, const char *nameserver,
add_dns_resolv6(c, &ns6, idx6);
}
+/**
+ * resolv_nameserver() - Get address from a nameserver line of resolv.conf
+ * @line: Line from resolv.conf, terminated, might be modified
+ *
+ * Return: address as string, NULL if @line is not a nameserver line
+ */
+static const char *resolv_nameserver(char *line)
+{
+ const char *p;
+ char *end;
+
+ if (strstr(line, "nameserver ") != line)
+ return NULL;
+
+ p = strrchr(line, ' ');
+ if (!p)
+ return NULL;
+
+ end = strpbrk(line, "%\n");
+ if (end)
+ *end = 0;
+
+ return p + 1;
+}
+
/**
* get_dns() - Get nameserver addresses from local /etc/resolv.conf
* @c: Execution context
@@ -219,6 +248,14 @@ static void get_dns(struct ctx *c)
dnss_set = !!*s->n || c->no_dns_search;
dns_set = (dns4_set && dns6_set) || c->no_dns;
+ /* Unless given with --dns-host, or unless we don't read nameservers
+ * at all, host nameservers we forward queries to come from resolv.conf
+ */
+ c->ip4.dns_host_resolv = !c->no_dns && !dns4_set &&
+ IN4_IS_ADDR_UNSPECIFIED(&c->ip4.dns_host);
+ c->ip6.dns_host_resolv = !c->no_dns && !dns6_set &&
+ IN6_IS_ADDR_UNSPECIFIED(&c->ip6.dns_host);
+
if (dns_set && dnss_set)
return;
@@ -227,16 +264,8 @@ static void get_dns(struct ctx *c)
lineread_init(&resolvconf, fd);
while ((line_len = lineread_get(&resolvconf, &line)) > 0) {
- if (!dns_set && strstr(line, "nameserver ") == line) {
- p = strrchr(line, ' ');
- if (!p)
- continue;
-
- end = strpbrk(line, "%\n");
- if (end)
- *end = 0;
-
- add_dns_resolv(c, p + 1,
+ if (!dns_set && (p = resolv_nameserver(line))) {
+ add_dns_resolv(c, p,
dns4_set ? NULL : &dns4_idx,
dns6_set ? NULL : &dns6_idx);
} else if (!dnss_set && strstr(line, "search ") == line &&
@@ -281,6 +310,216 @@ out:
}
}
+/* Name of host's resolv.conf, or of the target of a symbolic link to it, that
+ * we follow changes to: see conf_resolv_watch()
+ */
+#define RESOLV_NAME "resolv.conf"
+
+/**
+ * conf_resolv_update() - Update host nameservers from host's resolv.conf
+ * @c: Execution context
+ *
+ * Return: 0 on success, negative error code if the file can't be opened
+ *
+ * #syscalls:pasta openat
+ */
+static int conf_resolv_update(struct ctx *c)
+{
+ bool found4 = false, found6 = false;
+ char addr[INET6_ADDRSTRLEN];
+ struct lineread resolvconf;
+ struct in6_addr ns6;
+ struct in_addr ns4;
+ char *line;
+ int fd;
+
+ fd = openat(c->resolv_dir_fd, RESOLV_NAME, O_RDONLY | O_CLOEXEC);
+ if (fd < 0)
+ return -errno;
+
+ lineread_init(&resolvconf, fd);
+ while (lineread_get(&resolvconf, &line) > 0) {
+ const char *p = resolv_nameserver(line);
+
+ if (!p)
+ continue;
+
+ if (!found4)
+ found4 = inet_pton(AF_INET, p, &ns4) == 1;
+ if (!found6)
+ found6 = inet_pton(AF_INET6, p, &ns6) == 1;
+ }
+ close(fd);
+
+ if (c->ip4.dns_host_resolv && found4 &&
+ !IN4_ARE_ADDR_EQUAL(&ns4, &c->ip4.dns_host)) {
+ c->ip4.dns_host = ns4;
+ info("Forwarding DNS queries to new host nameserver %s",
+ inet_ntop(AF_INET, &ns4, addr, sizeof(addr)));
+ }
+
+ if (c->ip6.dns_host_resolv && found6 &&
+ !IN6_ARE_ADDR_EQUAL(&ns6, &c->ip6.dns_host)) {
+ c->ip6.dns_host = ns6;
+ info("Forwarding DNS queries to new host nameserver %s",
+ inet_ntop(AF_INET6, &ns6, addr, sizeof(addr)));
+ }
+
+ return 0;
+}
+
+/**
+ * conf_resolv_watch() - Watch host's resolv.conf to follow nameserver changes
+ * @c: Execution context
+ *
+ * The host nameserver we forward DNS queries to can change at any time, for
+ * example as the host joins a different network or connects to a VPN. Watch
+ * the directory containing resolv.conf with inotify, as the file is usually
+ * replaced by renaming a new one over it, not rewritten in place.
+ *
+ * Once sandboxed, we can't open resolv.conf by path, and a descriptor for the
+ * directory itself would let lookups of ".." reach the whole filesystem. Keep
+ * a detached, read-only copy of the directory instead: lookups can't go above
+ * its root. Cloning needs our own mount namespace, before its mounts are made
+ * unbindable: see isolate_prefork().
+ *
+ * To keep what we can open through the copy to a minimum, only follow changes
+ * to a file called resolv.conf, and give up if we can't make the copy
+ * read-only.
+ */
+void conf_resolv_watch(struct ctx *c)
+{
+ struct mount_attr attr = {
+ .attr_set = MOUNT_ATTR_RDONLY | MOUNT_ATTR_NOSUID |
+ MOUNT_ATTR_NODEV | MOUNT_ATTR_NOEXEC,
+ .propagation = MS_PRIVATE,
+ };
+ union epoll_ref ref = { .type = EPOLL_TYPE_RESOLV_INOTIFY };
+ char dir[PATH_MAX], *base;
+ int fd = -1, dir_fd, rc;
+
+ if (c->mode != MODE_PASTA)
+ return;
+
+ if (IN4_IS_ADDR_UNSPECIFIED(&c->ip4.dns_match))
+ c->ip4.dns_host_resolv = false;
+ if (IN6_IS_ADDR_UNSPECIFIED(&c->ip6.dns_match))
+ c->ip6.dns_host_resolv = false;
+
+ if (!c->ip4.dns_host_resolv && !c->ip6.dns_host_resolv)
+ return;
+
+ /* With systemd-resolved, resolvconf, or NetworkManager in some
+ * configurations, resolv.conf is a symbolic link: watch its target
+ */
+ if (!realpath("/etc/resolv.conf", dir)) {
+ debug_perror("Not following changes to /etc/resolv.conf");
+ return;
+ }
+
+ /* Absolute path from realpath(): split it into directory and file. The
+ * other usual target of the link, systemd-resolved's stub-resolv.conf,
+ * always points to its local stub resolver: no need to follow that one.
+ * Never copy the root directory, though: that's the whole filesystem.
+ */
+ base = strrchr(dir, '/');
+ if (!base || base == dir || strcmp(base + 1, RESOLV_NAME)) {
+ debug("Not following changes to %s", dir);
+ return;
+ }
+ *base = 0;
+
+ /* AT_RECURSIVE: mounts we inherited in our user namespace are locked,
+ * and the kernel refuses to clone a subtree without its submounts
+ */
+ dir_fd = syscall(SYS_open_tree, AT_FDCWD, dir,
+ OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC | AT_RECURSIVE);
+ if (dir_fd < 0) {
+ debug_perror("Can't clone %s, not following changes", dir);
+ return;
+ }
+
+ /* Needs Linux 5.12: don't follow changes through a writable copy */
+ if (syscall(SYS_mount_setattr, dir_fd, "", AT_EMPTY_PATH | AT_RECURSIVE,
+ &attr, sizeof(attr))) {
+ debug_perror("Can't make copy of %s read-only", dir);
+ goto fail;
+ }
+
+ if ((fd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC)) < 0) {
+ debug_perror("Can't follow changes to resolv.conf");
+ goto fail;
+ }
+
+ if (inotify_add_watch(fd, dir, IN_CLOSE_WRITE | IN_MOVED_TO) < 0) {
+ debug_perror("Can't watch %s for changes", dir);
+ goto fail;
+ }
+
+ /* With the watch in place, read the file again: it might have changed
+ * since get_dns() read it. This also checks that we can read it through
+ * the copy, which security policies might not allow.
+ */
+ c->resolv_dir_fd = dir_fd;
+ if ((rc = conf_resolv_update(c))) {
+ debug("Can't read %s/" RESOLV_NAME " through a copy: %s", dir,
+ strerror_(-rc));
+ goto fail;
+ }
+
+ if (fd > FD_REF_MAX) {
+ debug("resolv.conf watch file number %i too big", fd);
+ goto fail;
+ }
+
+ ref.fd = fd;
+ if (epoll_add(c->epollfd, EPOLLIN, ref))
+ goto fail;
+
+ return;
+
+fail:
+ c->resolv_dir_fd = -1;
+ if (fd >= 0)
+ close(fd);
+ close(dir_fd);
+}
+
+/**
+ * conf_resolv_handler() - Handle inotify events for host's resolv.conf
+ * @c: Execution context
+ * @fd: inotify file descriptor watching directory of resolv.conf
+ */
+void conf_resolv_handler(struct ctx *c, int fd)
+{
+ char buf[sizeof(struct inotify_event) + NAME_MAX + 1]
+ __attribute__ ((aligned(__alignof__(struct inotify_event))));
+ const struct inotify_event *ev;
+ bool changed = false;
+ ssize_t n;
+ char *p;
+ int rc;
+
+ if ((n = read(fd, buf, sizeof(buf))) < (ssize_t)sizeof(*ev))
+ return;
+
+ /* cppcheck-suppress [negativeIndex, unmatchedSuppression] */
+ buf[n - 1] = '\0';
+ for (p = buf; p < buf + n; p += sizeof(*ev) + ev->len) {
+ ev = (const struct inotify_event *)p;
+
+ if ((ev->mask & IN_Q_OVERFLOW) ||
+ (ev->len && !strcmp(ev->name, RESOLV_NAME))) {
+ changed = true;
+ break;
+ }
+ }
+
+ if (changed && (rc = conf_resolv_update(c)))
+ warn("Can't read " RESOLV_NAME " to update nameservers: %s",
+ strerror_(-rc));
+}
+
/**
* conf_netns_opt() - Parse --netns option
* @netns: buffer of size PATH_MAX, updated with netns path
diff --git a/conf.h b/conf.h
index 654fd16..5dc4aa2 100644
--- a/conf.h
+++ b/conf.h
@@ -11,5 +11,7 @@ void conf_fds(struct ctx *c, int argc, char **argv);
void conf(struct ctx *c, int argc, char **argv);
void conf_listen_handler(struct ctx *c, uint32_t events);
void conf_handler(struct ctx *c, uint32_t events);
+void conf_resolv_watch(struct ctx *c);
+void conf_resolv_handler(struct ctx *c, int fd);
#endif /* CONF_H */
diff --git a/contrib/apparmor/abstractions/pasta b/contrib/apparmor/abstractions/pasta
index 89fa427..7ee892b 100644
--- a/contrib/apparmor/abstractions/pasta
+++ b/contrib/apparmor/abstractions/pasta
@@ -29,6 +29,10 @@
@{run}/user/@{uid}/** rw, # pasta_open_ns()
+ # Host's resolv.conf in /run, read via a detached copy of its directory,
+ # with paths relative to /run: conf_resolv_watch(), conf_resolv_update()
+ /{NetworkManager,connman,netconfig,resolvconf,systemd/resolve}/resolv.conf r,
+
@{PROC}/[0-9]*/ns/ r, # pasta_netns_quit_init(),
@{PROC}/[0-9]*/ns/net r, # pasta_wait_for_ns(),
@{PROC}/[0-9]*/ns/user r, # conf_pasta_ns()
diff --git a/contrib/selinux/pasta.te b/contrib/selinux/pasta.te
index d0c9c32..1cc15da 100644
--- a/contrib/selinux/pasta.te
+++ b/contrib/selinux/pasta.te
@@ -164,6 +164,10 @@ allow pasta_t self:netlink_route_socket { bind create nlmsg_read nlmsg_write set
kernel_search_network_sysctl(pasta_t)
sysnet_read_config(pasta_t)
+# Watch the directory holding resolv.conf for changes: conf_resolv_watch()
+files_watch_etc_dirs(pasta_t)
+networkmanager_watch_pid_dirs(pasta_t)
+systemd_resolved_watch_pid_dirs(pasta_t)
allow pasta_t tmp_t:sock_file { create unlink write };
diff --git a/epoll_type.h b/epoll_type.h
index 061325a..a42944f 100644
--- a/epoll_type.h
+++ b/epoll_type.h
@@ -50,6 +50,8 @@ enum epoll_type {
EPOLL_TYPE_CONF_LISTEN,
/* Configuration socket */
EPOLL_TYPE_CONF,
+ /* inotify fd watching host's resolv.conf (pasta) */
+ EPOLL_TYPE_RESOLV_INOTIFY,
EPOLL_NUM_TYPES,
};
diff --git a/isolation.c b/isolation.c
index 0cb6adb..9cd299f 100644
--- a/isolation.c
+++ b/isolation.c
@@ -482,11 +482,12 @@ void isolate_user(const struct ctx *c, uid_t uid, gid_t gid, bool use_userns,
* Should:
* - Move us to our own IPC and UTS namespaces
* - Move us to a mount namespace with only an empty directory
+ * - Keep a detached copy of the directory of resolv.conf, if needed
* - Drop unneeded capabilities (in the new user namespace)
* Mustn't:
* - Remove syscalls we need to daemonise
*/
-int isolate_prefork(const struct ctx *c)
+int isolate_prefork(struct ctx *c)
{
int flags = CLONE_NEWIPC | CLONE_NEWNS | CLONE_NEWUTS;
uint64_t ns_caps = 0;
@@ -503,6 +504,9 @@ int isolate_prefork(const struct ctx *c)
return -errno;
}
+ /* This clones a mount: do it before they all become unbindable */
+ conf_resolv_watch(c);
+
if (mount("", "/", "", MS_UNBINDABLE | MS_REC, NULL)) {
err_perror("Failed to remount /");
return -errno;
diff --git a/isolation.h b/isolation.h
index fe8d7d5..0d5a838 100644
--- a/isolation.h
+++ b/isolation.h
@@ -14,7 +14,7 @@ void isolate_initial(void);
void isolate_fds(struct ctx *c, int argc, char **argv);
void isolate_user(const struct ctx *c, uid_t uid, gid_t gid, bool use_userns,
const char *userns);
-int isolate_prefork(const struct ctx *c);
+int isolate_prefork(struct ctx *c);
void isolate_postfork(const struct ctx *c);
#endif /* ISOLATION_H */
diff --git a/linux_dep.h b/linux_dep.h
index fa539ce..57ebc0d 100644
--- a/linux_dep.h
+++ b/linux_dep.h
@@ -138,6 +138,36 @@ struct tcp_info_linux {
#define CLOSE_RANGE_UNSHARE (1U << 1)
#endif
+#include <sys/mount.h>
+
+/* Older C libraries, or kernel headers < 5.2 and < 5.12, need these */
+#ifndef SYS_open_tree
+#define SYS_open_tree 428
+#endif
+#ifndef OPEN_TREE_CLONE
+#define OPEN_TREE_CLONE 1
+#define OPEN_TREE_CLOEXEC O_CLOEXEC
+#endif
+#ifndef AT_RECURSIVE
+#define AT_RECURSIVE 0x8000
+#endif
+#ifndef SYS_mount_setattr
+#define SYS_mount_setattr 442
+#endif
+#ifndef MOUNT_ATTR_RDONLY
+#define MOUNT_ATTR_RDONLY 0x00000001
+#define MOUNT_ATTR_NOSUID 0x00000002
+#define MOUNT_ATTR_NODEV 0x00000004
+#define MOUNT_ATTR_NOEXEC 0x00000008
+
+struct mount_attr {
+ uint64_t attr_set;
+ uint64_t attr_clr;
+ uint64_t propagation;
+ uint64_t userns_fd;
+};
+#endif
+
#ifndef TCP_REPAIR_ON
#define TCP_REPAIR_ON 1
#define TCP_REPAIR_OFF 0
diff --git a/passt.1 b/passt.1
index 9a5cc87..015d99f 100644
--- a/passt.1
+++ b/passt.1
@@ -287,7 +287,10 @@ Configure the host nameserver which guest or namespace queries to the
\fB\-\-dns-forward\fR address will be redirected to. This option can
be specified zero to two times (once for IPv4, once for IPv6).
By default, the first nameserver from the host's
-\fI/etc/resolv.conf\fR.
+\fI/etc/resolv.conf\fR. In pasta mode, on Linux 5.12 or later, this
+default follows changes to that file: new DNS flows go to its current
+first nameserver of the same IP version, while existing flows keep their
+target until they expire.
.TP
.BR \-S ", " \-\-search " " \fIlist
diff --git a/passt.c b/passt.c
index fa7afc5..1c4f594 100644
--- a/passt.c
+++ b/passt.c
@@ -69,6 +69,7 @@ struct ctx passt_ctx = {
.fd_control_listen = -1,
.fd_repair_listen = -1,
.pasta_netns_fd = -1,
+ .resolv_dir_fd = -1,
.device_state_fd = -1,
};
@@ -92,6 +93,7 @@ char *epoll_type_str[] = {
[EPOLL_TYPE_NL_NEIGH] = "netlink neighbour notifier socket",
[EPOLL_TYPE_CONF_LISTEN] = "configuration listening socket",
[EPOLL_TYPE_CONF] = "configuration socket",
+ [EPOLL_TYPE_RESOLV_INOTIFY] = "resolv.conf inotify watch",
};
static_assert(ARRAY_SIZE(epoll_type_str) == EPOLL_NUM_TYPES,
"epoll_type_str[] doesn't match enum epoll_type");
@@ -302,6 +304,9 @@ static void passt_worker(void *opaque, int nfds, struct epoll_event *events)
case EPOLL_TYPE_CONF:
conf_handler(c, eventmask);
break;
+ case EPOLL_TYPE_RESOLV_INOTIFY:
+ conf_resolv_handler(c, ref.fd);
+ break;
default:
/* Can't happen */
assert(0);
diff --git a/passt.h b/passt.h
index d4661d8..f98daca 100644
--- a/passt.h
+++ b/passt.h
@@ -99,6 +99,7 @@ struct guest_addr {
* @dns_host: Use this DNS on the host for forwarding
* @addr_out: Optional source address for outbound traffic
* @ifname_out: Optional interface name to bind outbound sockets to
+ * @dns_host_resolv: @dns_host comes from host's resolv.conf: follow changes
* @no_copy_routes: Don't copy all routes when configuring target namespace
* @no_copy_addrs: Don't copy all addresses when configuring namespace
* @addr_fixed: Address was given explicitly (-a): don't update
@@ -120,6 +121,7 @@ struct ip4_ctx {
char ifname_out[IFNAMSIZ];
+ bool dns_host_resolv;
bool no_copy_routes;
bool no_copy_addrs;
bool addr_fixed;
@@ -141,6 +143,7 @@ struct ip4_ctx {
* @dns_host: Use this DNS on the host for forwarding
* @addr_out: Optional source address for outbound traffic
* @ifname_out: Optional interface name to bind outbound sockets to
+ * @dns_host_resolv: @dns_host comes from host's resolv.conf: follow changes
* @no_copy_routes: Don't copy all routes when configuring target namespace
* @no_copy_addrs: Don't copy all addresses when configuring namespace
* @addr_fixed: Address was given explicitly (-a): don't update
@@ -164,6 +167,7 @@ struct ip6_ctx {
char ifname_out[IFNAMSIZ];
+ bool dns_host_resolv;
bool no_copy_routes;
bool no_copy_addrs;
bool addr_fixed;
@@ -190,6 +194,7 @@ struct ip6_ctx {
* @no_netns_quit: In pasta mode, don't exit if fs-bound namespace is gone
* @netns_base: Base name for fs-bound namespace, if any, in pasta mode
* @netns_dir: Directory of fs-bound namespace, if any, in pasta mode
+ * @resolv_dir_fd: Directory of host's resolv.conf, if watched for changes
* @epollfd: File descriptor for epoll instance
* @fd_tap_listen: File descriptor for listening AF_UNIX socket, if any
* @fd_tap: AF_UNIX socket, tuntap device, or pre-opened socket
@@ -270,6 +275,8 @@ struct ctx {
char netns_base[PATH_MAX];
char netns_dir[PATH_MAX];
+ int resolv_dir_fd;
+
int epollfd;
int fd_tap_listen;
int fd_tap;
diff --git a/test/pasta_options/dns_host_follow b/test/pasta_options/dns_host_follow
new file mode 100644
index 0000000..2d7ac6d
--- /dev/null
+++ b/test/pasta_options/dns_host_follow
@@ -0,0 +1,28 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+#
+# PASST - Plug A Simple Socket Transport
+# for qemu/UNIX domain socket mode
+#
+# PASTA - Pack A Subtle Tap Abstraction
+# for network namespace/tap device mode
+#
+# test/pasta_options/dns_host_follow - Check that we follow host's resolv.conf
+
+htools unshare mount grep
+
+test Host nameserver follows changes to resolv.conf
+set DIR __STATEDIR__/resolv
+set LOG_FILE __STATEDIR__/resolv.log
+
+passt unshare -rUm
+passt mkdir -p __DIR__/upper __DIR__/work
+passt mount -t overlay -o lowerdir=/etc,upperdir=__DIR__/upper,workdir=__DIR__/work none /etc
+passt echo "nameserver 192.0.2.1" > /etc/r && mv /etc/r /etc/resolv.conf
+passt ./pasta -d -l __LOG_FILE__ --dns-forward 192.0.2.53 -- sleep 3 2>/dev/null &
+sleep 1
+passt echo "nameserver 192.0.2.2" > /etc/r && mv /etc/r /etc/resolv.conf
+sleep 1
+check grep -q "new host nameserver 192.0.2.2" __LOG_FILE__
+passt wait
+passt umount /etc
+passt exit
diff --git a/test/run b/test/run
index 14ddbbf..751ff50 100755
--- a/test/run
+++ b/test/run
@@ -87,6 +87,7 @@ run() {
test pasta_options/log_to_file
test pasta_options/netns_only
test pasta_options/no_pidns
+ test pasta_options/dns_host_follow
teardown pasta_options
setup build
--
2.50.1 (Apple Git-155)
^ permalink raw reply [flat|nested] 3+ messages in thread