diff --git a/ansible/tasks/global/utils/smart-ssh/smart-ssh.go b/ansible/tasks/global/utils/smart-ssh/smart-ssh.go index 2a85e70..7999642 100644 --- a/ansible/tasks/global/utils/smart-ssh/smart-ssh.go +++ b/ansible/tasks/global/utils/smart-ssh/smart-ssh.go @@ -30,10 +30,10 @@ type LoggingConfig struct { // SmartAlias represents a smart SSH alias configuration type SmartAlias struct { - Primary string `yaml:"primary"` // SSH config host to use when local - Fallback string `yaml:"fallback"` // SSH config host to use when remote + Primary string `yaml:"primary"` // SSH config host to use when local + Fallback string `yaml:"fallback"` // SSH config host to use when remote CheckHost string `yaml:"check_host"` // IP to ping for connectivity test - Timeout string `yaml:"timeout"` // Ping timeout (default: "2s") + Timeout string `yaml:"timeout"` // Ping timeout (default: "2s") } // TunnelDefinition represents a tunnel configuration @@ -47,36 +47,39 @@ type TunnelDefinition struct { // TunnelState represents runtime state of an active tunnel type TunnelState struct { - Name string `json:"name"` - Source string `json:"source"` // "config" or "adhoc" - Type string `json:"type"` // local, remote, dynamic - LocalPort int `json:"local_port"` - RemoteHost string `json:"remote_host"` - RemotePort int `json:"remote_port"` - SSHHost string `json:"ssh_host"` - SSHHostResolved string `json:"ssh_host_resolved"` // After smart alias resolution - PID int `json:"pid"` - Status string `json:"status"` - StartedAt time.Time `json:"started_at"` - LastSeen time.Time `json:"last_seen"` - CommandLine string `json:"command_line"` + Name string `json:"name"` + Source string `json:"source"` // "config" or "adhoc" + Type string `json:"type"` // local, remote, dynamic + LocalPort int `json:"local_port"` + RemoteHost string `json:"remote_host"` + RemotePort int `json:"remote_port"` + SSHHost string `json:"ssh_host"` + SSHHostResolved string `json:"ssh_host_resolved"` // After smart alias resolution + PID int `json:"pid"` + Status string `json:"status"` + StartedAt time.Time `json:"started_at"` + LastSeen time.Time `json:"last_seen"` + CommandLine string `json:"command_line"` } // Config represents the YAML configuration structure type Config struct { - Logging LoggingConfig `yaml:"logging"` - SmartAliases map[string]SmartAlias `yaml:"smart_aliases"` - Tunnels map[string]TunnelDefinition `yaml:"tunnels"` + Logging LoggingConfig `yaml:"logging"` + SmartAliases map[string]SmartAlias `yaml:"smart_aliases"` + Tunnels map[string]TunnelDefinition `yaml:"tunnels"` } const ( - realSSHPath = "/usr/bin/ssh" + defaultSSHPath = "/usr/bin/ssh" + wslSSHPath = "ssh.exe" + wslDetectPath = "/mnt/c/Windows/System32/cmd.exe" ) var ( - configDir string - tunnelsDir string - config *Config + configDir string + tunnelsDir string + config *Config + sshPath string // Will be set based on WSL2 detection // Global flags tunnelMode bool @@ -92,10 +95,10 @@ var ( ) var rootCmd = &cobra.Command{ - Use: "ssh", - Short: "Smart SSH utility with tunnel management", - Long: "A transparent SSH wrapper that provides smart alias resolution and background tunnel management", - Run: handleSSH, + Use: "ssh", + Short: "Smart SSH utility with tunnel management", + Long: "A transparent SSH wrapper that provides smart alias resolution and background tunnel management", + Run: handleSSH, DisableFlagParsing: true, } @@ -103,13 +106,16 @@ var tunnelCmd = &cobra.Command{ Use: "tunnel [tunnel-name]", Short: "Manage background SSH tunnels", Long: "Create, list, and manage persistent SSH tunnels in the background", - Run: func(cmd *cobra.Command, args []string) { + Run: func(cmd *cobra.Command, args []string) { handleTunnelManual(append([]string{"--tunnel"}, args...)) }, - Args: cobra.MaximumNArgs(1), + Args: cobra.MaximumNArgs(1), } func init() { + // Detect and set SSH path based on environment (WSL2 vs native Linux) + sshPath = detectSSHPath() + // Initialize config directory homeDir, err := os.UserHomeDir() if err != nil { @@ -141,6 +147,13 @@ func init() { // Initialize logging initLogging(config.Logging) + // Log SSH path detection (after logging is initialized) + if isWSL2() { + log.Debug().Str("ssh_path", sshPath).Msg("WSL2 detected, using Windows SSH") + } else { + log.Debug().Str("ssh_path", sshPath).Msg("Native Linux environment, using Linux SSH") + } + // Global flags rootCmd.PersistentFlags().BoolVarP(&tunnelMode, "tunnel", "T", false, "Enable tunnel mode") rootCmd.Flags().BoolVarP(&tunnelOpen, "open", "O", false, "Open a tunnel") @@ -169,6 +182,22 @@ func init() { } } +// detectSSHPath determines the correct SSH binary path based on the environment +func detectSSHPath() string { + if isWSL2() { + // In WSL2, use Windows SSH + return wslSSHPath + } + // Default to Linux SSH + return defaultSSHPath +} + +// isWSL2 checks if we're running in WSL2 by looking for Windows System32 +func isWSL2() bool { + _, err := os.Stat(wslDetectPath) + return err == nil +} + func main() { // Check if this is a tunnel command first args := os.Args[1:] @@ -563,7 +592,7 @@ func openTunnel(name string) error { log.Debug().Strs("command", cmdArgs).Msg("Starting SSH tunnel") // Start SSH process - cmd := exec.Command(realSSHPath, cmdArgs[1:]...) + cmd := exec.Command(sshPath, cmdArgs[1:]...) // Capture stderr to see any SSH errors var stderr bytes.Buffer @@ -708,7 +737,9 @@ func createAdhocTunnel() (TunnelDefinition, error) { } func buildSSHCommand(tunnel TunnelDefinition, sshHost string) []string { - args := []string{"ssh", "-f", "-N"} + // Use the detected SSH path basename for the command + sshBinary := filepath.Base(sshPath) + args := []string{sshBinary, "-f", "-N"} switch tunnel.Type { case "local": @@ -1056,18 +1087,37 @@ func findSSHProcessByPort(port int) int { // executeRealSSH executes the real SSH binary with given arguments func executeRealSSH(args []string) { - // Check if real SSH exists - if _, err := os.Stat(realSSHPath); os.IsNotExist(err) { - log.Error().Str("path", realSSHPath).Msg("Real SSH binary not found") - fmt.Fprintf(os.Stderr, "Error: Real SSH binary not found at %s\n", realSSHPath) + log.Debug().Str("ssh_path", sshPath).Strs("args", args).Msg("Executing real SSH") + + // In WSL2, we need to use exec.Command instead of syscall.Exec for Windows binaries + if isWSL2() { + cmd := exec.Command(sshPath, args...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + err := cmd.Run() + if err != nil { + if exitErr, ok := err.(*exec.ExitError); ok { + os.Exit(exitErr.ExitCode()) + } + log.Error().Err(err).Msg("Failed to execute SSH") + fmt.Fprintf(os.Stderr, "Error executing SSH: %v\n", err) + os.Exit(1) + } + os.Exit(0) + } + + // For native Linux, check if SSH exists + if _, err := os.Stat(sshPath); os.IsNotExist(err) { + log.Error().Str("path", sshPath).Msg("Real SSH binary not found") + fmt.Fprintf(os.Stderr, "Error: Real SSH binary not found at %s\n", sshPath) os.Exit(1) } - log.Debug().Str("ssh_path", realSSHPath).Strs("args", args).Msg("Executing real SSH") - - // Execute the real SSH binary - // Using syscall.Exec to replace current process (like exec in shell) - err := syscall.Exec(realSSHPath, append([]string{"ssh"}, args...), os.Environ()) + // Execute the real SSH binary using syscall.Exec (Linux only) + // This replaces the current process (like exec in shell) + err := syscall.Exec(sshPath, append([]string{"ssh"}, args...), os.Environ()) if err != nil { log.Error().Err(err).Msg("Failed to execute SSH") fmt.Fprintf(os.Stderr, "Error executing SSH: %v\n", err) diff --git a/config/bash.nix b/config/bash.nix index 063eb1c..eda3ccb 100644 --- a/config/bash.nix +++ b/config/bash.nix @@ -137,6 +137,11 @@ bind -x '"\C-r": fzf_history_search' fi + # In case this is WSL, let's add various Windows executables as aliases + if [ -f "/mnt/c/Windows/System32/cmd.exe" ]; then + alias ssh-add="ssh-add.exe" + fi + # Display welcome message for interactive shells if [ -t 1 ]; then command -v helloworld &> /dev/null && helloworld @@ -190,10 +195,6 @@ # Kubernetes aliases "kubectl" = "minikube kubectl --"; - # Editor aliases - "zeditor" = "${config.home.homeDirectory}/.local/bin/zed"; - "zed" = "${config.home.homeDirectory}/.local/bin/zed"; - # SSH alias "ssh" = "${config.home.homeDirectory}/.local/bin/smart-ssh";