github.com/metacubex/mihomo@v1.18.5/adapter/outbound/ssh.go (about) 1 package outbound 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/base64" 7 "fmt" 8 "net" 9 "os" 10 "runtime" 11 "strconv" 12 "strings" 13 "sync" 14 15 N "github.com/metacubex/mihomo/common/net" 16 "github.com/metacubex/mihomo/component/dialer" 17 "github.com/metacubex/mihomo/component/proxydialer" 18 C "github.com/metacubex/mihomo/constant" 19 20 "github.com/zhangyunhao116/fastrand" 21 "golang.org/x/crypto/ssh" 22 ) 23 24 type Ssh struct { 25 *Base 26 27 option *SshOption 28 client *sshClient // using a standalone struct to avoid its inner loop invalidate the Finalizer 29 } 30 31 type SshOption struct { 32 BasicOption 33 Name string `proxy:"name"` 34 Server string `proxy:"server"` 35 Port int `proxy:"port"` 36 UserName string `proxy:"username"` 37 Password string `proxy:"password,omitempty"` 38 PrivateKey string `proxy:"private-key,omitempty"` 39 PrivateKeyPassphrase string `proxy:"private-key-passphrase,omitempty"` 40 HostKey []string `proxy:"host-key,omitempty"` 41 HostKeyAlgorithms []string `proxy:"host-key-algorithms,omitempty"` 42 } 43 44 func (s *Ssh) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) { 45 var cDialer C.Dialer = dialer.NewDialer(s.Base.DialOptions(opts...)...) 46 if len(s.option.DialerProxy) > 0 { 47 cDialer, err = proxydialer.NewByName(s.option.DialerProxy, cDialer) 48 if err != nil { 49 return nil, err 50 } 51 } 52 client, err := s.client.connect(ctx, cDialer, s.addr) 53 if err != nil { 54 return nil, err 55 } 56 c, err := client.DialContext(ctx, "tcp", metadata.RemoteAddress()) 57 if err != nil { 58 return nil, err 59 } 60 61 return NewConn(N.NewRefConn(c, s), s), nil 62 } 63 64 type sshClient struct { 65 config *ssh.ClientConfig 66 client *ssh.Client 67 cMutex sync.Mutex 68 } 69 70 func (s *sshClient) connect(ctx context.Context, cDialer C.Dialer, addr string) (client *ssh.Client, err error) { 71 s.cMutex.Lock() 72 defer s.cMutex.Unlock() 73 if s.client != nil { 74 return s.client, nil 75 } 76 c, err := cDialer.DialContext(ctx, "tcp", addr) 77 if err != nil { 78 return nil, err 79 } 80 N.TCPKeepAlive(c) 81 82 defer func(c net.Conn) { 83 safeConnClose(c, err) 84 }(c) 85 86 if ctx.Done() != nil { 87 done := N.SetupContextForConn(ctx, c) 88 defer done(&err) 89 } 90 91 clientConn, chans, reqs, err := ssh.NewClientConn(c, addr, s.config) 92 if err != nil { 93 return nil, err 94 } 95 client = ssh.NewClient(clientConn, chans, reqs) 96 97 s.client = client 98 99 go func() { 100 _ = client.Wait() // wait shutdown 101 _ = client.Close() 102 s.cMutex.Lock() 103 defer s.cMutex.Unlock() 104 if s.client == client { 105 s.client = nil 106 } 107 }() 108 109 return client, nil 110 } 111 112 func (s *sshClient) Close() error { 113 s.cMutex.Lock() 114 defer s.cMutex.Unlock() 115 if s.client != nil { 116 return s.client.Close() 117 } 118 return nil 119 } 120 121 func closeSsh(s *Ssh) { 122 _ = s.client.Close() 123 } 124 125 func NewSsh(option SshOption) (*Ssh, error) { 126 addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) 127 128 config := ssh.ClientConfig{ 129 User: option.UserName, 130 HostKeyCallback: ssh.InsecureIgnoreHostKey(), 131 HostKeyAlgorithms: option.HostKeyAlgorithms, 132 } 133 134 if option.PrivateKey != "" { 135 var b []byte 136 var err error 137 if strings.Contains(option.PrivateKey, "PRIVATE KEY") { 138 b = []byte(option.PrivateKey) 139 } else { 140 b, err = os.ReadFile(C.Path.Resolve(option.PrivateKey)) 141 if err != nil { 142 return nil, err 143 } 144 } 145 var pKey ssh.Signer 146 if option.PrivateKeyPassphrase != "" { 147 pKey, err = ssh.ParsePrivateKeyWithPassphrase(b, []byte(option.PrivateKeyPassphrase)) 148 } else { 149 pKey, err = ssh.ParsePrivateKey(b) 150 } 151 if err != nil { 152 return nil, err 153 } 154 155 config.Auth = append(config.Auth, ssh.PublicKeys(pKey)) 156 } 157 158 if option.Password != "" { 159 config.Auth = append(config.Auth, ssh.Password(option.Password)) 160 } 161 162 if len(option.HostKey) != 0 { 163 keys := make([]ssh.PublicKey, len(option.HostKey)) 164 for i, hostKey := range option.HostKey { 165 key, _, _, _, err := ssh.ParseAuthorizedKey([]byte(hostKey)) 166 if err != nil { 167 return nil, fmt.Errorf("parse host key :%s", key) 168 } 169 keys[i] = key 170 } 171 config.HostKeyCallback = func(hostname string, remote net.Addr, key ssh.PublicKey) error { 172 serverKey := key.Marshal() 173 for _, hostKey := range keys { 174 if bytes.Equal(serverKey, hostKey.Marshal()) { 175 return nil 176 } 177 } 178 return fmt.Errorf("host key mismatch, server send :%s %s", key.Type(), base64.StdEncoding.EncodeToString(serverKey)) 179 } 180 } 181 182 version := "SSH-2.0-OpenSSH_" 183 if fastrand.Intn(2) == 0 { 184 version += "7." + strconv.Itoa(fastrand.Intn(10)) 185 } else { 186 version += "8." + strconv.Itoa(fastrand.Intn(9)) 187 } 188 config.ClientVersion = version 189 190 outbound := &Ssh{ 191 Base: &Base{ 192 name: option.Name, 193 addr: addr, 194 tp: C.Ssh, 195 udp: false, 196 iface: option.Interface, 197 rmark: option.RoutingMark, 198 prefer: C.NewDNSPrefer(option.IPVersion), 199 }, 200 option: &option, 201 client: &sshClient{ 202 config: &config, 203 }, 204 } 205 runtime.SetFinalizer(outbound, closeSsh) 206 207 return outbound, nil 208 }