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:
@@ -0,0 +1,158 @@
|
||||
package starssh
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var ErrSSHAgentTimeout = errors.New("ssh-agent timeout")
|
||||
var dialResolvedSSHAgentFunc = dialResolvedSSHAgent
|
||||
|
||||
type sshAgentDialOptions struct {
|
||||
Endpoint string
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
type resolvedSSHAgentEndpoint struct {
|
||||
Endpoint string
|
||||
Source string
|
||||
Network string
|
||||
}
|
||||
|
||||
type deadlineAgentConn struct {
|
||||
net.Conn
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
func resolveSSHAgentEndpoint(options sshAgentDialOptions) (resolvedSSHAgentEndpoint, error) {
|
||||
endpoint := strings.TrimSpace(options.Endpoint)
|
||||
if endpoint != "" {
|
||||
return resolvedSSHAgentEndpoint{
|
||||
Endpoint: endpoint,
|
||||
Source: "identity-agent",
|
||||
Network: defaultSSHAgentNetwork(endpoint),
|
||||
}, nil
|
||||
}
|
||||
|
||||
endpoint = strings.TrimSpace(os.Getenv("SSH_AUTH_SOCK"))
|
||||
if endpoint != "" {
|
||||
return resolvedSSHAgentEndpoint{
|
||||
Endpoint: endpoint,
|
||||
Source: "SSH_AUTH_SOCK",
|
||||
Network: defaultSSHAgentNetwork(endpoint),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return defaultSSHAgentEndpoint()
|
||||
}
|
||||
|
||||
func dialSSHAgent(options sshAgentDialOptions) (net.Conn, resolvedSSHAgentEndpoint, error) {
|
||||
resolved, err := resolveSSHAgentEndpoint(options)
|
||||
if err != nil {
|
||||
return nil, resolvedSSHAgentEndpoint{}, err
|
||||
}
|
||||
|
||||
conn, err := dialResolvedSSHAgentFunc(resolved, options.Timeout)
|
||||
if isTimeoutError(err) {
|
||||
err = fmt.Errorf("%w: %v", ErrSSHAgentTimeout, err)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, resolved, err
|
||||
}
|
||||
return conn, resolved, nil
|
||||
}
|
||||
|
||||
func dialSSHAgentWithDebug(step string, timeouts sshAgentTimeouts) (net.Conn, resolvedSSHAgentEndpoint, error) {
|
||||
options := sshAgentDialOptions{
|
||||
Endpoint: timeouts.Endpoint,
|
||||
Timeout: timeouts.Dial,
|
||||
}
|
||||
started := time.Now()
|
||||
conn, resolved, err := dialSSHAgent(options)
|
||||
logSSHAgentDebug(timeouts.Debug, SSHAgentDebugEvent{
|
||||
Step: step,
|
||||
Source: resolved.Source,
|
||||
Endpoint: resolved.Endpoint,
|
||||
Network: resolved.Network,
|
||||
Phase: "dial",
|
||||
Status: debugStatus(err),
|
||||
Duration: time.Since(started),
|
||||
Err: err,
|
||||
})
|
||||
return conn, resolved, err
|
||||
}
|
||||
|
||||
func logSSHAgentDebug(debug SSHAgentDebugFunc, event SSHAgentDebugEvent) {
|
||||
if debug == nil {
|
||||
return
|
||||
}
|
||||
debug(event)
|
||||
}
|
||||
|
||||
func debugStatus(err error) string {
|
||||
if err != nil {
|
||||
return "error"
|
||||
}
|
||||
return "ok"
|
||||
}
|
||||
|
||||
func wrapSSHAgentConnWithDeadline(conn net.Conn, timeout time.Duration) net.Conn {
|
||||
if conn == nil || timeout <= 0 {
|
||||
return conn
|
||||
}
|
||||
return &deadlineAgentConn{Conn: conn, timeout: timeout}
|
||||
}
|
||||
|
||||
func (c *deadlineAgentConn) Read(p []byte) (int, error) {
|
||||
c.setDeadline()
|
||||
n, err := c.Conn.Read(p)
|
||||
return n, wrapSSHAgentConnError(err)
|
||||
}
|
||||
|
||||
func (c *deadlineAgentConn) Write(p []byte) (int, error) {
|
||||
c.setDeadline()
|
||||
n, err := c.Conn.Write(p)
|
||||
return n, wrapSSHAgentConnError(err)
|
||||
}
|
||||
|
||||
func (c *deadlineAgentConn) setDeadline() {
|
||||
if c == nil || c.timeout <= 0 || c.Conn == nil {
|
||||
return
|
||||
}
|
||||
_ = c.Conn.SetDeadline(time.Now().Add(c.timeout))
|
||||
}
|
||||
|
||||
func isTimeoutError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
if errors.Is(err, os.ErrDeadlineExceeded) {
|
||||
return true
|
||||
}
|
||||
var netErr net.Error
|
||||
return errors.As(err, &netErr) && netErr.Timeout()
|
||||
}
|
||||
|
||||
func wrapSSHAgentConnError(err error) error {
|
||||
if isTimeoutError(err) {
|
||||
return fmt.Errorf("%w: %v", ErrSSHAgentTimeout, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func normalizeSSHAgentError(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if errors.Is(err, ErrSSHAgentTimeout) {
|
||||
return err
|
||||
}
|
||||
if strings.Contains(err.Error(), ErrSSHAgentTimeout.Error()) {
|
||||
return fmt.Errorf("%w: %v", ErrSSHAgentTimeout, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
Reference in New Issue
Block a user