diff --git a/README.md b/README.md index b4afe54..fded4cd 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,13 @@ A tool to deploy and manage [dnstt](https://www.bamsoftware.com/software/dnstt/) ## Features -- Interactive CLI wizard for easy setup +- Interactive menu and full CLI command support - Downloads and installs dnstt-server binary - Generates Curve25519 key pairs - Configures firewall rules (UFW, firewalld, iptables) - Sets up systemd service with security hardening -- Optional Dante SOCKS proxy setup +- SSH tunnel mode with integrated user management via [sshtun-user](https://github.com/net2share/sshtun-user) +- Optional Dante SOCKS proxy setup for SOCKS mode - Supports multiple architectures (amd64, arm64, armv7, 386) ## Quick Install @@ -42,19 +43,97 @@ Before running dnstm, configure your DNS records: ## Usage -Run the tool as root: +### Interactive Menu + +Run without arguments for an interactive menu: ```bash sudo dnstm ``` -The interactive menu provides options to: +**When dnstt is not installed:** +1. Install dnstt server +2. Manage SSH tunnel users + +**When dnstt is installed:** +1. Reconfigure dnstt server +2. Check service status +3. View service logs +4. Show configuration info +5. Restart service +6. Manage SSH tunnel users +7. Uninstall + +### CLI Commands + +```bash +# Show help +dnstm --help + +# Install with interactive wizard +sudo dnstm install + +# Install with CLI options (non-interactive) +sudo dnstm install --ns-subdomain t.example.com --mode ssh + +# Check service status +sudo dnstm status + +# View service logs +sudo dnstm logs + +# Show current configuration +sudo dnstm config + +# Restart the service +sudo dnstm restart + +# Manage SSH tunnel users (opens submenu) +sudo dnstm ssh-users + +# Uninstall (interactive - asks about SSH users) +sudo dnstm uninstall + +# Uninstall and remove SSH tunnel users +sudo dnstm uninstall --remove-ssh-users + +# Uninstall but keep SSH tunnel users +sudo dnstm uninstall --keep-ssh-users +``` + +### Install Options -1. **Install/Update dnstt** - Download the latest dnstt-server binary -2. **Configure** - Set up domain, keys, and tunnel mode -3. **Start/Stop/Restart** - Manage the dnstt service -4. **View Status** - Check service status and configuration -5. **Setup Dante Proxy** - Optional SOCKS5 proxy for SSH tunneling +| Option | Description | +| ------ | ----------- | +| `--ns-subdomain ` | NS subdomain (e.g., t.example.com) | +| `--mtu ` | MTU value (512-1400, default: 1232) | +| `--mode ` | Tunnel mode (default: ssh) | +| `--port ` | Target port (default: 22 for ssh, 1080 for socks) | + +### Global Options + +| Option | Description | +| ------ | ----------- | +| `--help`, `-h` | Show help message | +| `--version`, `-v` | Show version | + +## Tunnel Modes + +### SSH Mode (default) + +In SSH mode, dnstt tunnels SSH traffic. During installation, dnstm automatically: + +1. Applies sshd hardening configuration +2. Configures fail2ban for brute-force protection +3. Prompts to create a restricted tunnel user + +Tunnel users can only create local (`-L`) and SOCKS (`-D`) tunnels, with no shell access. + +Manage SSH tunnel users anytime via `sudo dnstm ssh-users` or menu option 6. + +### SOCKS Mode + +In SOCKS mode, dnstt runs a Dante SOCKS5 proxy. Clients connect directly to the proxy without SSH. ## Configuration @@ -90,6 +169,26 @@ dnstt-client -udp RESOLVER_IP:53 -pubkey-file server.pub t.example.com 127.0.0.1 # Then configure your application to use SOCKS5 proxy at 127.0.0.1:1080 ``` +## Uninstall + +```bash +# Interactive uninstall (asks about SSH tunnel users) +sudo dnstm uninstall + +# Uninstall everything including SSH tunnel users and config +sudo dnstm uninstall --remove-ssh-users + +# Uninstall dnstt but keep SSH tunnel users +sudo dnstm uninstall --keep-ssh-users +``` + +The uninstall process removes: +- dnstt-server service and binary +- Configuration files and keys +- Firewall rules +- dnstt system user +- (Optionally) SSH tunnel users and sshd hardening config + ## Building from Source ```bash diff --git a/go.mod b/go.mod index 82df4c5..3647791 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.21 require ( github.com/fatih/color v1.16.0 github.com/net2share/go-corelib v0.1.0 + github.com/net2share/sshtun-user v0.3.0 golang.org/x/crypto v0.18.0 ) diff --git a/go.sum b/go.sum index be53d77..74c383a 100644 --- a/go.sum +++ b/go.sum @@ -7,6 +7,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/net2share/go-corelib v0.1.0 h1:1uwGJVgaoxDf49LSX5BP3yRwsWVFK0zpkZrAlG8IGZs= github.com/net2share/go-corelib v0.1.0/go.mod h1:0gACJp4RRjo4vtC4We0uhK9RNBv06xgER9tuXx9FffA= +github.com/net2share/sshtun-user v0.3.0 h1:f7+oJizXcMbbCi9gUCzN2odZrW/uiTr3sRMqo+Nm0T4= +github.com/net2share/sshtun-user v0.3.0/go.mod h1:Y/dzfHCD6SmT1klEvBmlsvJSUtA72JcwTwmIGNlKMro= golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/internal/app/app.go b/internal/app/app.go index dc694e9..1aaf2cb 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "strconv" + "strings" "github.com/fatih/color" "github.com/net2share/dnstm/internal/config" @@ -12,12 +13,42 @@ import ( "github.com/net2share/dnstm/internal/network" "github.com/net2share/dnstm/internal/proxy" "github.com/net2share/dnstm/internal/service" + "github.com/net2share/dnstm/internal/sshtunnel" "github.com/net2share/dnstm/internal/system" "github.com/net2share/go-corelib/osdetect" "github.com/net2share/go-corelib/tui" ) var version string +var buildTime string + +// Command represents the CLI command to run. +type Command string + +const ( + CommandNone Command = "" + CommandInstall Command = "install" + CommandStatus Command = "status" + CommandLogs Command = "logs" + CommandConfig Command = "config" + CommandRestart Command = "restart" + CommandUninstall Command = "uninstall" + CommandSSHUsers Command = "ssh-users" +) + +// Options holds the parsed command-line options. +type Options struct { + Command Command + ShowHelp bool + ShowVersion bool + // Install options + NSSubdomain string + MTU string + TunnelMode string + TargetPort string + // Uninstall options + RemoveSSHUsers *bool // nil = not specified (error in CLI mode), true/false = specified +} // ArchInfo contains architecture information for dnstt downloads. type ArchInfo struct { @@ -48,7 +79,7 @@ func detectArch() *ArchInfo { } // printBanner displays the application banner. -func printBanner(buildTime string) { +func printBanner() { titleColor := color.New(color.FgCyan, color.Bold) banner := ` ____ _ _______ ________ ___ @@ -61,76 +92,333 @@ func printBanner(buildTime string) { fmt.Printf("DNS Tunnel Manager v%s (built %s)\n\n", version, buildTime) } -func Run(v, buildTime string) error { +func printUsage() { + fmt.Printf(`dnstm v%s (built %s) +DNS Tunnel Manager - https://github.com/net2share/dnstm + +Usage: dnstm [command] [options] + +Commands: + install Install/configure dnstt server + status Show service status + logs View service logs + config Show current configuration + restart Restart the dnstt service + uninstall Uninstall dnstt server + ssh-users Manage SSH tunnel users + +If no command is specified, an interactive menu is shown. + +Install Options: + --ns-subdomain NS subdomain (e.g., t.example.com) + --mtu MTU value (512-1400, default: 1232) + --mode Tunnel mode (default: ssh) + --port Target port (SSH port for ssh mode, default: 22) + +Uninstall Options: + --remove-ssh-users Also remove SSH tunnel users and sshd config + --keep-ssh-users Keep SSH tunnel users and sshd config + +Global Options: + --help, -h Show this help message + --version, -v Show version information + +Examples: + dnstm Run the interactive menu + dnstm install Interactive installation wizard + dnstm install --ns-subdomain t.example.com --mode ssh + Full CLI installation + dnstm status Check service status + dnstm ssh-users Manage SSH tunnel users + dnstm uninstall --remove-ssh-users + Uninstall dnstt and SSH tunnel config + dnstm --help Show this help +`, version, buildTime) +} + +func parseArgs(args []string) (*Options, error) { + opts := &Options{ + Command: CommandNone, + } + + i := 0 + + // Check for subcommand as first argument + if len(args) > 0 && len(args[0]) > 0 && args[0][0] != '-' { + switch args[0] { + case "install": + opts.Command = CommandInstall + i++ + case "status": + opts.Command = CommandStatus + i++ + case "logs": + opts.Command = CommandLogs + i++ + case "config": + opts.Command = CommandConfig + i++ + case "restart": + opts.Command = CommandRestart + i++ + case "uninstall": + opts.Command = CommandUninstall + i++ + case "ssh-users": + opts.Command = CommandSSHUsers + i++ + default: + return nil, fmt.Errorf("unknown command: %s\nRun 'dnstm --help' for usage", args[0]) + } + } + + // Parse remaining arguments + for ; i < len(args); i++ { + arg := args[i] + switch arg { + case "--help", "-h": + opts.ShowHelp = true + case "--version", "-v": + opts.ShowVersion = true + case "--ns-subdomain": + if i+1 >= len(args) { + return nil, fmt.Errorf("--ns-subdomain requires a value") + } + i++ + opts.NSSubdomain = args[i] + case "--mtu": + if i+1 >= len(args) { + return nil, fmt.Errorf("--mtu requires a value") + } + i++ + opts.MTU = args[i] + case "--mode": + if i+1 >= len(args) { + return nil, fmt.Errorf("--mode requires a value") + } + i++ + opts.TunnelMode = args[i] + if opts.TunnelMode != "ssh" && opts.TunnelMode != "socks" { + return nil, fmt.Errorf("--mode must be 'ssh' or 'socks'") + } + case "--port": + if i+1 >= len(args) { + return nil, fmt.Errorf("--port requires a value") + } + i++ + opts.TargetPort = args[i] + case "--remove-ssh-users": + t := true + opts.RemoveSSHUsers = &t + case "--keep-ssh-users": + f := false + opts.RemoveSSHUsers = &f + default: + if len(arg) > 0 && arg[0] == '-' { + return nil, fmt.Errorf("unknown option: %s\nRun 'dnstm --help' for usage", arg) + } + return nil, fmt.Errorf("unexpected argument: %s", arg) + } + } + + return opts, nil +} + +func Run(v, bt string, args []string) error { version = v + buildTime = bt + + opts, err := parseArgs(args) + if err != nil { + return err + } + + if opts.ShowHelp { + printUsage() + return nil + } + + if opts.ShowVersion { + fmt.Printf("dnstm v%s (built %s)\n", version, buildTime) + return nil + } + + // Commands that don't require root + // (none currently, but structure is here) if !osdetect.IsRoot() { return fmt.Errorf("this program must be run as root") } - printBanner(buildTime) - osInfo, err := osdetect.Detect() if err != nil { tui.PrintWarning("Could not detect OS: " + err.Error()) - } else { - tui.PrintInfo(fmt.Sprintf("Detected OS: %s", osInfo.PrettyName)) } archInfo := detectArch() - tui.PrintInfo(fmt.Sprintf("Architecture: %s", archInfo.Arch)) - if service.IsInstalled() && config.Exists() { - return showMainMenu() - } + // Route to command handlers + switch opts.Command { + case CommandNone: + printBanner() + if osInfo != nil { + tui.PrintInfo(fmt.Sprintf("Detected OS: %s", osInfo.PrettyName)) + } + tui.PrintInfo(fmt.Sprintf("Architecture: %s", archInfo.Arch)) + return showMainMenu(osInfo, archInfo) + + case CommandInstall: + printBanner() + if osInfo != nil { + tui.PrintInfo(fmt.Sprintf("Detected OS: %s", osInfo.PrettyName)) + } + tui.PrintInfo(fmt.Sprintf("Architecture: %s", archInfo.Arch)) + return runInstallCommand(osInfo, archInfo, opts) + + case CommandStatus: + return runStatusCommand() + + case CommandLogs: + return runLogsCommand() + + case CommandConfig: + return runConfigCommand() - return runInstallation(osInfo, archInfo) + case CommandRestart: + return runRestartCommand() + + case CommandUninstall: + printBanner() + return runUninstallCommand(opts) + + case CommandSSHUsers: + printBanner() + sshtunnel.ShowMenu() + return nil + + default: + return fmt.Errorf("unknown command") + } } -func showMainMenu() error { +func showMainMenu(osInfo *osdetect.OSInfo, archInfo *ArchInfo) error { for { - options := []tui.MenuOption{ - {Key: "1", Label: "Install/Reconfigure dnstt server"}, - {Key: "2", Label: "Check service status"}, - {Key: "3", Label: "View service logs"}, - {Key: "4", Label: "Show configuration info"}, - {Key: "5", Label: "Restart service"}, - {Key: "6", Label: "Uninstall"}, - {Key: "0", Label: "Exit"}, + fmt.Println() + + // Check current state + isInstalled := service.IsInstalled() && config.Exists() + + // Build menu options based on state + var options []tui.MenuOption + + if isInstalled { + options = []tui.MenuOption{ + {Key: "1", Label: "Reconfigure dnstt server"}, + {Key: "2", Label: "Check service status"}, + {Key: "3", Label: "View service logs"}, + {Key: "4", Label: "Show configuration info"}, + {Key: "5", Label: "Restart service"}, + {Key: "6", Label: "Manage SSH tunnel users"}, + {Key: "7", Label: "Uninstall"}, + {Key: "0", Label: "Exit"}, + } + } else { + // Not installed - show menu with install and SSH users + options = []tui.MenuOption{ + {Key: "1", Label: "Install dnstt server"}, + {Key: "2", Label: "Manage SSH tunnel users"}, + {Key: "0", Label: "Exit"}, + } } tui.ShowMenu(options) choice := tui.Prompt("Select option") - switch choice { - case "1": - osInfo, _ := osdetect.Detect() - archInfo := detectArch() - if err := runInstallation(osInfo, archInfo); err != nil { - tui.PrintError(err.Error()) + if isInstalled { + switch choice { + case "1": + if err := runInstallInteractive(osInfo, archInfo); err != nil { + tui.PrintError(err.Error()) + } + tui.WaitForEnter() + case "2": + showStatus() + case "3": + showLogs() + case "4": + showConfig() + case "5": + restartService() + case "6": + sshtunnel.ShowMenu() + case "7": + runUninstall() + tui.WaitForEnter() + case "0", "q", "quit", "exit": + tui.PrintInfo("Goodbye!") + return nil + default: + tui.PrintError("Invalid option") } - case "2": - showStatus() - case "3": - showLogs() - case "4": - showConfig() - case "5": - restartService() - case "6": - if runUninstall() { + } else { + switch choice { + case "1": + if err := runInstallInteractive(osInfo, archInfo); err != nil { + tui.PrintError(err.Error()) + tui.WaitForEnter() + } + case "2": + sshtunnel.ShowMenu() + case "0", "q", "quit", "exit": + tui.PrintInfo("Goodbye!") return nil + default: + tui.PrintError("Invalid option") } - case "0", "q", "quit", "exit": - tui.PrintInfo("Goodbye!") - return nil - default: - tui.PrintError("Invalid option") } } } -func runInstallation(osInfo *osdetect.OSInfo, archInfo *ArchInfo) error { +func runInstallCommand(osInfo *osdetect.OSInfo, archInfo *ArchInfo, opts *Options) error { + // Check if any install options were provided + hasAnyOption := opts.NSSubdomain != "" || opts.MTU != "" || opts.TunnelMode != "" || opts.TargetPort != "" + + if !hasAnyOption { + // No options provided - run interactive mode + return runInstallInteractive(osInfo, archInfo) + } + + // Some options provided - validate all required options are present + if opts.NSSubdomain == "" { + return fmt.Errorf("--ns-subdomain is required for CLI installation") + } + + // Set defaults for optional parameters + if opts.MTU == "" { + opts.MTU = "1232" + } + if opts.TunnelMode == "" { + opts.TunnelMode = "ssh" + } + if opts.TargetPort == "" { + if opts.TunnelMode == "ssh" { + opts.TargetPort = osdetect.DetectSSHPort() + } else { + opts.TargetPort = "1080" + } + } + + // Validate MTU + mtu, err := strconv.Atoi(opts.MTU) + if err != nil || mtu < 512 || mtu > 1400 { + return fmt.Errorf("--mtu must be a number between 512 and 1400") + } + + // Run CLI installation + return runInstallCLI(osInfo, archInfo, opts) +} + +func runInstallInteractive(osInfo *osdetect.OSInfo, archInfo *ArchInfo) error { cfg, err := config.Load() if err != nil { cfg = &config.Config{ @@ -178,6 +466,23 @@ func runInstallation(osInfo *osdetect.OSInfo, archInfo *ArchInfo) error { return fmt.Errorf("installation cancelled") } + return runInstallation(osInfo, archInfo, cfg) +} + +func runInstallCLI(osInfo *osdetect.OSInfo, archInfo *ArchInfo, opts *Options) error { + cfg := &config.Config{ + NSSubdomain: opts.NSSubdomain, + MTU: opts.MTU, + TunnelMode: opts.TunnelMode, + TargetPort: opts.TargetPort, + } + + cfg.PrivateKeyFile, cfg.PublicKeyFile = config.GetKeyFilenames(cfg.NSSubdomain) + + return runInstallation(osInfo, archInfo, cfg) +} + +func runInstallation(osInfo *osdetect.OSInfo, archInfo *ArchInfo, cfg *config.Config) error { totalSteps := 7 currentStep := 0 @@ -221,6 +526,7 @@ func runInstallation(osInfo *osdetect.OSInfo, archInfo *ArchInfo) error { tui.PrintStep(currentStep, totalSteps, "Generating cryptographic keys...") var publicKey string + var err error if keys.KeysExist(cfg.PrivateKeyFile, cfg.PublicKeyFile) { if tui.Confirm("Keys already exist. Regenerate?", false) { publicKey, err = keys.Generate(cfg.PrivateKeyFile, cfg.PublicKeyFile) @@ -260,7 +566,8 @@ func runInstallation(osInfo *osdetect.OSInfo, archInfo *ArchInfo) error { tui.PrintStatus("IPv6 rules configured") } - // Step 6: Setup Dante (if SOCKS mode) + // Step 6: Setup Dante (if SOCKS mode) or SSH tunnel hardening (if SSH mode) + var createdUser *sshtunnel.CreatedUserInfo currentStep++ if cfg.TunnelMode == "socks" { tui.PrintStep(currentStep, totalSteps, "Setting up Dante SOCKS proxy...") @@ -281,9 +588,22 @@ func runInstallation(osInfo *osdetect.OSInfo, archInfo *ArchInfo) error { tui.PrintWarning("Dante start warning: " + err.Error()) } tui.PrintStatus("Dante SOCKS proxy configured") + cfg.SSHTunnelEnabled = "false" } else { - tui.PrintStep(currentStep, totalSteps, "Skipping Dante setup (SSH mode)...") - tui.PrintStatus("SSH mode selected") + tui.PrintStep(currentStep, totalSteps, "Setting up SSH tunnel hardening...") + createdUser = sshtunnel.ConfigureAndCreateUser() + if sshtunnel.IsConfigured() { + cfg.SSHTunnelEnabled = "true" + tui.PrintStatus("SSH tunnel hardening configured") + } else { + cfg.SSHTunnelEnabled = "false" + tui.PrintStatus("SSH mode selected (tunnel hardening failed)") + } + } + + // Save config again to persist SSHTunnelEnabled + if err := cfg.Save(); err != nil { + tui.PrintWarning("Failed to save SSH tunnel state: " + err.Error()) } // Step 7: Create and start systemd service @@ -308,12 +628,12 @@ func runInstallation(osInfo *osdetect.OSInfo, archInfo *ArchInfo) error { tui.PrintStatus("Service started") // Show success information - showSuccessInfo(cfg, publicKey) + showSuccessInfo(cfg, publicKey, createdUser) return nil } -func showSuccessInfo(cfg *config.Config, publicKey string) { +func showSuccessInfo(cfg *config.Config, publicKey string, createdUser *sshtunnel.CreatedUserInfo) { lines := []string{ fmt.Sprintf("NS Subdomain: %s", cfg.NSSubdomain), fmt.Sprintf("Tunnel Mode: %s", cfg.TunnelMode), @@ -321,12 +641,24 @@ func showSuccessInfo(cfg *config.Config, publicKey string) { "", "Public Key (for client):", publicKey, - "", - "DNS Configuration Required:", - " A record: tns.yourdomain.com -> ", - " NS record: t.yourdomain.com -> tns.yourdomain.com", } + // Add created user info if available + if createdUser != nil { + lines = append(lines, "") + lines = append(lines, "SSH Tunnel User Created:") + lines = append(lines, fmt.Sprintf(" Username: %s", createdUser.Username)) + lines = append(lines, fmt.Sprintf(" Auth: %s", createdUser.AuthMode)) + if createdUser.Password != "" { + lines = append(lines, fmt.Sprintf(" Password: %s", createdUser.Password)) + } + } + + lines = append(lines, "") + lines = append(lines, "DNS Configuration Required:") + lines = append(lines, " A record: tns.yourdomain.com -> ") + lines = append(lines, " NS record: t.yourdomain.com -> tns.yourdomain.com") + tui.PrintBox("Installation Complete!", lines) tui.PrintInfo("Useful commands:") @@ -335,6 +667,67 @@ func showSuccessInfo(cfg *config.Config, publicKey string) { fmt.Println(" dnstm - Open this menu") } +func runStatusCommand() error { + if !service.IsInstalled() { + return fmt.Errorf("dnstt is not installed") + } + status, _ := service.Status() + fmt.Println(status) + return nil +} + +func runLogsCommand() error { + if !service.IsInstalled() { + return fmt.Errorf("dnstt is not installed") + } + logs, err := service.GetLogs(50) + if err != nil { + return err + } + fmt.Println(logs) + return nil +} + +func runConfigCommand() error { + if !config.Exists() { + return fmt.Errorf("dnstt is not configured") + } + + cfg, err := config.Load() + if err != nil { + return fmt.Errorf("failed to load configuration: %w", err) + } + + publicKey := "" + if cfg.PublicKeyFile != "" { + publicKey, _ = keys.ReadPublicKey(cfg.PublicKeyFile) + } + + fmt.Printf("NS Subdomain: %s\n", cfg.NSSubdomain) + fmt.Printf("Tunnel Mode: %s\n", cfg.TunnelMode) + fmt.Printf("MTU: %s\n", cfg.MTU) + fmt.Printf("Target Port: %s\n", cfg.TargetPort) + fmt.Printf("Private Key: %s\n", cfg.PrivateKeyFile) + fmt.Printf("Public Key File: %s\n", cfg.PublicKeyFile) + fmt.Println() + fmt.Println("Public Key (for client):") + fmt.Println(publicKey) + + return nil +} + +func runRestartCommand() error { + if !service.IsInstalled() { + return fmt.Errorf("dnstt is not installed") + } + fmt.Println("Restarting dnstt-server...") + if err := service.Restart(); err != nil { + return err + } + fmt.Println("Service restarted successfully") + return nil +} + func showStatus() { fmt.Println() status, _ := service.Status() @@ -399,7 +792,30 @@ func restartService() { tui.WaitForEnter() } -func runUninstall() bool { +func runUninstallCommand(opts *Options) error { + // Check if SSH users option was specified in CLI mode + if opts.RemoveSSHUsers == nil { + // No option specified - this is an error in CLI mode + // Check if any other args were provided to determine if this is CLI mode + // If we got here with no option, it means user ran "dnstm uninstall" without flags + // which should be interactive mode + _, err := runUninstallInteractive() + return err + } + + // CLI mode - run non-interactive uninstall + return runUninstallCLI(*opts.RemoveSSHUsers) +} + +// uninstallResult indicates what happened during uninstall +type uninstallResult int + +const ( + uninstallCancelled uninstallResult = iota + uninstallCompleted +) + +func runUninstallInteractive() (uninstallResult, error) { fmt.Println() tui.PrintWarning("This will completely remove dnstt from your system:") fmt.Println(" - Stop and remove the dnstt-server service") @@ -411,12 +827,32 @@ func runUninstall() bool { if !tui.Confirm("Are you sure you want to uninstall?", false) { tui.PrintInfo("Uninstall cancelled") - tui.WaitForEnter() - return false + return uninstallCancelled, nil } + // Ask about SSH tunnel users + removeSSHUsers := false + if sshtunnel.IsConfigured() { + fmt.Println() + tui.PrintInfo("SSH tunnel hardening is configured on this system.") + removeSSHUsers = tui.Confirm("Also remove SSH tunnel users and sshd hardening config?", false) + } + + performUninstall(removeSSHUsers) + return uninstallCompleted, nil +} + +func runUninstallCLI(removeSSHUsers bool) error { + performUninstall(removeSSHUsers) + return nil +} + +func performUninstall(removeSSHUsers bool) { fmt.Println() totalSteps := 5 + if removeSSHUsers { + totalSteps = 6 + } currentStep := 0 // Step 1: Stop and remove service @@ -449,15 +885,44 @@ func runUninstall() bool { network.RemoveFirewallRules() tui.PrintStatus("Firewall rules removed") - // Step 5: Remove user + // Step 5: Remove dnstt user currentStep++ tui.PrintStep(currentStep, totalSteps, "Removing dnstt user...") system.RemoveDnsttUser() tui.PrintStatus("User removed") + // Step 6: Remove SSH tunnel users and config (if requested) + if removeSSHUsers { + currentStep++ + tui.PrintStep(currentStep, totalSteps, "Removing SSH tunnel users and config...") + if err := sshtunnel.UninstallAll(); err != nil { + tui.PrintWarning("SSH tunnel uninstall warning: " + err.Error()) + } else { + tui.PrintStatus("SSH tunnel config removed") + } + } + fmt.Println() tui.PrintSuccess("Uninstallation complete!") tui.PrintInfo("All dnstt components have been removed from your system.") +} - return true +// runUninstall is called from interactive menu, returns true only if uninstall was completed +func runUninstall() bool { + result, err := runUninstallInteractive() + if err != nil { + tui.PrintError(err.Error()) + return false + } + return result == uninstallCompleted +} + +// Helper to check if a string is in a slice +func contains(slice []string, item string) bool { + for _, s := range slice { + if strings.EqualFold(s, item) { + return true + } + } + return false } diff --git a/internal/config/config.go b/internal/config/config.go index 40adf17..2df9b96 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -15,12 +15,13 @@ const ( ) type Config struct { - NSSubdomain string - MTU string - TunnelMode string - PrivateKeyFile string - PublicKeyFile string - TargetPort string + NSSubdomain string + MTU string + TunnelMode string + PrivateKeyFile string + PublicKeyFile string + TargetPort string + SSHTunnelEnabled string // "true" or "false" - tracks if SSH tunnel hardening is applied } func Load() (*Config, error) { @@ -71,6 +72,8 @@ func Load() (*Config, error) { config.PublicKeyFile = value case "TARGET_PORT": config.TargetPort = value + case "SSH_TUNNEL_ENABLED": + config.SSHTunnelEnabled = value } } @@ -93,7 +96,8 @@ TUNNEL_MODE="%s" PRIVATE_KEY_FILE="%s" PUBLIC_KEY_FILE="%s" TARGET_PORT="%s" -`, c.NSSubdomain, c.MTU, c.TunnelMode, c.PrivateKeyFile, c.PublicKeyFile, c.TargetPort) +SSH_TUNNEL_ENABLED="%s" +`, c.NSSubdomain, c.MTU, c.TunnelMode, c.PrivateKeyFile, c.PublicKeyFile, c.TargetPort, c.SSHTunnelEnabled) if err := os.WriteFile(configPath, []byte(content), 0640); err != nil { return fmt.Errorf("failed to write config file: %w", err) diff --git a/internal/sshtunnel/sshtunnel.go b/internal/sshtunnel/sshtunnel.go new file mode 100644 index 0000000..329b29d --- /dev/null +++ b/internal/sshtunnel/sshtunnel.go @@ -0,0 +1,43 @@ +// Package sshtunnel provides SSH tunnel user management integration for dnstm. +// This is a thin wrapper around sshtun-user's pkg/cli module. +package sshtunnel + +import ( + "github.com/net2share/sshtun-user/pkg/cli" +) + +// CreatedUserInfo holds information about a created tunnel user +type CreatedUserInfo struct { + Username string + AuthMode string + Password string // Only set if password auth and auto-generated +} + +// ConfigureAndCreateUser auto-configures sshd hardening and prompts for user creation. +// Used during dnstm SSH mode installation. Returns user info if created, nil otherwise. +func ConfigureAndCreateUser() *CreatedUserInfo { + userInfo := cli.ConfigureAndCreateUser() + if userInfo == nil { + return nil + } + return &CreatedUserInfo{ + Username: userInfo.Username, + AuthMode: userInfo.AuthMode, + Password: userInfo.Password, + } +} + +// ShowMenu displays the SSH tunnel users submenu. +func ShowMenu() { + cli.ShowUserManagementMenu() +} + +// IsConfigured checks if SSH tunnel hardening has been applied. +func IsConfigured() bool { + return cli.IsConfigured() +} + +// UninstallAll performs complete uninstall of SSH tunnel users and config. +func UninstallAll() error { + return cli.UninstallAllNonInteractive() +} diff --git a/main.go b/main.go index dcb3c06..db178ba 100644 --- a/main.go +++ b/main.go @@ -13,7 +13,7 @@ var ( ) func main() { - if err := app.Run(Version, BuildTime); err != nil { + if err := app.Run(Version, BuildTime, os.Args[1:]); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) os.Exit(1) }