summaryrefslogtreecommitdiff
path: root/internal/wireguard
diff options
context:
space:
mode:
authorJames O'Doherty <james@theodohertyfamily.com>2026-05-29 19:56:45 -0400
committerJames O'Doherty <james@theodohertyfamily.com>2026-05-29 19:56:45 -0400
commita7c7fa9e76c9c7015c31378062aa5d0c17b0f38f (patch)
treef45c63ab1d8647c657175dd92ec15000dd64975e /internal/wireguard
parentc6a1240e469ff8170cf31b39a01c1cb08fdb86f4 (diff)
Fix DNS leaks, lifecycle race, and editor arg splitting
- DNS Leak / Isolation Bypass: Blocked glibc's systemd-resolved and D-Bus socket communication within the unprivileged mount namespace by introducing BlockHostServices(). This targeted mount-blocking forces glibc to fall back to the standard resolv.conf DNS routing path and prevents host leaks. - Lifecycle Race: Reordered and protected the reference-counting cleanup routine under the profile flock to ensure that check-and-unpin operations are atomic and do not teardown namespaces actively used by parallel processes. - Editor Arguments: Split the EDITOR environment variable into discrete field tokens before invocation to support editor configurations containing command-line flags. - Testing: Added E2E regression tests for DNS leak detection, namespace unpinning concurrency, and editor argument parsing. All E2E tests now compile and pass cleanly.
Diffstat (limited to 'internal/wireguard')
-rw-r--r--internal/wireguard/wireguard.go58
1 files changed, 56 insertions, 2 deletions
diff --git a/internal/wireguard/wireguard.go b/internal/wireguard/wireguard.go
index 48bd562..3f17392 100644
--- a/internal/wireguard/wireguard.go
+++ b/internal/wireguard/wireguard.go
@@ -42,6 +42,11 @@ func StartTunnel(cfg *wgconf.Config, dnsServer string) (*Tunnel, error) {
fmt.Printf("warning: failed to make mount namespace private: %v\n", err)
}
+ // Block host services (D-Bus, nscd) to prevent name resolution leak bypasses
+ if err := BlockHostServices(); err != nil {
+ fmt.Printf("warning: failed to block host services: %v\n", err)
+ }
+
tunDev, err := tun.CreateTUN(tunName, mtu)
if err != nil {
return nil, fmt.Errorf("failed to create TUN device %s: %w", tunName, err)
@@ -254,13 +259,62 @@ func ConfigureResolvConf(dns string) error {
// 2. Make the mount private to ensure it doesn't propagate back to the host
// and to satisfy kernel requirements for mount transitions in some environments.
- if err := unix.Mount("/etc/resolv.conf", "/etc/resolv.conf", "", unix.MS_REMOUNT|unix.MS_BIND|unix.MS_PRIVATE, ""); err != nil {
- return fmt.Errorf("failed to make /etc/resolv.conf mount private: %w", err)
+ // We do this by applying MS_PRIVATE in a separate mount call.
+ if err := unix.Mount("", "/etc/resolv.conf", "", unix.MS_PRIVATE, ""); err != nil {
+ // If MS_PRIVATE fails, we can log a warning but proceed since / is already private
+ fmt.Printf("warning: failed to make /etc/resolv.conf mount private: %v\n", err)
}
return nil
}
+// BlockHostServices blocks local D-Bus and name service cache daemon (nscd) sockets
+// inside the mount namespace. This prevents glibc from bypassing the network namespace
+// isolation via host services (e.g. systemd-resolved via D-Bus).
+func BlockHostServices() error {
+ tmpDir, err := os.MkdirTemp("", "wg-wrap-block-")
+ if err != nil {
+ return fmt.Errorf("failed to create temp dir: %w", err)
+ }
+ defer func() { _ = os.Remove(tmpDir) }()
+
+ tmpFile, err := os.CreateTemp("", "wg-wrap-block-file-")
+ if err != nil {
+ return fmt.Errorf("failed to create temp file: %w", err)
+ }
+ tmpFileName := tmpFile.Name()
+ _ = tmpFile.Close()
+ defer func() { _ = os.Remove(tmpFileName) }()
+
+ // Specific socket files and directories to block
+ pathsToBlock := []string{
+ "/run/dbus/system_bus_socket",
+ "/run/systemd/resolve/io.systemd.Resolve",
+ "/run/systemd/resolve/io.systemd.Resolve.Monitor",
+ "/run/nscd/socket",
+ "/var/run/dbus/system_bus_socket",
+ "/var/run/systemd/resolve/io.systemd.Resolve",
+ "/var/run/systemd/resolve/io.systemd.Resolve.Monitor",
+ "/var/run/nscd/socket",
+ }
+
+ for _, p := range pathsToBlock {
+ stat, err := os.Stat(p)
+ if err == nil {
+ source := tmpFileName
+ if stat.IsDir() {
+ source = tmpDir
+ }
+ if err := unix.Mount(source, p, "", unix.MS_BIND, ""); err != nil {
+ fmt.Printf("warning: failed to bind-mount block over %s: %v\n", p, err)
+ } else {
+ _ = unix.Mount("", p, "", unix.MS_PRIVATE, "")
+ }
+ }
+ }
+ return nil
+}
+
// HostBind wraps a standard conn.Bind so that its socket creation (Open)
// is forced to execute within a host network namespace.
type HostBind struct {