diff --git a/README.md b/README.md index 05f03799..4b606ce1 100644 --- a/README.md +++ b/README.md @@ -98,4 +98,11 @@ API. This software is released under the GPL-3.0 license. -**In addition, any downstream projects not affiliated with `MetaCubeX` shall not contain the word `mihomo` in their names.** \ No newline at end of file +**In addition, any downstream projects not affiliated with `MetaCubeX` shall not contain the word `mihomo` in their names.** + +## 命令 + +```bash +# 打包 +CGO_ENABLED=0 GOARCH=arm64 GOOS=darwin go build -tags with_gvisor -trimpath -ldflags '-X "github.com/metacubex/mihomo/constant.Version=v1.19.15-debug" -w -s -buildid=' -o verge-mihomo-alpha +``` diff --git a/adapter/outbound/ssh.go b/adapter/outbound/ssh.go index 3b915147..16a24b9d 100644 --- a/adapter/outbound/ssh.go +++ b/adapter/outbound/ssh.go @@ -23,7 +23,8 @@ import ( type Ssh struct { *Base - option *SshOption + option *SshOption + useSystemSsh bool config *ssh.ClientConfig client *ssh.Client @@ -41,6 +42,9 @@ type SshOption struct { PrivateKeyPassphrase string `proxy:"private-key-passphrase,omitempty"` HostKey []string `proxy:"host-key,omitempty"` HostKeyAlgorithms []string `proxy:"host-key-algorithms,omitempty"` + UseSshConfigAlias bool `proxy:"use-ssh-config-alias,omitempty"` + SshUser string `proxy:"ssh-user,omitempty"` + SshUserHome string `proxy:"ssh-user-home,omitempty"` } func (s *Ssh) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) { @@ -69,9 +73,21 @@ func (s *Ssh) connect(ctx context.Context, cDialer C.Dialer, addr string) (clien if s.client != nil { return s.client, nil } - c, err := cDialer.DialContext(ctx, "tcp", addr) - if err != nil { - return nil, err + + var c net.Conn + + // 如果使用系统 ssh 命令(完整 SSH config 支持) + if s.useSystemSsh { + c, err = s.dialViaSystemSsh(ctx, s.option.Server) + if err != nil { + return nil, fmt.Errorf("system ssh failed: %w", err) + } + } else { + // 原有的纯 Go 实现 + c, err = cDialer.DialContext(ctx, "tcp", addr) + if err != nil { + return nil, err + } } defer func(c net.Conn) { @@ -190,6 +206,8 @@ func NewSsh(option SshOption) (*Ssh, error) { } config.ClientVersion = version + useSystemSsh := option.UseSshConfigAlias + outbound := &Ssh{ Base: &Base{ name: option.Name, @@ -200,8 +218,9 @@ func NewSsh(option SshOption) (*Ssh, error) { rmark: option.RoutingMark, prefer: C.NewDNSPrefer(option.IPVersion), }, - option: &option, - config: &config, + option: &option, + useSystemSsh: useSystemSsh, + config: &config, } return outbound, nil diff --git a/adapter/outbound/ssh_system.go b/adapter/outbound/ssh_system.go new file mode 100644 index 00000000..55c0d071 --- /dev/null +++ b/adapter/outbound/ssh_system.go @@ -0,0 +1,182 @@ +package outbound + +import ( + "bytes" + "context" + "fmt" + "io" + "net" + "os" + "os/exec" + "strings" + "time" + + "github.com/metacubex/mihomo/log" +) + +// dialViaSystemSsh 使用系统 SSH 命令建立连接 +func (s *Ssh) dialViaSystemSsh(ctx context.Context, hostAlias string) (net.Conn, error) { + log.Infoln("[SSH] Using system SSH with config alias: %s", hostAlias) + + port := s.option.Port + if port == 0 { + port = 22 + } + + // 获取实际用户(非root) + var actualUser string + var cmdArgs []string + + // 方法0: 用户显式指定(最高优先级) + if s.option.SshUser != "" { + actualUser = s.option.SshUser + log.Infoln("[SSH] Using configured user: %s", actualUser) + } + + // 方法1: 从配置中的 ssh-user-home 提取 + if actualUser == "" && s.option.SshUserHome != "" { + // 从路径提取用户名: /Users/fa -> fa + parts := strings.Split(s.option.SshUserHome, "/") + if len(parts) >= 3 && parts[1] == "Users" { + actualUser = parts[2] + log.Infoln("[SSH] Detected user from ssh-user-home: %s", actualUser) + } + } + + // 方法2: 从环境变量获取 + if actualUser == "" { + actualUser = os.Getenv("SUDO_USER") + if actualUser != "" { + log.Infoln("[SSH] Detected user from SUDO_USER: %s", actualUser) + } + } + + // 方法3: 检查/Users目录(macOS)- 跳过隐藏目录 + if actualUser == "" { + entries, err := os.ReadDir("/Users") + if err == nil { + for _, entry := range entries { + name := entry.Name() + // 跳过隐藏目录(.开头)、系统目录 + if entry.IsDir() && !strings.HasPrefix(name, ".") && + name != "Shared" && name != "Guest" { + actualUser = name + log.Infoln("[SSH] Auto-detected user from /Users: %s", actualUser) + break + } + } + } + } + + targetAddr := fmt.Sprintf("localhost:%d", port) + + if actualUser != "" { + // 以实际用户身份执行: sudo -u username -i ssh -W ... + // -i: 加载用户的登录环境(包括 PATH,确保 cloudflared 等命令可用) + cmdArgs = []string{"-u", actualUser, "-i", "ssh", "-W", targetAddr, hostAlias} + log.Infoln("[SSH] Running as user: %s (with login env)", actualUser) + log.Infoln("[SSH] Command: sudo %s", strings.Join(cmdArgs, " ")) + } else { + // 回退:直接执行(可能失败) + cmdArgs = []string{"-W", targetAddr, hostAlias} + log.Warnln("[SSH] Could not determine actual user, running ssh directly") + log.Infoln("[SSH] Command: ssh %s", strings.Join(cmdArgs, " ")) + } + + var cmd *exec.Cmd + if actualUser != "" { + cmd = exec.CommandContext(ctx, "sudo", cmdArgs...) + } else { + cmd = exec.CommandContext(ctx, "ssh", cmdArgs...) + } + + // 设置 HOME 环境变量(如果用户指定) + if s.option.SshUserHome != "" { + cmd.Env = append(cmd.Environ(), "HOME="+s.option.SshUserHome) + log.Infoln("[SSH] Setting HOME=%s for SSH command", s.option.SshUserHome) + } + + // 获取 stdin/stdout + stdin, err := cmd.StdinPipe() + if err != nil { + return nil, fmt.Errorf("failed to get stdin pipe: %w", err) + } + + stdout, err := cmd.StdoutPipe() + if err != nil { + return nil, fmt.Errorf("failed to get stdout pipe: %w", err) + } + + // 捕获 stderr + var stderr bytes.Buffer + cmd.Stderr = &stderr + + // 启动命令 + if err := cmd.Start(); err != nil { + return nil, fmt.Errorf("failed to start ssh: %w", err) + } + + log.Infoln("[SSH] SSH subprocess started, PID: %d", cmd.Process.Pid) + + // 监控进程退出 + go func() { + if err := cmd.Wait(); err != nil { + output := stderr.String() + if len(output) > 0 { + log.Errorln("[SSH] SSH process exited with error: %v, stderr: %s", err, output) + } else { + log.Errorln("[SSH] SSH process exited with error: %v", err) + } + } + }() + + // 返回包装的连接 + return &sshCmdConn{ + stdin: stdin, + stdout: stdout, + cmd: cmd, + stderr: &stderr, + }, nil +} + +// sshCmdConn 实现 net.Conn 接口 +type sshCmdConn struct { + stdin io.WriteCloser + stdout io.ReadCloser + cmd *exec.Cmd + stderr *bytes.Buffer +} + +func (c *sshCmdConn) Read(b []byte) (n int, err error) { + return c.stdout.Read(b) +} + +func (c *sshCmdConn) Write(b []byte) (n int, err error) { + return c.stdin.Write(b) +} + +func (c *sshCmdConn) Close() error { + c.stdin.Close() + c.stdout.Close() + return c.cmd.Wait() +} + +func (c *sshCmdConn) LocalAddr() net.Addr { + return &net.TCPAddr{IP: net.IPv4zero, Port: 0} +} + +func (c *sshCmdConn) RemoteAddr() net.Addr { + return &net.TCPAddr{IP: net.IPv4zero, Port: 0} +} + +func (c *sshCmdConn) SetDeadline(t time.Time) error { + return nil +} + +func (c *sshCmdConn) SetReadDeadline(t time.Time) error { + return nil +} + +func (c *sshCmdConn) SetWriteDeadline(t time.Time) error { + return nil +}