From mboxrd@z Thu Jan 1 00:00:00 1970 Authentication-Results: passt.top; dmarc=fail (p=none dis=none) header.from=gmail.com Received: from mail-dy1-x132d.google.com (mail-dy1-x132d.google.com [IPv6:2607:f8b0:4864:20::132d]) by passt.top (Postfix) with ESMTPS id 9FDFD5A0619 for ; Fri, 13 Nov 2026 12:21:24 +0100 (CET) Received: by mail-dy1-x132d.google.com with SMTP id 5a478bee46e88-36e99cf7bb3so1043393eec.0 for ; Fri, 13 Nov 2026 03:21:24 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20260707; t=1794568883; x=1795173683; darn=passt.top; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to:content-type; bh=0xzKwrYw4g06XUg3ooTIc+zhjx+Ddd8xmd3sH/YLeAI=; b=Oc/mIB4ePcMYzK0r3r6QL/OZdveCQRpN4IUPUKOkBzqxbUMn56X/IySPU/kCNfhjO3 BdcdjT+KPoV/rQwoE6mR/86bC3zNl4y7IH76JpWX8LtMwcQvzy6+e89knaEo053OGpaw /TXtBreaIzTku1qzKeNXdNxeBRu8cz1p2CpXXnYJV35BaTLmdIC/Mg8N3X5PEfFbSIAT 77S96l2sa/DO+ITnsYBM19pvTe0+HBvtiLPk2iBC/vTQCMteXVAuMlS15SdcNhQAEBDo SqPIIGzHPCgl+6BAvdfdVuhL5rwF78feLxOrp0SIoS13jsFxd1AAZ0NgM9g1YjzwywIa vciw== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20260707; t=1794568883; x=1795173683; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from :to:cc:subject:date:message-id:reply-to:content-type; bh=0xzKwrYw4g06XUg3ooTIc+zhjx+Ddd8xmd3sH/YLeAI=; b=a4fyIMJaGVkb0nG4BSjuSp1mjsY1qKDQWclE1CzQz4piBWsSlXl492cPfZOqrOFrUK gMozFZm2Xlde6yJCVGTthzW4GHEVnI8cUti1++++D1BFoCjFWXiaKVzy00xlZMTZQA7R 1enaDq1N1l93DMmR/uMisBKeBl2kmfQeAfbCiTCXvg4Dfg5DmN215cP2tghsF2NUk2CD JULXiMuYPNDeWlC9sHUL8Nmu/GyRblL845zRfRyDLDsxjKDRGtkTkngHeubZeWmK2W9V YTb1M+v9Jw/pQLS+ieYvukc7cv8eVUD8K1LK1C6mZvOb2UQ1BZnjPzbiILA1NL/IfkuQ WMXg== X-Gm-Message-State: APuU4Z8KCmTg+zQradA2QcB+2o2XpeV5mOankIknOBt3fyE7OtSbwF3g LcK9xwRBsAY/K1BHwXUkqPDADNH7ZXgdNGLM6QqvrLAWi9L0/7RL+UVXBTITuLHf X-Gm-Gg: AYBFou3w/Fz+tbi9/f/0V+l7UPC4qijhTr27O3QAPJadW2mWTbcfWgtlMVxEi2VEyZA GHWbshlI24c7xuHcVw02s6BlMwQpPa1p5AgNbGw4I+MsUUk0xa/XfJU3+HnB164SUTpY31QCj2y ggNE9MiWUUz3FA8z6nhanD5B2t8PSffAilhU4Hher0R6fSm7yR67vzNcLoDKJxirKB+YKpR4Iu8 jAPUHcmuCmkNNCmmEUGxYIE9lCVC/i/vZhz4l0KZ7bEtGc9UbLofKqUW71AJ8VEAdn1G4lpmVnc ckz3wH9okdH/0FdLathIBk8J3BP2anBnrM+z3I139N05/ll/VaIJ/n3sDGxbxv0VM102OBv/uBV mIA9G37+qJF5+owZqE2TcxswMPoXPxCcSIIoSMDmoOnj7n6/WmFau++jAXXmVzmTRapwRuCYCEk 4j9i1jqILSmsVrLTA2rP72tibiy2TfdIBVlqzxetXdQ68TjBF2aYj/rJC7/8KIC3Xv5rLdYOZD6 EXYVvA4MJC1XsKfjxmokX0xLVhN25fziEpbEYWBP8NAbw2/z35hnKaz40Y3jb68q/ZJAnPZe1/K 24rsPPscLqXCTIbCn5Lw0LPObngnuxgMOltgb5bMc2+uclK7/uY4iYrLWp1kvqc7M8JjmkuTIRb QqXmN+5FVufPh2Dt/HPX9Uun16fqE X-Received: by 2002:a05:693c:8107:b0:36c:b7b4:407e with SMTP id 5a478bee46e88-37967cdd709mr2057114eec.22.1794568882733; Fri, 13 Nov 2026 03:21:22 -0800 (PST) Received: from localhost ([2405:201:7:7:2843:7bd:92d7:9f87]) by smtp.gmail.com with ESMTPSA id 5a478bee46e88-379563df84fsm12598764eec.20.2026.11.13.03.21.20 (version=TLS1_3 cipher=TLS_CHACHA20_POLY1305_SHA256 bits=256/256); Fri, 13 Nov 2026 03:21:22 -0800 (PST) From: Aditya Merchant To: passt-dev@passt.top Subject: [PATCH v2 1/2] conf: Follow changes to the host nameserver used for DNS forwarding Date: Fri, 13 Nov 2026 16:51:13 +0530 Message-ID: <20261113112114.38764-2-adityamerchant08@gmail.com> X-Mailer: git-send-email 2.50.1 In-Reply-To: <20261113112114.38764-1-adityamerchant08@gmail.com> References: <20261113112114.38764-1-adityamerchant08@gmail.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-MailFrom: adityamerchant08@gmail.com X-Mailman-Rule-Hits: nonmember-moderation X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; emergency; loop; banned-address; member-moderation Message-ID-Hash: JW2YCYRSNMLUF3HJPRYW35OWFGAUXM5U X-Message-ID-Hash: JW2YCYRSNMLUF3HJPRYW35OWFGAUXM5U X-Mailman-Approved-At: Fri, 13 Nov 2026 14:07:19 +0100 CC: Stefano Brivio , David Gibson , Paul Holzinger X-Mailman-Version: 3.3.8 Precedence: list List-Id: Development discussion and patches for passt Archived-At: Archived-At: List-Archive: List-Archive: List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: 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 --- 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 #include #include +#include +#include +#include #include #include #include @@ -36,6 +39,7 @@ #include #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 + +/* 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)