diff --git a/adapter/outbound/ssh.go b/adapter/outbound/ssh.go new file mode 100644 index 00000000..140a9331 --- /dev/null +++ b/adapter/outbound/ssh.go @@ -0,0 +1,98 @@ +package outbound + +import ( + "context" + "net" + "os" + "runtime" + "strconv" + + CN "github.com/metacubex/mihomo/common/net" + "github.com/metacubex/mihomo/component/dialer" + C "github.com/metacubex/mihomo/constant" + "golang.org/x/crypto/ssh" +) + +type Ssh struct { + *Base + + option *SshOption + client *ssh.Client +} + +type SshOption struct { + BasicOption + Name string `proxy:"name"` + Server string `proxy:"server"` + Port int `proxy:"port"` + UserName string `proxy:"username"` + Password string `proxy:"password,omitempty"` + PrivateKey string `proxy:"privateKey,omitempty"` +} + +func (h *Ssh) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) { + c, err := h.client.Dial("tcp", metadata.RemoteAddress()) + if err != nil { + return nil, err + } + return NewConn(CN.NewRefConn(c, h), h), nil +} + +func closeSsh(h *Ssh) { + if h.client != nil { + _ = h.client.Close() + } +} + +func NewSsh(option SshOption) (*Ssh, error) { + addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) + + config := ssh.ClientConfig{ + User: option.UserName, + HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error { + return nil + }, + } + + if option.Password == "" { + + b, err := os.ReadFile(option.PrivateKey) + if err != nil { + return nil, err + } + pKey, err := ssh.ParsePrivateKey(b) + if err != nil { + return nil, err + } + + config.Auth = []ssh.AuthMethod{ + ssh.PublicKeys(pKey), + } + } else { + config.Auth = []ssh.AuthMethod{ + ssh.Password(option.Password), + } + } + + client, err := ssh.Dial("tcp", addr, &config) + if err != nil { + return nil, err + } + + outbound := &Ssh{ + Base: &Base{ + name: option.Name, + addr: addr, + tp: C.Ssh, + udp: true, + iface: option.Interface, + rmark: option.RoutingMark, + prefer: C.NewDNSPrefer(option.IPVersion), + }, + option: &option, + client: client, + } + runtime.SetFinalizer(outbound, closeSsh) + + return outbound, nil +} diff --git a/adapter/parser.go b/adapter/parser.go index fa94708d..c64ee13a 100644 --- a/adapter/parser.go +++ b/adapter/parser.go @@ -134,6 +134,13 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) { break } proxy = outbound.NewRejectWithOption(*rejectOption) + case "ssh": + sshOption := &outbound.SshOption{} + err = decoder.Decode(mapping, sshOption) + if err != nil { + break + } + proxy, err = outbound.NewSsh(*sshOption) default: return nil, fmt.Errorf("unsupport proxy type: %s", proxyType) } diff --git a/constant/adapters.go b/constant/adapters.go index 105a7904..cb213b3c 100644 --- a/constant/adapters.go +++ b/constant/adapters.go @@ -41,6 +41,7 @@ const ( Hysteria2 WireGuard Tuic + Ssh ) const ( @@ -222,7 +223,8 @@ func (at AdapterType) String() string { return "URLTest" case LoadBalance: return "LoadBalance" - + case Ssh: + return "Ssh" default: return "Unknown" }