//go:build linux package namespace import ( "fmt" "os" "path/filepath" "strconv" "git.theodohertyfamily.com/wg-wrap/internal/paths" "golang.org/x/sys/unix" ) // blockPaths defines the host services that are bind-mounted over to block access // from within the isolated namespace. var blockPaths = []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", } // GetBlockPaths returns the list of paths blocked for namespace isolation. func GetBlockPaths() []string { return blockPaths } // PinNamespace binds the current network namespace to the profile's namespace path. // This prevents the kernel from destroying the namespace when all processes exit. func PinNamespace(pm *paths.PathManager, profile string) error { nsPath := GetProfileNamespacePath(pm, profile) profilesDir := filepath.Dir(nsPath) if err := os.MkdirAll(profilesDir, 0755); err != nil { return fmt.Errorf("failed to create profiles directory: %w", err) } // 1. Create an empty file to serve as the mount point if err := os.WriteFile(nsPath, []byte(""), 0644); err != nil { return fmt.Errorf("failed to create namespace pin file: %w", err) } // 2. Bind-mount the current network namespace to the file. // This increments the kernel's reference count for the namespace. if err := unix.Mount("/proc/self/ns/net", nsPath, "", unix.MS_BIND, ""); err != nil { return fmt.Errorf("failed to bind-mount network namespace: %w", err) } return nil } // UnpinNamespace unmounts and removes the pinned namespace file. func UnpinNamespace(pm *paths.PathManager, profile string) error { nsPath := GetProfileNamespacePath(pm, profile) if _, err := os.Stat(nsPath); os.IsNotExist(err) { return nil } // 1. Unmount the namespace first. if err := unix.Unmount(nsPath, 0); err != nil { return fmt.Errorf("failed to unmount namespace %s: %w", nsPath, err) } // 2. Remove the mount point file. if err := os.Remove(nsPath); err != nil { return fmt.Errorf("failed to remove pin file %s: %w", nsPath, err) } // 3. Unmount and clean up blocking services. // Since the block files are located within the profile directory, // we must unmount them before we can remove the directory. for _, p := range GetBlockPaths() { _ = unix.Unmount(p, unix.MNT_DETACH) } blockDir := filepath.Join(pm.RuntimeBaseDir(), "profiles", profile, "block") _ = os.RemoveAll(blockDir) pidsDir := GetPidsDirPath(pm, profile) // Try to remove pids directory and empty parent directories _ = os.Remove(pidsDir) _ = os.Remove(filepath.Dir(pidsDir)) _ = os.Remove(filepath.Dir(filepath.Dir(pidsDir))) return nil } // FindActiveProfilePid looks for an active PID running under the specified profile. // Returns 0 if no active process is found. func FindActiveProfilePid(pm *paths.PathManager, profile string) (int, error) { if err := PruneStalePids(pm, profile); err != nil { return 0, fmt.Errorf("failed to prune stale pids: %w", err) } pidsDir := GetPidsDirPath(pm, profile) files, err := os.ReadDir(pidsDir) if err != nil { if os.IsNotExist(err) { return 0, nil } return 0, fmt.Errorf("failed to read pids dir: %w", err) } for _, file := range files { pid, err := strconv.Atoi(file.Name()) if err != nil { continue } // Since we already pruned stale pids, the first file we find is an active pid! return pid, nil } return 0, nil }