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:
+120
-21
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user