summaryrefslogtreecommitdiff
path: root/internal/cli/cli.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/cli/cli.go')
-rw-r--r--internal/cli/cli.go178
1 files changed, 123 insertions, 55 deletions
diff --git a/internal/cli/cli.go b/internal/cli/cli.go
index 13a4a6b..66b5f79 100644
--- a/internal/cli/cli.go
+++ b/internal/cli/cli.go
@@ -5,12 +5,15 @@ import (
"fmt"
"os"
"os/exec"
+ "path/filepath"
+ "strings"
"git.theodohertyfamily.com/tools/wg-wrap/internal/config"
"git.theodohertyfamily.com/tools/wg-wrap/internal/namespace"
+ "git.theodohertyfamily.com/tools/wg-wrap/internal/paths"
+ "git.theodohertyfamily.com/tools/wg-wrap/pkg/wgconf"
)
-
type App struct {
Args []string
ConfigDir string // Optional override for profile storage location
@@ -21,8 +24,11 @@ func NewApp(args []string) *App {
return &App{Args: args}
}
+func (a *App) getPathManager() *paths.PathManager {
+ return paths.NewPathManager(a.ConfigDir, a.RuntimeBaseDir)
+}
+
func (a *App) Route() 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 {
@@ -31,7 +37,6 @@ func (a *App) Route() error {
}
}
- // Handle the internal diagnostic commands that should run on the HOST
if len(a.Args) > 1 {
switch a.Args[1] {
case "show-config":
@@ -39,18 +44,14 @@ func (a *App) Route() error {
}
}
- // Handle subcommands first (profile list, import, configure, delete, stop)
if len(a.Args) > 1 && a.Args[1] == "profile" {
return a.handleProfileCmd()
}
- // If we reach here, we are either wrapping a process or running a command
- // that requires isolation (e.g., test-ns, test-args).
return a.Run()
}
func (a *App) Run() error {
- // Handle the internal diagnostic commands that require ISOLATION
if len(a.Args) > 1 {
switch a.Args[1] {
case "test-ns":
@@ -78,7 +79,7 @@ func (a *App) Run() error {
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.Profile, "profile", "", "WireGuard profile to use")
fs.StringVar(&cfg.DNSServer, "dns-server", "", "Override DNS server to use")
args := a.Args[1:]
@@ -115,57 +116,46 @@ func (a *App) Run() error {
cfg.Profile = "default"
}
- // If we are already isolated, we enter the execution phase.
if namespace.IsIsolated() {
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
- baseDir := a.RuntimeBaseDir
- if baseDir == "" {
- // Use XDG_RUNTIME_DIR or default via the namespace package
- // Since the namespace package now handles the default in GetProfileNamespacePath,
- // we can pass empty string if no override is present.
- baseDir = ""
- }
+ pm := a.getPathManager()
- namespace.PruneStalePids(baseDir, cfg.Profile)
- if err := namespace.RegisterProcess(baseDir, cfg.Profile); err != nil {
+ if err := namespace.PruneStalePids(pm, cfg.Profile); err != nil {
+ fmt.Printf("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)
}
- // Ensure we unregister and check for cleanup on exit
defer func() {
- namespace.UnregisterProcess(baseDir, cfg.Profile)
- if last, err := namespace.IsLastProcess(baseDir, cfg.Profile); err == nil && last {
+ if err := namespace.UnregisterProcess(pm, cfg.Profile); err != nil {
+ fmt.Printf("failed to unregister process: %v\n", err)
+ }
+ if last, err := namespace.IsLastProcess(pm, cfg.Profile); err == nil && last {
fmt.Printf("Last process exiting. Cleaning up profile %s...\n", cfg.Profile)
- // Here we would call namespace.UnpinNamespace(baseDir, cfg.Profile)
- // and terminate the userspace WG process.
+ if err := namespace.UnpinNamespace(pm, cfg.Profile); err != nil {
+ fmt.Printf("failed to unpin namespace: %v\n", err)
+ }
}
}()
- // 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
@@ -187,69 +177,147 @@ func (a *App) handleProfileCmd() error {
subCmd := a.Args[2]
switch subCmd {
case "list":
- fmt.Println("Listing profiles...")
- return fmt.Errorf("profile list not yet implemented")
+ return a.handleProfileList()
case "import":
if len(a.Args) < 4 {
return fmt.Errorf("usage: wg-wrap profile import <path>")
}
- fmt.Printf("Importing profile from %s...\n", a.Args[3])
- return fmt.Errorf("profile import not yet implemented")
+ return a.handleProfileImport(a.Args[3])
case "configure":
if len(a.Args) < 4 {
return fmt.Errorf("usage: wg-wrap profile configure <name>")
}
- fmt.Printf("Configuring profile %s...\n", a.Args[3])
- return fmt.Errorf("profile configure not yet implemented")
+ return a.handleProfileConfigure(a.Args[3])
case "delete":
if len(a.Args) < 4 {
return fmt.Errorf("usage: wg-wrap profile delete <name>")
}
- fmt.Printf("Deleting profile %s...\n", a.Args[3])
- return fmt.Errorf("profile delete not yet implemented")
+ return a.handleProfileDelete(a.Args[3])
case "stop":
if len(a.Args) < 4 {
return fmt.Errorf("usage: wg-wrap profile stop <name>")
}
fmt.Printf("Stopping profile %s and unpinning namespace...\n", a.Args[3])
- return fmt.Errorf("profile stop not yet implemented")
+ pm := a.getPathManager()
+ if err := namespace.UnpinNamespace(pm, 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])
+ return nil
default:
return fmt.Errorf("unknown profile subcommand: %s", subCmd)
}
}
+func (a *App) handleProfileConfigure(name string) error {
+ profilesDir := a.getPathManager().ConfigDir()
+ profilePath := filepath.Join(profilesDir, name+".conf")
+ if _, err := os.Stat(profilePath); os.IsNotExist(err) {
+ return fmt.Errorf("profile '%s' not found", name)
+ }
+
+ cfg, err := wgconf.Parse(profilePath)
+ if err != nil {
+ return fmt.Errorf("failed to parse profile %s: %w", name, err)
+ }
+
+ fmt.Printf("Editing profile %s...\n", name)
+ fmt.Println("DNS server (current: '" + cfg.DNS + "'):")
+
+ return fmt.Errorf("interactive configuration not supported in this environment, use a config file")
+}
+
+func (a *App) handleProfileList() error {
+ profilesDir := a.getPathManager().ConfigDir()
+ entries, err := os.ReadDir(profilesDir)
+ if err != nil {
+ return fmt.Errorf("failed to read profiles directory %s: %w", profilesDir, err)
+ }
+
+ fmt.Println("Available profiles:")
+ found := false
+ for _, entry := range entries {
+ if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".conf") {
+ name := strings.TrimSuffix(entry.Name(), ".conf")
+ fmt.Printf("- %s\n", name)
+ found = true
+ }
+ }
+
+ if !found {
+ fmt.Println(" (no profiles found)")
+ }
+
+ return nil
+}
+
+func (a *App) handleProfileImport(srcPath string) error {
+ profilesDir := a.getPathManager().ConfigDir()
+ if err := os.MkdirAll(profilesDir, 0755); err != nil {
+ return fmt.Errorf("failed to create profiles directory: %w", err)
+ }
+
+ if _, err := wgconf.Parse(srcPath); err != nil {
+ return fmt.Errorf("invalid WireGuard configuration at %s: %w", srcPath, err)
+ }
+
+ baseName := filepath.Base(srcPath)
+ name := strings.TrimSuffix(baseName, filepath.Ext(baseName))
+ if name == "" {
+ return fmt.Errorf("invalid source filename")
+ }
+
+ destPath := filepath.Join(profilesDir, name+".conf")
+ data, err := os.ReadFile(srcPath)
+ if err != nil {
+ return fmt.Errorf("failed to read source file: %w", err)
+ }
+
+ if err := os.WriteFile(destPath, data, 0644); err != nil {
+ return fmt.Errorf("failed to write profile to %s: %w", destPath, err)
+ }
+
+ fmt.Printf("Profile '%s' imported successfully to %s\n", name, destPath)
+ return nil
+}
+
+func (a *App) handleProfileDelete(name string) error {
+ profilesDir := a.getPathManager().ConfigDir()
+ destPath := filepath.Join(profilesDir, name+".conf")
+
+ if _, err := os.Stat(destPath); os.IsNotExist(err) {
+ return fmt.Errorf("profile '%s' not found", name)
+ }
+
+ if err := os.Remove(destPath); err != nil {
+ return fmt.Errorf("failed to delete profile %s: %w", name, err)
+ }
+
+ fmt.Printf("Profile '%s' deleted successfully\n", name)
+ return nil
+}
+
func (a *App) showConfig() error {
cfg := &config.Config{}
fs := flag.NewFlagSet("wg-wrap", flag.ExitOnError)
fs.StringVar(&cfg.Profile, "profile", "default", "WireGuard profile to use")
fs.StringVar(&cfg.DNSServer, "dns-server", "", "Override DNS server to use")
- // Parse the arguments that follow 'show-config'
if len(a.Args) > 2 {
_ = fs.Parse(a.Args[2:])
}
- // Determine runtime base directory
- runtimeBase := a.RuntimeBaseDir
- if runtimeBase == "" {
- runtimeBase = os.Getenv("XDG_RUNTIME_DIR")
- if runtimeBase == "" {
- runtimeBase = fmt.Sprintf("/run/user/%d", os.Getuid())
- }
- }
-
- // Resolve paths
- profilePath := namespace.GetProfileNamespacePath(runtimeBase, cfg.Profile)
- pidsPath := namespace.GetPidsDirPath(runtimeBase, cfg.Profile)
+ pm := paths.NewPathManager(a.ConfigDir, a.RuntimeBaseDir)
+ profilePath := pm.ProfileNamespacePath(cfg.Profile)
+ pidsPath := pm.ProfilePidsDir(cfg.Profile)
fmt.Printf("Configuration:\n")
fmt.Printf(" Profile: %s\n", cfg.Profile)
fmt.Printf(" DNS Server: %s\n", cfg.DNSServer)
- fmt.Printf(" Runtime Base: %s\n", runtimeBase)
+ fmt.Printf(" Runtime Base: %s\n", pm.RuntimeBaseDir())
fmt.Printf(" Profile Path: %s\n", profilePath)
fmt.Printf(" PIDs Path: %s\n", pidsPath)
fmt.Printf(" Isolated: %v\n", namespace.IsIsolated())
fmt.Printf(" UID: %d\n", os.Getuid())
-
return nil
}