diff options
| author | James O'Doherty <james@theodohertyfamily.com> | 2026-05-29 20:42:23 -0400 |
|---|---|---|
| committer | James O'Doherty <james@theodohertyfamily.com> | 2026-05-29 20:42:23 -0400 |
| commit | b7745456d67f48f56ba94e47946e40805b6ef1ee (patch) | |
| tree | 789516e8f6e95b712458ec66cd77366f9e4f3e26 /internal/wireguard/wireguard.go | |
| parent | d4cec92f5690a60b3509ab718bdea72dc520110e (diff) | |
refactor: improve resource management and cleanup patterns
- Simplify namespace bootstrapping by introducing `prepareLauncher` helper
- Implement a cleanup stack in `StartTunnel` to ensure orderly resource release on error
- Streamline temporary file and mount lifecycles in `ConfigureResolvConf` and `BlockHostServices`
- Ensure `Tunnel.Close()` also closes the underlying TUN device
- Reduce redundant manual cleanup calls using defer-based error handling
Diffstat (limited to 'internal/wireguard/wireguard.go')
| -rw-r--r-- | internal/wireguard/wireguard.go | 58 |
1 files changed, 21 insertions, 37 deletions
diff --git a/internal/wireguard/wireguard.go b/internal/wireguard/wireguard.go index 3f17392..3a2bfa3 100644 --- a/internal/wireguard/wireguard.go +++ b/internal/wireguard/wireguard.go @@ -28,21 +28,24 @@ type Tunnel struct { } // StartTunnel creates a TUN device, launches wireguard-go over it, and configures IPs/routes. -func StartTunnel(cfg *wgconf.Config, dnsServer string) (*Tunnel, error) { +func StartTunnel(cfg *wgconf.Config, dnsServer string) (t *Tunnel, err error) { + var cleanups []func() + defer func() { + if err != nil { + for i := len(cleanups) - 1; i >= 0; i-- { + cleanups[i]() + } + } + }() + // 1. Create the TUN device inside the current (isolated) namespace - // We use the default name 'tun0' tunName := "tun0" mtu := 1420 - // Ensure the mount namespace is private to prevent mount propagation to the host. - // This is critical for the bind-mount of /etc/resolv.conf to work in rootless environments. if err := unix.Mount("", "/", "", unix.MS_REC|unix.MS_PRIVATE, ""); err != nil { - // We log this as a warning because some environments might not allow this, - // but we can still try to proceed. 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) } @@ -51,12 +54,12 @@ func StartTunnel(cfg *wgconf.Config, dnsServer string) (*Tunnel, error) { if err != nil { return nil, fmt.Errorf("failed to create TUN device %s: %w", tunName, err) } + cleanups = append(cleanups, func() { tunDev.Close() }) // 2. Instantiate the userspace WireGuard device logger := device.NewLogger(device.LogLevelSilent, "[wg-wrap] ") var bind conn.Bind - // Check if a pre-opened host UDP socket file descriptor was passed first (Approach A - FD Passing) if hostSocketFdStr := os.Getenv("WG_WRAP_HOST_SOCKET_FD"); hostSocketFdStr != "" { if fd, err := strconv.Atoi(hostSocketFdStr); err == nil && fd > 0 { if fdBind, err := NewFDBind(fd); err == nil { @@ -65,7 +68,6 @@ func StartTunnel(cfg *wgconf.Config, dnsServer string) (*Tunnel, error) { } } - // Fallback to NewHostBind or standard Bind if no host socket was passed if bind == nil { bind = conn.NewDefaultBind() if hostNetNSFdStr := os.Getenv("WG_WRAP_HOST_NETNS_FD"); hostNetNSFdStr != "" { @@ -76,38 +78,28 @@ func StartTunnel(cfg *wgconf.Config, dnsServer string) (*Tunnel, error) { } wgDev := device.NewDevice(tunDev, bind, logger) + cleanups = append(cleanups, func() { wgDev.Close() }) // 3. Formulate the UAPI configuration string to configure peers/keys uapiConf, err := buildUAPIConfig(cfg) if err != nil { - wgDev.Close() return nil, fmt.Errorf("failed to build UAPI config: %w", err) } - // Apply configuration via UAPI (IpcSet) if err := wgDev.IpcSet(uapiConf); err != nil { - wgDev.Close() return nil, fmt.Errorf("failed to configure WireGuard device: %w", err) } - // Enable device if err := wgDev.Up(); err != nil { - wgDev.Close() return nil, fmt.Errorf("failed to bring up WireGuard device: %w", err) } // 4. Configure network interface using standard Linux network commands (iproute2) - // Since we are mapped to root (UID 0) inside our isolated network namespace, - // we have complete control over local network interfaces without affecting the host. if err := configureInterface(tunName, cfg.Address, mtu); err != nil { - wgDev.Close() return nil, fmt.Errorf("failed to configure network interface %s: %w", tunName, err) } - // Configure DNS resolver inside the namespace if err := ConfigureResolvConf(dnsServer); err != nil { - // We treat DNS failure as a warning rather than a fatal error to allow - // the tunnel to function even if /etc/resolv.conf is read-only. fmt.Printf("warning: failed to configure DNS resolver: %v\n", err) } @@ -235,33 +227,26 @@ func ConfigureResolvConf(dns string) error { return nil } - // To avoid modifying the host's /etc/resolv.conf, we use the private mount namespace. tmpFile, err := os.CreateTemp("", "resolvconf") if err != nil { return fmt.Errorf("failed to create temp resolv.conf: %w", err) } - defer func() { _ = tmpFile.Close() }() + launcherPath := tmpFile.Name() + defer func() { + _ = tmpFile.Close() + _ = os.Remove(launcherPath) + }() content := fmt.Sprintf("nameserver %s\n", dns) if _, err := tmpFile.WriteString(content); err != nil { return fmt.Errorf("failed to write to temp resolv.conf: %w", err) } - // 1. Bind-mount the temp file over /etc/resolv.conf - if err := unix.Mount(tmpFile.Name(), "/etc/resolv.conf", "", unix.MS_BIND, ""); err != nil { - _ = os.Remove(tmpFile.Name()) - return fmt.Errorf("failed to bind-mount %s to /etc/resolv.conf: %w", tmpFile.Name(), err) + if err := unix.Mount(launcherPath, "/etc/resolv.conf", "", unix.MS_BIND, ""); err != nil { + return fmt.Errorf("failed to bind-mount %s to /etc/resolv.conf: %w", launcherPath, err) } - // Unlink the temporary source file. Since /etc/resolv.conf is a bind mount, - // the kernel will keep the inode alive, but the file is removed from /tmp. - _ = os.Remove(tmpFile.Name()) - - // 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. - // 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) } @@ -276,7 +261,7 @@ func BlockHostServices() error { if err != nil { return fmt.Errorf("failed to create temp dir: %w", err) } - defer func() { _ = os.Remove(tmpDir) }() + defer os.RemoveAll(tmpDir) tmpFile, err := os.CreateTemp("", "wg-wrap-block-file-") if err != nil { @@ -284,9 +269,8 @@ func BlockHostServices() error { } tmpFileName := tmpFile.Name() _ = tmpFile.Close() - defer func() { _ = os.Remove(tmpFileName) }() + defer os.Remove(tmpFileName) - // Specific socket files and directories to block pathsToBlock := []string{ "/run/dbus/system_bus_socket", "/run/systemd/resolve/io.systemd.Resolve", |
