mihomo/adapter/outbound/ssh_system.go

183 lines
4.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}