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  }