package cli import ( "flag" "fmt" "os" "os/exec" "git.theodohertyfamily.com/tools/wg-wrap/internal/config" "git.theodohertyfamily.com/tools/wg-wrap/internal/namespace" ) type App struct { Args []string ConfigDir string // Optional override for profile storage location RuntimeBaseDir string // Optional override for namespace/PID tracking } func NewApp(args []string) *App { return &App{Args: args} } func (a *App) Run() error { // 1. Validate arguments for null bytes to prevent exec failures in the C launcher for i, arg := range a.Args { for j := 0; j < len(arg); j++ { if arg[j] == 0 { return fmt.Errorf("argument %d contains null byte at position %d", i, j) } } } // Handle the internal diagnostic commands first if len(a.Args) > 1 { switch a.Args[1] { case "test-ns": ok, msg := namespace.VerifyIsolation() if !ok { return fmt.Errorf("isolation check failed: %s", msg) } fmt.Println("Isolation Verified: OK") return nil case "test-args": return namespace.VerifyArguments(a.Args) } } // Handle subcommands first (profile list, import, configure, delete, stop) if len(a.Args) > 1 && a.Args[1] == "profile" { return a.handleProfileCmd() } // ... cfg := &config.Config{} fs := flag.NewFlagSet("wg-wrap", flag.ExitOnError) fs.StringVar(&cfg.Profile, "profile", "", "WireGuard profile to use (filename without extension in ~/.config/wg-wrap/profiles/)") fs.StringVar(&cfg.DNSServer, "dns-server", "", "Override DNS server to use") args := a.Args[1:] sepIdx := -1 for i, arg := range args { if arg == "--" { sepIdx = i break } } var flagsToParse []string if sepIdx != -1 { flagsToParse = args[:sepIdx] cfg.Command = args[sepIdx+1:] } else { flagsToParse = args } err := fs.Parse(flagsToParse) if err != nil { return fmt.Errorf("error parsing flags: %w", err) } if sepIdx == -1 { cfg.Command = fs.Args() } if len(cfg.Command) == 0 { return fmt.Errorf("no command provided. use --help for usage") } if cfg.Profile == "" { cfg.Profile = "default" } if namespace.IsIsolated() { // Inject runtime base dir if provided if a.RuntimeBaseDir != "" { namespace.SetRuntimeBaseDir(a.RuntimeBaseDir) } return a.ExecuteCommand(cfg) } // If we are not isolated, we bootstrap. // The Bootstrap process will replace this process and restart it. if err := namespace.Bootstrap(); err != nil { return fmt.Errorf("bootstrap failed: %w", err) } // This point is never reached because Bootstrap uses syscall.Exec return nil } // ExecuteCommand handles the isolated execution of the target application. // This is called after the bootstrap loop has successfully isolated the process. func (a *App) ExecuteCommand(cfg *config.Config) error { if !namespace.IsIsolated() { return fmt.Errorf("ExecuteCommand called without namespace isolation") } // 1. Prepare the namespace namespace.PruneStalePids(cfg.Profile) if err := namespace.RegisterProcess(cfg.Profile); err != nil { return fmt.Errorf("failed to register process: %w", err) } // Ensure we unregister and check for cleanup on exit defer func() { namespace.UnregisterProcess(cfg.Profile) if last, err := namespace.IsLastProcess(cfg.Profile); err == nil && last { fmt.Printf("Last process exiting. Cleaning up profile %s...\n", cfg.Profile) // Here we would call namespace.UnpinNamespace(cfg.Profile) // and terminate the userspace WG process. } }() // 2. VPN Setup (Stubbed) fmt.Printf("Initializing WireGuard tunnel for profile %s...\n", cfg.Profile) // TODO: Integrate with internal/wireguard to set up TUN and WG-Go // 3. Execute the target command 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 [args]") } subCmd := a.Args[2] switch subCmd { case "list": fmt.Println("Listing profiles...") return fmt.Errorf("profile list not yet implemented") case "import": if len(a.Args) < 4 { return fmt.Errorf("usage: wg-wrap profile import ") } fmt.Printf("Importing profile from %s...\n", a.Args[3]) return fmt.Errorf("profile import not yet implemented") case "configure": if len(a.Args) < 4 { return fmt.Errorf("usage: wg-wrap profile configure ") } fmt.Printf("Configuring profile %s...\n", a.Args[3]) return fmt.Errorf("profile configure not yet implemented") case "delete": if len(a.Args) < 4 { return fmt.Errorf("usage: wg-wrap profile delete ") } fmt.Printf("Deleting profile %s...\n", a.Args[3]) return fmt.Errorf("profile delete not yet implemented") case "stop": if len(a.Args) < 4 { return fmt.Errorf("usage: wg-wrap profile stop ") } fmt.Printf("Stopping profile %s and unpinning namespace...\n", a.Args[3]) return fmt.Errorf("profile stop not yet implemented") default: return fmt.Errorf("unknown profile subcommand: %s", subCmd) } }