feat: 增强 ssh-agent 认证与转发可靠性

- 拆分 ssh-agent 认证、连接与 endpoint 解析逻辑
- 新增 IdentityAgent、SSHAgentTimeout、SSHAgentForwardTimeout 和调试事件
- 为 agent list/sign 操作增加独立 deadline,避免硬件 agent 卡死登录
- 支持 agent signer 失败后跳过坏 key 并重试后续 key
- 优先处理 RSA-SHA2 签名,兼容现代 OpenSSH 认证要求
- 增强 agent forwarding 的探测、通道空闲超时和关闭清理
- 补充 Windows OpenSSH pipe 与 GPG S.gpg-agent.ssh socket 文件支持
- 增加相关回归测试和 Windows 编译验证覆盖
This commit is contained in:
2026-05-27 13:10:35 +08:00
parent ad7c8b0587
commit 0c23e7d4bf
10 changed files with 2173 additions and 294 deletions
+120 -21
View File
@@ -19,12 +19,12 @@ var requestSSHAgentForwarding = func(session *ssh.Session) error {
const sshAgentChannelType = "auth-agent@openssh.com"
var routeSSHAgentForwarding = func(client *ssh.Client, timeout time.Duration) (io.Closer, error) {
return startSSHAgentForwardProxy(client, timeout)
var routeSSHAgentForwarding = func(client *ssh.Client, timeouts sshAgentTimeouts) (io.Closer, error) {
return startSSHAgentForwardProxy(client, timeouts)
}
var probeSSHAgentForwarding = func(timeout time.Duration) error {
conn, err := dialSSHAgent(timeout)
var probeSSHAgentForwarding = func(timeouts sshAgentTimeouts) error {
conn, _, err := dialSSHAgentWithDebug("forward-probe", timeouts)
if err != nil {
return wrapSSHAgentForwardingUnavailable(err)
}
@@ -57,11 +57,15 @@ func (p *sshAgentForwardProxy) Close() error {
}
type sshAgentForwardBridge struct {
proxy *sshAgentForwardProxy
channel ssh.Channel
conn net.Conn
proxy *sshAgentForwardProxy
channel ssh.Channel
conn net.Conn
idleTimeout time.Duration
closeOnce sync.Once
closeOnce sync.Once
signalOnce sync.Once
done chan struct{}
activity chan struct{}
}
func (s *StarSSH) RequestAgentForwarding(session *ssh.Session) error {
@@ -111,14 +115,14 @@ func (s *StarSSH) ensureAgentForwarding() error {
return err
}
timeout := effectiveDialTimeout(s.LoginInfo)
if err := probeSSHAgentForwarding(timeout); err != nil {
timeouts := effectiveSSHAgentTimeouts(s.LoginInfo)
if err := probeSSHAgentForwarding(timeouts); err != nil {
return wrapSSHAgentForwardingUnavailable(err)
}
if s.closing.Load() {
return errSSHClientClosing
}
closer, err := routeSSHAgentForwarding(client, timeout)
closer, err := routeSSHAgentForwarding(client, timeouts)
if err != nil {
return err
}
@@ -182,7 +186,7 @@ func wrapSSHAgentForwardingUnavailable(err error) error {
return fmt.Errorf("%w: %v", errSSHAgentForwardingUnavailable, err)
}
func startSSHAgentForwardProxy(client *ssh.Client, timeout time.Duration) (io.Closer, error) {
func startSSHAgentForwardProxy(client *ssh.Client, timeouts sshAgentTimeouts) (io.Closer, error) {
if client == nil {
return nil, errors.New("ssh client is nil")
}
@@ -204,18 +208,18 @@ func startSSHAgentForwardProxy(client *ssh.Client, timeout time.Duration) (io.Cl
if !ok {
return
}
go handleSSHAgentForwardChannel(proxy, ch, timeout)
go handleSSHAgentForwardChannel(proxy, ch, timeouts)
}
}
}()
return proxy, nil
}
func handleSSHAgentForwardChannel(proxy *sshAgentForwardProxy, ch ssh.NewChannel, timeout time.Duration) {
func handleSSHAgentForwardChannel(proxy *sshAgentForwardProxy, ch ssh.NewChannel, timeouts sshAgentTimeouts) {
if ch == nil {
return
}
conn, err := dialSSHAgent(timeout)
conn, _, err := dialSSHAgentWithDebug("forward-channel", timeouts)
if err != nil {
_ = ch.Reject(ssh.ConnectionFailed, err.Error())
return
@@ -224,7 +228,6 @@ func handleSSHAgentForwardChannel(proxy *sshAgentForwardProxy, ch ssh.NewChannel
_ = ch.Reject(ssh.ConnectionFailed, "ssh-agent connection unavailable")
return
}
channel, reqs, err := ch.Accept()
if err != nil {
_ = conn.Close()
@@ -233,9 +236,10 @@ func handleSSHAgentForwardChannel(proxy *sshAgentForwardProxy, ch ssh.NewChannel
go ssh.DiscardRequests(reqs)
bridge := &sshAgentForwardBridge{
proxy: proxy,
channel: channel,
conn: conn,
proxy: proxy,
channel: channel,
conn: conn,
idleTimeout: timeouts.Forward,
}
if !proxy.registerBridge(bridge) {
bridge.close()
@@ -256,18 +260,27 @@ func (b *sshAgentForwardBridge) run() {
if b == nil {
return
}
b.ensureSignals()
stopWatchdog := b.startIdleWatchdog()
defer stopWatchdog()
defer b.unregister()
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
_, _ = io.Copy(b.channel, b.conn)
_, _ = io.Copy(
sshAgentForwardActivityWriter{Writer: b.channel, touch: b.touch},
sshAgentForwardActivityReader{Reader: b.conn, touch: b.touch},
)
b.close()
}()
go func() {
defer wg.Done()
_, _ = io.Copy(b.conn, b.channel)
_, _ = io.Copy(
sshAgentForwardActivityWriter{Writer: b.conn, touch: b.touch},
sshAgentForwardActivityReader{Reader: b.channel, touch: b.touch},
)
b.close()
}()
wg.Wait()
@@ -278,6 +291,8 @@ func (b *sshAgentForwardBridge) close() {
return
}
b.closeOnce.Do(func() {
b.ensureSignals()
close(b.done)
closeWriter(b.channel)
closeWriter(b.conn)
if b.channel != nil {
@@ -289,6 +304,90 @@ func (b *sshAgentForwardBridge) close() {
})
}
func (b *sshAgentForwardBridge) ensureSignals() {
if b == nil {
return
}
b.signalOnce.Do(func() {
b.done = make(chan struct{})
b.activity = make(chan struct{}, 1)
})
}
func (b *sshAgentForwardBridge) startIdleWatchdog() func() {
if b == nil || b.idleTimeout <= 0 {
return func() {}
}
b.ensureSignals()
timer := time.NewTimer(b.idleTimeout)
stopped := make(chan struct{})
go func() {
defer timer.Stop()
for {
select {
case <-timer.C:
b.close()
return
case <-b.activity:
resetTimer(timer, b.idleTimeout)
case <-b.done:
return
case <-stopped:
return
}
}
}()
return func() {
close(stopped)
}
}
func (b *sshAgentForwardBridge) touch() {
if b == nil || b.idleTimeout <= 0 || b.activity == nil {
return
}
select {
case b.activity <- struct{}{}:
default:
}
}
type sshAgentForwardActivityReader struct {
io.Reader
touch func()
}
func (r sshAgentForwardActivityReader) Read(p []byte) (int, error) {
n, err := r.Reader.Read(p)
if n > 0 && r.touch != nil {
r.touch()
}
return n, err
}
type sshAgentForwardActivityWriter struct {
io.Writer
touch func()
}
func (w sshAgentForwardActivityWriter) Write(p []byte) (int, error) {
n, err := w.Writer.Write(p)
if n > 0 && w.touch != nil {
w.touch()
}
return n, err
}
func resetTimer(timer *time.Timer, timeout time.Duration) {
if !timer.Stop() {
select {
case <-timer.C:
default:
}
}
timer.Reset(timeout)
}
func (b *sshAgentForwardBridge) unregister() {
if b == nil || b.proxy == nil {
return