From 29621ecbd1e77e6e1a70b6b3ea8fbe3a56e47df3 Mon Sep 17 00:00:00 2001 From: James O'Doherty Date: Sat, 13 Jun 2026 11:51:04 -0400 Subject: refactor: implement dependency injection and enable parallel testing This commit refactors the core system operations to use a manager-based dependency injection pattern, eliminating global state and resolving data races in the test suite. Architecture: - Introduced NetworkManager and NetworkOps interface in internal/network to abstract netlink calls. - Introduced MountOps and FileSystem interfaces in internal/namespace to abstract mount and filesystem operations. - Introduced TunnelManager in internal/wireguard to coordinate tunnel lifecycle using the new abstractions. - Updated internal/cli and internal/manager to use these managers. Testing: - Restored t.Parallel() to unit tests in internal/network and internal/wireguard. - Implemented setupParallelEnv and an enhanced mockFS in wireguard_unit_test.go to ensure complete test isolation. - Added bootstrap_test.go to verify launcher preparation logic in internal/namespace without requiring syscall.Exec. - Resolved data races in internal/network tests. CLI: - Added support for -h, --help, and -help flags for the main command. Verification: - Passed all tests (unit, integration, E2E). - Verified zero data races with 'go test -race'. - Passed golangci-lint and go vet. --- internal/wireguard/wireguard.go | 64 ++++++++++++++++++++++++----------------- 1 file changed, 38 insertions(+), 26 deletions(-) (limited to 'internal/wireguard/wireguard.go') diff --git a/internal/wireguard/wireguard.go b/internal/wireguard/wireguard.go index 8ffe794..67cc211 100644 --- a/internal/wireguard/wireguard.go +++ b/internal/wireguard/wireguard.go @@ -51,8 +51,24 @@ type Tunnel struct { dnsFile string } +// TunnelManager coordinates the creation and management of a WireGuard tunnel. +type TunnelManager struct { + MountOps namespace.MountOps + FS namespace.FileSystem + Net *network.NetworkManager +} + +// NewTunnelManager creates a new TunnelManager with production defaults. +func NewTunnelManager() *TunnelManager { + return &TunnelManager{ + MountOps: namespace.DefaultMountOps, + FS: namespace.DefaultFS, + Net: network.NewNetworkManager(), + } +} + // StartTunnel creates a TUN device, launches wireguard-go over it, and configures IPs/routes. -func StartTunnel(pm *paths.PathManager, profile string, cfg *wgconf.Config, dnsServer string) (t *Tunnel, err error) { +func (tm *TunnelManager) StartTunnel(pm *paths.PathManager, profile string, cfg *wgconf.Config, dnsServer string) (t *Tunnel, err error) { var cleanups []func() defer func() { if err != nil { @@ -66,11 +82,11 @@ func StartTunnel(pm *paths.PathManager, profile string, cfg *wgconf.Config, dnsS tunName := "tun0" mtu := 1420 - if err := unix.Mount("", "/", "", unix.MS_REC|unix.MS_PRIVATE, ""); err != nil { + if err := tm.MountOps.Mount("", "/", "", unix.MS_REC|unix.MS_PRIVATE, ""); err != nil { fmt.Printf("warning: failed to make mount namespace private: %v\n", err) } - if err := BlockHostServices(pm, profile); err != nil { + if err := tm.BlockHostServices(pm, profile); err != nil { fmt.Printf("warning: failed to block host services: %v\n", err) } @@ -118,18 +134,18 @@ func StartTunnel(pm *paths.PathManager, profile string, cfg *wgconf.Config, dnsS } // 4. Configure network interface - if err := network.ConfigureInterface(tunName, cfg.Address, mtu); err != nil { + if err := tm.Net.ConfigureInterface(tunName, cfg.Address, mtu); err != nil { return nil, fmt.Errorf("failed to configure network interface %s: %w", tunName, err) } var dnsFile string profileDir := filepath.Join(pm.RuntimeBaseDir(), "profiles", profile) - if path, err := ConfigureResolvConf(dnsServer, profileDir); err != nil { + if path, err := tm.ConfigureResolvConf(dnsServer, profileDir); err != nil { fmt.Printf("warning: failed to configure DNS resolver: %v\n", err) } else { dnsFile = path cleanups = append(cleanups, func() { - if err := UnmountResolvConf(dnsFile); err != nil { + if err := tm.UnmountResolvConf(dnsFile); err != nil { fmt.Printf("warning: failed to unmount resolv.conf during cleanup: %v\n", err) } }) @@ -143,12 +159,12 @@ func StartTunnel(pm *paths.PathManager, profile string, cfg *wgconf.Config, dnsS } // Close shuts down the userspace WireGuard device and closes the TUN interface. -func (t *Tunnel) Close() { +func (t *Tunnel) Close(tm *TunnelManager) { if t.Device != nil { t.Device.Close() } if t.dnsFile != "" { - if err := UnmountResolvConf(t.dnsFile); err != nil { + if err := tm.UnmountResolvConf(t.dnsFile); err != nil { fmt.Printf("warning: failed to unmount resolv.conf: %v\n", err) } } @@ -216,14 +232,12 @@ func GetTunnelLocalIP(cfg *wgconf.Config) (string, error) { } // ConfigureResolvConf creates a temporary resolv.conf file and bind-mounts it to /etc/resolv.conf. -func ConfigureResolvConf(dns string, profileDir string) (string, error) { +func (tm *TunnelManager) ConfigureResolvConf(dns string, profileDir string) (string, error) { if dns == "" { return "", nil } - // Create the temporary resolv.conf file within the profile directory - // so it can be cleaned up during namespace unpinning. - tmpFile, err := os.CreateTemp(profileDir, "resolvconf") + tmpFile, err := tm.FS.CreateTemp(profileDir, "resolvconf") if err != nil { return "", fmt.Errorf("failed to create temp resolv.conf in %s: %w", profileDir, err) } @@ -235,11 +249,11 @@ func ConfigureResolvConf(dns string, profileDir string) (string, error) { } _ = tmpFile.Close() - if err := unix.Mount(launcherPath, "/etc/resolv.conf", "", unix.MS_BIND, ""); err != nil { + if err := tm.MountOps.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) } - if err := unix.Mount("", "/etc/resolv.conf", "", unix.MS_PRIVATE, ""); err != nil { + if err := tm.MountOps.Mount("", "/etc/resolv.conf", "", unix.MS_PRIVATE, ""); err != nil { fmt.Printf("warning: failed to make /etc/resolv.conf mount private: %v\n", err) } @@ -247,16 +261,14 @@ func ConfigureResolvConf(dns string, profileDir string) (string, error) { } // UnmountResolvConf unmounts the bind-mounted resolv.conf and removes the temporary file. -func UnmountResolvConf(path string) error { +func (tm *TunnelManager) UnmountResolvConf(path string) error { if path == "" { return nil } - // Attempt to unmount. If it fails, it might already be unmounted - // or the namespace might be gone. - _ = unix.Unmount("/etc/resolv.conf", unix.MNT_DETACH) + _ = tm.MountOps.Unmount("/etc/resolv.conf", unix.MNT_DETACH) - if err := os.Remove(path); err != nil && !os.IsNotExist(err) { + if err := tm.FS.Remove(path); err != nil && !os.IsNotExist(err) { return fmt.Errorf("failed to remove temp resolv.conf file %s: %w", path, err) } return nil @@ -264,18 +276,18 @@ func UnmountResolvConf(path string) error { // BlockHostServices bind-mounts empty files/directories over sensitive host services // to prevent access from within the isolated namespace. -func BlockHostServices(pm *paths.PathManager, profile string) error { +func (tm *TunnelManager) BlockHostServices(pm *paths.PathManager, profile string) error { blockDirBase := filepath.Join(pm.RuntimeBaseDir(), "profiles", profile, "block") - if err := os.MkdirAll(blockDirBase, 0755); err != nil { + if err := tm.FS.MkdirAll(blockDirBase, 0755); err != nil { return fmt.Errorf("failed to create block base directory: %w", err) } - tmpDir, err := os.MkdirTemp(blockDirBase, "dir-") + tmpDir, err := tm.FS.MkdirTemp(blockDirBase, "dir-") if err != nil { return fmt.Errorf("failed to create temp block dir: %w", err) } - tmpFile, err := os.CreateTemp(blockDirBase, "file-") + tmpFile, err := tm.FS.CreateTemp(blockDirBase, "file-") if err != nil { return fmt.Errorf("failed to create temp block file: %w", err) } @@ -283,16 +295,16 @@ func BlockHostServices(pm *paths.PathManager, profile string) error { _ = tmpFile.Close() for _, p := range namespace.GetBlockPaths() { - stat, err := os.Stat(p) + stat, err := tm.FS.Stat(p) if err == nil { source := tmpFileName if stat.IsDir() { source = tmpDir } - if err := unix.Mount(source, p, "", unix.MS_BIND, ""); err != nil { + if err := tm.MountOps.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, "") + _ = tm.MountOps.Mount("", p, "", unix.MS_PRIVATE, "") } } } -- cgit v1.2.3