sshx

package
v0.7.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 28, 2026 License: MIT Imports: 10 Imported by: 0

Documentation

Overview

Package sshx 是 SSH 客户端的通用原语:拨号、私钥、密钥类型识别、保活。

为什么不直接用 golang.org/x/crypto/ssh:它给的是协议原语,拨号这一步要 做的判断一个都没替你做——连接超时、握手超时、调用方的总预算、以及 「超时」与「被拒绝」这两种失败的区分,都要自己写一遍。写错一次的后果 不是报错难看,而是调用方永远等下去。

本包的四件事:

  • Dial:两段界限的拨号(连接一段、握手一段),并接受调用方的总预算
  • LoadSigner / DetectKeyType:私钥的读、解析与类型识别
  • PublicKeyType:公钥的类型识别
  • StartKeepAlive:空闲连接的保活

用法:

signer, err := sshx.LoadSigner("/root/.ssh/id_rsa")
if err != nil {
	return err
}
client, err := sshx.Dial(ctx, "10.0.0.1:2222", &ssh.ClientConfig{
	User:            "ops",
	Auth:            []ssh.AuthMethod{ssh.PublicKeys(signer)},
	HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}, sshx.DialOptions{Budget: 10 * time.Second})

与 x/crypto/ssh 的关系是叠加不是替代:拨号与凭据用本包,会话与通道 仍然用 x/crypto/ssh。

Index

Constants

View Source
const (
	// DefaultConnectTimeout 界 TCP 连接。
	DefaultConnectTimeout = 15 * time.Second

	// DefaultHandshakeTimeout 界 SSH 协议握手。
	//
	// 健康的握手是毫秒级的,所以这个数是它的许多倍:它在这里是为了接住
	// 病态情况,不是为了催健康的那个。
	//
	// 不对称决定了这个数。误判超时会把快路径推去降级路径,每次调用多花
	// 十秒;而漏了超时会让调用永远不返回,永远不返回的调用没有降级可言。
	// 宁可偏向长的一边——那是便宜的错误。
	DefaultHandshakeTimeout = 5 * time.Second
)

拨号的两道界限。

View Source
const DefaultKeepAliveInterval = 30 * time.Second

DefaultKeepAliveInterval 是戳连接的间隔。

View Source
const KeepAliveRequest = "keepalive@openssh.com"

KeepAliveRequest 是 OpenSSH 的全局 keepalive 请求名。

调用方拿着它往连接上发这个请求(`client.SendRequest(sshx.KeepAliveRequest, false, nil)`)——WantReply 是 false,因为等回包意味着等一个回应,而一个 既不应也不关的服务端会让这个请求卡在连接的整个余生里。

Variables

View Source
var ErrBusy = errors.New("the connection is busy")

ErrBusy 说连接当时正忙着,所以这一下没戳。

它不是失败:忙着说明连接不闲,不闲的连接不需要保温。

Functions

func DetectKeyType

func DetectKeyType(ctx context.Context, path string) (string, error)

DetectKeyType 报告私钥文件的密钥类型("ssh-rsa" / "ssh-ed25519" / "ecdsa-sha2-nistp256" 这类)。

三级回落,越靠前越准:

  1. 直接解析私钥。最准,而且不需要外部进程——公钥类型就在签名器里。
  2. ssh-keygen -y。解析不了的私钥(比如带口令的)在这里还有救:有 agent 时 ssh-keygen 能读出公钥。
  3. 文件头嗅探。最后的手段,只能给出粗粒度的结论。

ctx 管住第二级里那个外部进程:ssh-keygen 在一个卡住的 agent 上会一直 等下去,而调用方不该为此无限期等下去。

读不出文件是错误(调用方拿到的路径就是错的);读得出但认不出类型不是 错误——返回 "unknown" 或粗粒度结论,让调用方自己去判断。

func Dial

func Dial(ctx context.Context, addr string, cc *ssh.ClientConfig, opts DialOptions) (*ssh.Client, error)

Dial 打开到 addr 的 SSH 连接。

ctx 对连接阶段有效:放弃的调用方不会留下一个开着的 socket。握手由固定 界限、调用方预算与 ctx 的截止时间三者中最小的那个界定。

