mirror of
https://github.com/MetaCubeX/mihomo.git
synced 2025-12-19 16:30:07 +08:00
183 lines
4.4 KiB
Go
183 lines
4.4 KiB
Go
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
|
||
}
|