Jelajahi Sumber

Add SSH outbound host key validation

lyc8503 2 tahun lalu
induk
melakukan
df3a982141

+ 7 - 0
docs/configuration/outbound/ssh.md

@@ -12,6 +12,9 @@
   "private_key": "",
   "private_key_path": "$HOME/.ssh/id_rsa",
   "private_key_passphrase": "",
+  "host_key": [
+    "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdH..."
+  ],
   "host_key_algorithms": [],
   "client_version": "SSH-2.0-OpenSSH_7.4p1",
 
@@ -51,6 +54,10 @@ Private key path.
 
 Private key passphrase.
 
+#### host_key
+
+Host key. Accept any if empty.
+
 #### host_key_algorithms
 
 Host key algorithms.

+ 7 - 0
docs/configuration/outbound/ssh.zh.md

@@ -12,6 +12,9 @@
   "private_key": "",
   "private_key_path": "$HOME/.ssh/id_rsa",
   "private_key_passphrase": "",
+  "host_key": [
+    "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdH..."
+  ],
   "host_key_algorithms": [],
   "client_version": "SSH-2.0-OpenSSH_7.4p1",
 
@@ -51,6 +54,10 @@ SSH 用户, 默认使用 root。
 
 密钥密码。
 
+#### host_key
+
+主机密钥,留空接受所有。
+
 #### host_key_algorithms
 
 主机密钥算法。

+ 1 - 0
option/ssh.go

@@ -8,6 +8,7 @@ type SSHOutboundOptions struct {
 	PrivateKey           string           `json:"private_key,omitempty"`
 	PrivateKeyPath       string           `json:"private_key_path,omitempty"`
 	PrivateKeyPassphrase string           `json:"private_key_passphrase,omitempty"`
+	HostKey              Listable[string] `json:"host_key,omitempty"`
 	HostKeyAlgorithms    Listable[string] `json:"host_key_algorithms,omitempty"`
 	ClientVersion        string           `json:"client_version,omitempty"`
 }

+ 22 - 1
outbound/ssh.go

@@ -1,7 +1,9 @@
 package outbound
 
 import (
+	"bytes"
 	"context"
+	"encoding/base64"
 	"math/rand"
 	"net"
 	"os"
@@ -32,6 +34,7 @@ type SSH struct {
 	dialer            N.Dialer
 	serverAddr        M.Socksaddr
 	user              string
+	hostKey           []ssh.PublicKey
 	hostKeyAlgorithms []string
 	clientVersion     string
 	authMethod        []ssh.AuthMethod
@@ -91,6 +94,15 @@ func NewSSH(ctx context.Context, router adapter.Router, logger log.ContextLogger
 		}
 		outbound.authMethod = append(outbound.authMethod, ssh.PublicKeys(signer))
 	}
+	if len(options.HostKey) > 0 {
+		for _, hostKey := range options.HostKey {
+			key, _, _, _, err := ssh.ParseAuthorizedKey([]byte(hostKey))
+			if err != nil {
+				return nil, E.New("parse host key ", key)
+			}
+			outbound.hostKey = append(outbound.hostKey, key)
+		}
+	}
 	return outbound, nil
 }
 
@@ -126,7 +138,16 @@ func (s *SSH) connect() (*ssh.Client, error) {
 		ClientVersion:     s.clientVersion,
 		HostKeyAlgorithms: s.hostKeyAlgorithms,
 		HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
-			return nil
+			if len(s.hostKey) == 0 {
+				return nil
+			}
+			serverKey := key.Marshal()
+			for _, hostKey := range s.hostKey {
+				if bytes.Equal(serverKey, hostKey.Marshal()) {
+					return nil
+				}
+			}
+			return E.New("host key mismatch, server send ", key.Type(), " ", base64.StdEncoding.EncodeToString(serverKey))
 		},
 	}
 	clientConn, chans, reqs, err := ssh.NewClientConn(conn, s.serverAddr.Addr.String(), config)