diff options
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/cli/cli.go | 230 | ||||
| -rw-r--r-- | internal/config/config.go | 8 | ||||
| -rw-r--r-- | internal/manager/manager.go | 238 | ||||
| -rw-r--r-- | internal/namespace/namespace.go | 21 | ||||
| -rw-r--r-- | internal/paths/paths.go | 14 | ||||
| -rw-r--r-- | internal/wireguard/wireguard.go | 33 |
6 files changed, 338 insertions, 206 deletions
diff --git a/internal/cli/cli.go b/internal/cli/cli.go index edf6048..1873e28 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -1,3 +1,5 @@ +// Package cli implements the command-line interface and bootstrap orchestration +// for wg-wrap. package cli import ( @@ -9,26 +11,37 @@ import ( "strings" "git.theodohertyfamily.com/wg-wrap/internal/config" + "git.theodohertyfamily.com/wg-wrap/internal/manager" "git.theodohertyfamily.com/wg-wrap/internal/namespace" "git.theodohertyfamily.com/wg-wrap/internal/paths" - "git.theodohertyfamily.com/wg-wrap/internal/wireguard" "git.theodohertyfamily.com/wg-wrap/pkg/wgconf" ) +// App handles the CLI interaction and orchestration for wg-wrap. type App struct { Args []string ConfigDir string // Optional override for profile storage location RuntimeBaseDir string // Optional override for namespace/PID tracking + mgr *manager.Manager } +// NewApp creates a new App instance with the provided arguments. func NewApp(args []string) *App { return &App{Args: args} } +func (a *App) getManager() *manager.Manager { + if a.mgr == nil { + a.mgr = manager.New(a.getPathManager()) + } + return a.mgr +} + func (a *App) getPathManager() *paths.PathManager { return paths.NewPathManager(a.ConfigDir, a.RuntimeBaseDir) } +// Route parses the command line arguments and routes to the appropriate handler. func (a *App) Route() error { for i, arg := range a.Args { for j := 0; j < len(arg); j++ { @@ -50,6 +63,8 @@ func (a *App) Route() error { return a.Run() } +// Run executes the main logic of wg-wrap, including bootstrapping the namespace +// and launching the wrapped command. func (a *App) Run() error { if len(a.Args) > 1 { switch a.Args[1] { @@ -73,8 +88,14 @@ func (a *App) Run() error { } return namespace.VerifyArguments(a.Args) case "test-lifecycle": - pm := a.getPathManager() - return a.verifyLifecycle(pm) + profile := "default" + for i := 0; i < len(a.Args)-1; i++ { + if a.Args[i] == "--profile" && i+1 < len(a.Args) { + profile = a.Args[i+1] + break + } + } + return a.getManager().VerifyLifecycle(profile) } } @@ -123,39 +144,11 @@ func (a *App) Run() error { } if namespace.IsIsolated() { - return a.ExecuteCommand(cfg) - } - - pm := a.getPathManager() - - // Preserve the host runtime base dir in the environment before bootstrapping - _ = os.Setenv("WG_WRAP_HOST_RUNTIME_BASE_DIR", pm.RuntimeBaseDir()) - - // Acquire startup lock to prevent concurrent bootstrap/joining races - lockFile, lockErr := namespace.AcquireProfileLock(pm, cfg.Profile) - if lockErr == nil { - defer namespace.ReleaseProfileLock(lockFile) - } - - // Before bootstrapping, see if an active namespace/process for the profile exists. - // If yes, we can join it! - activePid, err := namespace.FindActiveProfilePid(pm, cfg.Profile) - if err == nil && activePid > 0 { - // Release the lock before executing the command to allow others to join - namespace.ReleaseProfileLock(lockFile) - - // Register this PID before joining to prevent the race where the joining process - // hasn't registered itself yet, causing the existing process to think it's the last one. - _ = namespace.RegisterProcess(pm, cfg.Profile) - - if err := namespace.BootstrapJoin(activePid); err != nil { - return fmt.Errorf("failed to join existing namespace: %w", err) - } - return nil + return a.getManager().Execute(cfg, a.isVerbose()) } - if err := namespace.Bootstrap(); err != nil { - return fmt.Errorf("bootstrap failed: %w", err) + if err := a.getManager().Bootstrap(cfg); err != nil { + return err } return nil @@ -165,154 +158,6 @@ func (a *App) isVerbose() bool { return os.Getenv("WG_WRAP_VERBOSE") == "1" } -func (a *App) ExecuteCommand(cfg *config.Config) error { - if !namespace.IsIsolated() { - return fmt.Errorf("ExecuteCommand called without namespace isolation") - } - - pm := a.getPathManager() - - // Acquire execution lock during configuration and startup inside the namespace - lockFile, lockErr := namespace.AcquireProfileLock(pm, cfg.Profile) - var lockFileReleased bool - if lockErr == nil { - defer func() { - if !lockFileReleased { - namespace.ReleaseProfileLock(lockFile) - } - }() - } - - if err := namespace.PruneStalePids(pm, cfg.Profile); err != nil { - fmt.Fprintf(os.Stderr, "failed to prune stale pids: %v\n", err) - } - if err := namespace.RegisterProcess(pm, cfg.Profile); err != nil { - return fmt.Errorf("failed to register process: %w", err) - } - - defer func() { - var cleanupLock *os.File - var cleanupErr error - - if lockErr == nil && !lockFileReleased { - // We already hold the lock, so we can just reuse lockFile for cleanup! - cleanupLock = lockFile - } else { - // Re-acquire lock for the entire cleanup sequence to ensure atomic unregister and unpin - cleanupLock, cleanupErr = namespace.AcquireProfileLock(pm, cfg.Profile) - } - - if cleanupErr == nil { - // 1. Unregister the process first. - if err := namespace.UnregisterProcess(pm, cfg.Profile); err != nil { - fmt.Fprintf(os.Stderr, "failed to unregister process: %v\n", err) - } - - // 2. Prune and check if we are the last process. - if err := namespace.PruneStalePids(pm, cfg.Profile); err != nil { - fmt.Fprintf(os.Stderr, "failed to prune stale pids during cleanup: %v\n", err) - } - - last, lastErr := namespace.IsLastProcess(pm, cfg.Profile) - - if lastErr == nil && last { - if err := namespace.UnpinNamespace(pm, cfg.Profile); err != nil { - fmt.Fprintf(os.Stderr, "failed to unpin namespace: %v\n", err) - } - } - if lockErr == nil && !lockFileReleased { - lockFileReleased = true - } - namespace.ReleaseProfileLock(cleanupLock) - } else { - // Fallback if lock fails to ensure we still unregister - if err := namespace.UnregisterProcess(pm, cfg.Profile); err != nil { - fmt.Fprintf(os.Stderr, "failed to unregister process: %v\n", err) - } - } - }() - - if os.Getenv("WG_WRAP_JOINED") == "1" { - if a.isVerbose() { - fmt.Printf("Joining active WireGuard tunnel session for profile %s...\n", cfg.Profile) - } - } else { - if a.isVerbose() { - fmt.Printf("Initializing WireGuard tunnel for profile %s...\n", cfg.Profile) - } - - // Parse the profile configuration - profilesDir := pm.ConfigDir() - profilePath := filepath.Join(profilesDir, cfg.Profile+".conf") - - // Create tunnel if the file exists - if _, err := os.Stat(profilePath); err == nil { - wgCfg, err := wgconf.Parse(profilePath) - if err != nil { - return fmt.Errorf("failed to parse profile %s: %w", cfg.Profile, err) - } - - // Start the WireGuard userspace device & routing table setup - dnsServer := cfg.DNSServer - if dnsServer == "" { - dnsServer = wgCfg.DNS - } - if dnsServer == "" { - dnsServer = "1.1.1.1" // Fallback to safe public DNS to prevent leaks - hasDefaultRoute := false - for _, peer := range wgCfg.Peers { - for _, ip := range peer.AllowedIPs { - trimmed := strings.TrimSpace(ip) - if trimmed == "0.0.0.0/0" || trimmed == "::/0" { - hasDefaultRoute = true - break - } - } - if hasDefaultRoute { - break - } - } - if !hasDefaultRoute { - fmt.Fprintf(os.Stderr, "warning: Falling back to 1.1.1.1, but your profile does not route all traffic (0.0.0.0/0). DNS resolution may fail.\n") - } - } - - tunnel, err := wireguard.StartTunnel(pm, cfg.Profile, wgCfg, dnsServer) - if err != nil { - return fmt.Errorf("failed to start WireGuard tunnel: %w", err) - } - defer tunnel.Close() - - // Pin the namespace so others can join it - if err := namespace.PinNamespace(pm, cfg.Profile); err != nil { - fmt.Fprintf(os.Stderr, "warning: failed to pin namespace: %v\n", err) - } - } else { - // If profile is not default or it was explicitly requested but doesn't exist, we error - if cfg.Profile != "default" { - return fmt.Errorf("profile %s not found: %w", cfg.Profile, err) - } - fmt.Fprintf(os.Stderr, "warning: default profile configuration not found. Executing command in bare isolation.\n") - } - } - - // We can now release the startup lock and execute the command - lockFileReleased = true - namespace.ReleaseProfileLock(lockFile) - - cmd := exec.Command(cfg.Command[0], cfg.Command[1:]...) - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - cmd.Env = os.Environ() - - if err := cmd.Run(); err != nil { - return fmt.Errorf("command execution failed: %w", err) - } - - return nil -} - func (a *App) handleProfileCmd() error { if len(a.Args) < 3 { return fmt.Errorf("usage: wg-wrap profile <list|import|configure|delete|stop> [args]") @@ -349,8 +194,7 @@ func (a *App) handleProfileCmd() error { return fmt.Errorf("invalid profile name: %q", a.Args[3]) } fmt.Printf("Stopping profile %s and unpinning namespace...\n", a.Args[3]) - pm := a.getPathManager() - if err := namespace.UnpinNamespace(pm, a.Args[3]); err != nil { + if err := a.getManager().StopProfile(a.Args[3]); err != nil { return fmt.Errorf("failed to stop profile: %w", err) } fmt.Printf("Profile %s stopped and unpinned.\n", a.Args[3]) @@ -478,24 +322,6 @@ func (a *App) handleProfileDelete(name string) error { return nil } -func (a *App) verifyLifecycle(pm *paths.PathManager) error { - profile := "default" - for i := 0; i < len(a.Args)-1; i++ { - if a.Args[i] == "--profile" && i+1 < len(a.Args) { - profile = a.Args[i+1] - break - } - } - - activePid, err := namespace.FindActiveProfilePid(pm, profile) - if err != nil || activePid <= 0 { - return fmt.Errorf("no active session found for profile %s", profile) - } - - fmt.Printf("Active session found for profile %s (PID: %d)\n", profile, activePid) - return nil -} - func (a *App) showConfig() error { cfg := &config.Config{} fs := flag.NewFlagSet("wg-wrap", flag.ExitOnError) diff --git a/internal/config/config.go b/internal/config/config.go index 5aa8462..d83c59d 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,7 +1,11 @@ package config +// Config holds the application-level configuration for a single execution run. type Config struct { - Profile string + // Profile is the name of the WireGuard profile to use. + Profile string + // DNSServer is an optional override for the DNS server. DNSServer string - Command []string + // Command is the actual command and arguments to be executed within the namespace. + Command []string } diff --git a/internal/manager/manager.go b/internal/manager/manager.go new file mode 100644 index 0000000..99a1a32 --- /dev/null +++ b/internal/manager/manager.go @@ -0,0 +1,238 @@ +// Package manager orchestrates the high-level lifecycle of WireGuard tunnels +// and their associated network namespaces. +// +// Architecture: +// wg-wrap provides a transparent data path: +// Linux Application -> Linux Kernel Routing -> TUN Device -> Userspace WireGuard -> UDP Socket -> Internet. +// +// Persistent Namespaces & Shared Sessions: +// To support multiple concurrent commands on the same WireGuard tunnel without re-establishing +// connections, wg-wrap employs session-based persistent, unprivileged namespaces. +// +// 1. Tracking: Process usage is tracked using active PID files inside the runtime base directory. +// 2. Ref-Counting & Cleanup: Active PIDs are regularly pruned. When the last active process exits, +// the namespace is unpinned and resources are reclaimed. +// 3. Setns Join: When a new process is executed on an active profile, it discovers an active PID +// and attaches itself to the existing User, Mount, and Network namespaces of the active tunnel. +package manager + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + + "git.theodohertyfamily.com/wg-wrap/internal/config" + "git.theodohertyfamily.com/wg-wrap/internal/namespace" + "git.theodohertyfamily.com/wg-wrap/internal/paths" + "git.theodohertyfamily.com/wg-wrap/internal/wireguard" + "git.theodohertyfamily.com/wg-wrap/pkg/wgconf" +) + +// Manager orchestrates the high-level lifecycle of WireGuard tunnels +// and their associated network namespaces. +type Manager struct { + // PM is the path manager used to resolve configuration and runtime directories. + PM *paths.PathManager +} + +// New creates a new Manager with the given path manager. +func New(pm *paths.PathManager) *Manager { + return &Manager{PM: pm} +} + +// Bootstrap ensures the process is running in an isolated user and network namespace. +// If an active session already exists for the profile, it joins it. +func (m *Manager) Bootstrap(cfg *config.Config) error { + if namespace.IsIsolated() { + return nil + } + + // Preserve the host runtime base dir in the environment before bootstrapping. + _ = os.Setenv("WG_WRAP_HOST_RUNTIME_BASE_DIR", m.PM.RuntimeBaseDir()) + + // Acquire startup lock to prevent concurrent bootstrap/joining races. + lockFile, lockErr := namespace.AcquireProfileLock(m.PM, cfg.Profile) + if lockErr == nil { + defer namespace.ReleaseProfileLock(lockFile) + } + + // Before bootstrapping, see if an active namespace/process for the profile exists. + activePid, err := namespace.FindActiveProfilePid(m.PM, cfg.Profile) + if err == nil && activePid > 0 { + // Release the lock before executing the command to allow others to join. + namespace.ReleaseProfileLock(lockFile) + + // Register this PID before joining to prevent the race where the joining process + // hasn't registered itself yet, causing the existing process to think it's the last one. + _ = namespace.RegisterProcess(m.PM, cfg.Profile) + + if err := namespace.BootstrapJoin(activePid); err != nil { + return fmt.Errorf("failed to join existing namespace: %w", err) + } + return nil + } + + if err := namespace.Bootstrap(); err != nil { + return fmt.Errorf("bootstrap failed: %w", err) + } + + return nil +} + +// Execute manages the full execution lifecycle inside an isolated namespace: +// lock acquisition, PID registration, tunnel initialization, command execution, and cleanup. +func (m *Manager) Execute(cfg *config.Config, verbose bool) error { + if !namespace.IsIsolated() { + return fmt.Errorf("Execute called without namespace isolation") + } + + // Acquire execution lock during configuration and startup inside the namespace. + lockFile, lockErr := namespace.AcquireProfileLock(m.PM, cfg.Profile) + var lockFileReleased bool + if lockErr == nil { + defer func() { + if !lockFileReleased { + namespace.ReleaseProfileLock(lockFile) + } + }() + } + + if err := namespace.PruneStalePids(m.PM, cfg.Profile); err != nil { + fmt.Fprintf(os.Stderr, "failed to prune stale pids: %v\n", err) + } + if err := namespace.RegisterProcess(m.PM, cfg.Profile); err != nil { + return fmt.Errorf("failed to register process: %w", err) + } + + defer func() { + var cleanupLock *os.File + var cleanupErr error + + if lockErr == nil && !lockFileReleased { + cleanupLock = lockFile + } else { + cleanupLock, cleanupErr = namespace.AcquireProfileLock(m.PM, cfg.Profile) + } + + if cleanupErr == nil { + if err := namespace.UnregisterProcess(m.PM, cfg.Profile); err != nil { + fmt.Fprintf(os.Stderr, "failed to unregister process: %v\n", err) + } + + if err := namespace.PruneStalePids(m.PM, cfg.Profile); err != nil { + fmt.Fprintf(os.Stderr, "failed to prune stale pids during cleanup: %v\n", err) + } + + last, lastErr := namespace.IsLastProcess(m.PM, cfg.Profile) + + if lastErr == nil && last { + if err := namespace.UnpinNamespace(m.PM, cfg.Profile); err != nil { + fmt.Fprintf(os.Stderr, "failed to unpin namespace: %v\n", err) + } + } + if lockErr == nil && !lockFileReleased { + lockFileReleased = true + } + namespace.ReleaseProfileLock(cleanupLock) + } else { + if err := namespace.UnregisterProcess(m.PM, cfg.Profile); err != nil { + fmt.Fprintf(os.Stderr, "failed to unregister process: %v\n", err) + } + } + }() + + if os.Getenv("WG_WRAP_JOINED") == "1" { + if verbose { + fmt.Printf("Joining active WireGuard tunnel session for profile %s...\n", cfg.Profile) + } + } else { + if verbose { + fmt.Printf("Initializing WireGuard tunnel for profile %s...\n", cfg.Profile) + } + + profilesDir := m.PM.ConfigDir() + profilePath := filepath.Join(profilesDir, cfg.Profile+".conf") + + if _, err := os.Stat(profilePath); err == nil { + wgCfg, err := wgconf.Parse(profilePath) + if err != nil { + return fmt.Errorf("failed to parse profile %s: %w", cfg.Profile, err) + } + + dnsServer := cfg.DNSServer + if dnsServer == "" { + dnsServer = wgCfg.DNS + } + if dnsServer == "" { + dnsServer = "1.1.1.1" + hasDefaultRoute := false + for _, peer := range wgCfg.Peers { + for _, ip := range peer.AllowedIPs { + trimmed := strings.TrimSpace(ip) + if trimmed == "0.0.0.0/0" || trimmed == "::/0" { + hasDefaultRoute = true + break + } + } + if hasDefaultRoute { + break + } + } + if !hasDefaultRoute { + fmt.Fprintf(os.Stderr, "warning: Falling back to 1.1.1.1, but your profile does not route all traffic (0.0.0.0/0). DNS resolution may fail.\n") + } + } + + tunnel, err := wireguard.StartTunnel(m.PM, cfg.Profile, wgCfg, dnsServer) + if err != nil { + return fmt.Errorf("failed to start WireGuard tunnel: %w", err) + } + defer tunnel.Close() + + if err := namespace.PinNamespace(m.PM, cfg.Profile); err != nil { + fmt.Fprintf(os.Stderr, "warning: failed to pin namespace: %v\n", err) + } + } else { + if cfg.Profile != "default" { + return fmt.Errorf("profile %s not found", cfg.Profile) + } + fmt.Fprintf(os.Stderr, "warning: default profile configuration not found. Executing command in bare isolation.\n") + } + } + + lockFileReleased = true + namespace.ReleaseProfileLock(lockFile) + + cmd := exec.Command(cfg.Command[0], cfg.Command[1:]...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Env = os.Environ() + + if err := cmd.Run(); err != nil { + return fmt.Errorf("command execution failed: %w", err) + } + + return nil +} + +// StopProfile stops a profile session by unpinning its namespace. +func (m *Manager) StopProfile(profile string) error { + if err := namespace.UnpinNamespace(m.PM, profile); err != nil { + return fmt.Errorf("failed to stop profile: %w", err) + } + return nil +} + +// VerifyLifecycle checks for an active session for the given profile. +func (m *Manager) VerifyLifecycle(profile string) error { + activePid, err := namespace.FindActiveProfilePid(m.PM, profile) + if err != nil || activePid <= 0 { + return fmt.Errorf("no active session found for profile %s", profile) + } + + fmt.Printf("Active session found for profile %s (PID: %d)\n", profile, activePid) + return nil +} diff --git a/internal/namespace/namespace.go b/internal/namespace/namespace.go index 54414a9..b05dea2 100644 --- a/internal/namespace/namespace.go +++ b/internal/namespace/namespace.go @@ -1,3 +1,24 @@ +// Package namespace provides primitives for managing Linux user and network +// namespaces, including bootstrapping and pinning. +// +// Rootless Bootstrap Loop & Host-Socket Preservation: +// To achieve rootless network isolation without interfering with the Go runtime's multi-threaded +// scheduler, and to maintain encrypted UDP socket connectivity over the host's network, +// wg-wrap employs an advanced bootstrap loop: +// +// 1. Host-Bound Socket Creation: During the initial host-level start, a UDP socket is opened +// on 0.0.0.0:0 on the host, and its FD is stored in the environment (WG_WRAP_HOST_SOCKET_FD). +// 2. Helper Deployment: An embedded single-threaded C launcher is used to bridge the transition. +// 3. Namespace Transition: The process replaces itself with the C launcher via syscall.Exec. +// 4. Isolation: The launcher performs the unshare(CLONE_NEWUSER | CLONE_NEWNS | CLONE_NEWNET) +// sequence to isolate Mount, User, and Network environments. +// 5. Re-entry: The launcher then execvp's the original wg-wrap binary. +// 6. FDBind Tunnel Initialization: The second instance of wg-wrap wraps the host socket FD +// inside a custom FDBind struct to initialize wireguard-go. +// +// User Namespace Sequence: +// To create a network namespace without root, wg-wrap follows the sequence: +// CLONE_NEWUSER -> CLONE_NEWNET -> Setuid/Setgid -> Configure Interfaces. package namespace import ( diff --git a/internal/paths/paths.go b/internal/paths/paths.go index c7bdd94..67f6da6 100644 --- a/internal/paths/paths.go +++ b/internal/paths/paths.go @@ -1,3 +1,13 @@ +// Package paths handles the resolution of configuration and runtime directories, +// providing a consistent way to locate profile and PID tracking files. +// +// Profile Storage: +// Profiles are stored as standard .conf files in ~/.config/wg-wrap/profiles/. +// +// PID Tracking: +// To manage namespace lifecycles and shared sessions, wg-wrap tracks active processes +// using PID files located in the runtime base directory: +// /run/user/$UID/wg-wrap/profiles/<profile-name>/pids/ package paths import ( @@ -9,7 +19,9 @@ import ( // PathManager handles the resolution of configuration and runtime directories. // By using a struct, we can instantiate different managers for parallel tests. type PathManager struct { - ConfigDirOverride string + // ConfigDirOverride allows overriding the default config directory (usually XDG_CONFIG_HOME/wg-wrap/profiles). + ConfigDirOverride string + // RuntimeBaseOverride allows overriding the default runtime directory (usually XDG_RUNTIME_DIR). RuntimeBaseOverride string } diff --git a/internal/wireguard/wireguard.go b/internal/wireguard/wireguard.go index 0c341f2..cea9590 100644 --- a/internal/wireguard/wireguard.go +++ b/internal/wireguard/wireguard.go @@ -1,5 +1,24 @@ //go:build linux +// Package wireguard provides the userspace WireGuard implementation and TUN device binding. +// +// Data Flow: +// 1. Egress: A process sends a packet. The Linux kernel routes it via tun0. The userspace +// WireGuard device reads this packet, encrypts it, and sends it as a UDP packet to the +// remote endpoint via the preserved host socket. +// 2. Ingress: A UDP packet arrives via the host socket. The userspace WireGuard device +// decrypts it and writes the raw IP packet back into the TUN device, delivering it to +// the process. +// +// MTU Management: +// WireGuard adds overhead. To prevent fragmentation and packet loss, the TUN device +// MTU is set to 1420 bytes. +// +// DNS Isolation: +// To prevent DNS leaks, wg-wrap isolates the namespace's DNS resolution by: +// 1. Creating a temporary resolv.conf within the profile's runtime directory. +// 2. Bind-mounting this file over /etc/resolv.conf inside the namespace. +// 3. Falling back to trusted public DNS (e.g., 1.1.1.1) if no DNS server is configured. package wireguard import ( @@ -25,7 +44,9 @@ import ( // Tunnel represents an active Userspace WireGuard tunnel inside a network namespace. type Tunnel struct { - Device *device.Device + // Device is the wireguard-go device instance. + Device *device.Device + // Tun is the underlying TUN device. Tun tun.Device dnsFile string } @@ -241,6 +262,7 @@ func GetTunnelLocalIP(cfg *wgconf.Config) (string, error) { return ip.String(), nil } +// ConfigureResolvConf creates a temporary resolv.conf file and bind-mounts it to /etc/resolv.conf. func ConfigureResolvConf(dns string, profileDir string) (string, error) { if dns == "" { return "", nil @@ -271,6 +293,7 @@ func ConfigureResolvConf(dns string, profileDir string) (string, error) { return launcherPath, nil } +// UnmountResolvConf unmounts the bind-mounted resolv.conf and removes the temporary file. func UnmountResolvConf(path string) error { if path == "" { return nil @@ -286,6 +309,8 @@ func UnmountResolvConf(path string) error { return nil } +// 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 { blockDirBase := filepath.Join(pm.RuntimeBaseDir(), "profiles", profile, "block") if err := os.MkdirAll(blockDirBase, 0755); err != nil { @@ -321,8 +346,10 @@ func BlockHostServices(pm *paths.PathManager, profile string) error { return nil } +// HostBind is a placeholder bind implementation for WireGuard. type HostBind struct{} +// NewHostBind creates a new HostBind instance. func NewHostBind(inner conn.Bind, hostNetNSFd int) *HostBind { return &HostBind{} } @@ -337,11 +364,14 @@ func (h *HostBind) Send(bufs [][]byte, endpoint conn.Endpoint) error { return ni func (h *HostBind) ParseEndpoint(s string) (conn.Endpoint, error) { return nil, nil } func (h *HostBind) BatchSize() int { return 0 } +// FDBind implements wireguard-go's conn.Bind using an existing file descriptor. +// This allows the tunnel to use a UDP socket opened on the host. type FDBind struct { originalFd int conn *net.UDPConn } +// FDEndpoint implements wireguard-go's conn.Endpoint for file-descriptor based binds. type FDEndpoint struct { addr netip.AddrPort } @@ -354,6 +384,7 @@ func (e *FDEndpoint) SrcIP() netip.Addr { return netip.Addr{} } func (e *FDEndpoint) SrcToString() string { return "" } func (e *FDEndpoint) SrcIfidx() int32 { return 0 } +// NewFDBind creates a new FDBind instance from a raw file descriptor. func NewFDBind(fd int) (*FDBind, error) { return &FDBind{originalFd: fd}, nil } |