返回的 client 拥有这条连接:Close 它就关掉 socket。

失败分两种,措辞不同,因为调用方对它们的反应不同:一种是超时(对端没 说 SSH),一种是拒绝(对端说了但不让进)。调用方常常要在这两者之间做 降级决定,所以它们不能被揉成同一句话。

func LoadSigner

func LoadSigner(path string) (ssh.Signer, error)

LoadSigner 读私钥文件并解析成签名器。

只认未加密的私钥:带口令的私钥要口令或者要 agent,这两样都不是本包的 事(调用方手里有 ssh-agent 的话,agent 本身会完成握手)。

func ParsePublicKey

func ParsePublicKey(publicKey string) (ssh.PublicKey, error)

ParsePublicKey 解析一行 SSH 公钥(authorized_keys 的那种格式)。

它比「看第一个字段」严格:字段对但内容是坏 base64 的公钥,在这里会被 拒掉,而在字符串前缀检查里它是「通过」的——然后在登录那一刻才失败。

⚠️ 但它**按行扫描**:解析不了的行被跳过,取第一条能解析的。所以输入里 有多把钥匙时,它判的是「第一条能解析的」,不是「第一把」。要的是 「恰好一把」就用 ParseSinglePublicKey。

func ParseSinglePublicKey added in v0.6.1

func ParseSinglePublicKey(publicKey string) (ssh.PublicKey, error)

ParseSinglePublicKey 解析**恰好一把** SSH 公钥。

它拒掉 ParsePublicKey 容忍的两种输入:

  • **多行**:ssh.ParseAuthorizedKey 是逐行扫描的,解析不了的行被跳过。 于是「第一行 ed25519、第二行 rsa」会被判成 rsa——而调用方接下来通常 是把**整个字符串**存下去,堡垒机存的是两把钥匙,且会安静地接受那把 ed25519。**被检查的东西与被存下的东西不是同一个**,这类漏洞极难查。
  • **一行里塞了两把**:authorized_keys 是一行一把;rest 非空说明后面 还有内容。

说清它为什么比 ParsePublicKey 严:ParsePublicKey 回答的是「这里面有 一把能用的钥匙吗」,本函数回答的是「这里面**只有**一把能用的钥匙吗」。 要把这段字符串原样存进某个系统时,后者才是该问的那个。

func PublicKeyType

func PublicKeyType(publicKey string) string

PublicKeyType 报告一行 SSH 公钥的类型("ssh-rsa" / "ssh-ed25519" / "ecdsa-sha2-nistp256" 这类)。

解析不了就退回首字段——「这是什么类型」这个问题即使公钥本身是坏的也 答得出来,而调用方要的往往正是那句「你给的是 ed25519,而这里只收 ssh-rsa」。空串表示这一行什么都没说。

func StartKeepAlive

func StartKeepAlive(poke func() error, done <-chan struct{}, onDead func(), interval time.Duration)

StartKeepAlive 定期戳连接,直到 done 被关掉。

poke 发出这个请求并报告发生了什么:ErrBusy 表示连接正忙着(跳过这一 下),别的错误表示连接没了。onDead 在那时被调用——它是调用方自己的信号: 把连接标记为坏掉,下次调用开一条新的。

interval 是参数,这样测试不必等半分钟才看到一次保活。零值即默认。

它在返回之前不等待:保活是后台的,done 关掉之后 goroutine 自己退出。

Types

type DialOptions

type DialOptions struct {
	// Connect 界 TCP 连接。零值即 DefaultConnectTimeout。
	Connect time.Duration
	// Handshake 界 SSH 握手。零值即 DefaultHandshakeTimeout。
	Handshake time.Duration
	// Budget 是调用方给这次调用的总预算,它同时压住上面两个界限:
	// 给了两秒的调用方不该坐在一个五秒的握手里等一个自己不会用的结果。
	Budget time.Duration
}

DialOptions 界住打开一次连接的过程。

三个值都是零值即默认(见各自的默认常量)。它们是参数而不是常量,这样 测试才能用短的值:要等五秒才看得到一次超时的测试,没人跑。

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL