diff options
| author | James O'Doherty <james@theodohertyfamily.com> | 2026-05-29 19:56:45 -0400 |
|---|---|---|
| committer | James O'Doherty <james@theodohertyfamily.com> | 2026-05-29 19:56:45 -0400 |
| commit | a7c7fa9e76c9c7015c31378062aa5d0c17b0f38f (patch) | |
| tree | f45c63ab1d8647c657175dd92ec15000dd64975e /internal/wireguard/wireguard.go | |
| parent | c6a1240e469ff8170cf31b39a01c1cb08fdb86f4 (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/wireguard.go')
| -rw-r--r-- | internal/wireguard/wireguard.go | 58 |
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 { |
