SSH连接超时

14

我正在使用 golang.org/x/crypto/ssh 进行SSH连接,但很惊讶似乎找不到设置 NewSession 函数超时的方法(我也没看到其他任何超时的方式)。如果我尝试连接一个有问题的服务器,它会卡住很长时间。我已经写了一些代码使用 selecttime.After,但感觉这只是一个hack。我还没有尝试过在结构体中保持底层的 net.Conn 并多次调用 Conn.SetDeadline()。因为我不知道 crypto/ssh 库是否覆盖此方法。

有没有人知道如何使用这个库超时处理死亡的服务器?或者有没有更好的库可以推荐?


是的,我认为使用 Conn 上的 SetDeadline() 来执行 NewClient() 是正确的方法。我进行评论而不是回答,因为我对 ssh 包没有特别的熟悉度。grep -R Deadline src/golang.org/x/crypto/ssh 的输出让我想到该包不会妨碍你,但也不会替你完成;唯一的提及似乎与 ssh 端口转发有关。 - twotwotwo
是的,SetDeadline() 是有效的,但我必须说,一个旨在建立连接的库中缺少超时等类似功能似乎是一个重大疏忽。并不是要表现得不感激什么的,也许如果我有时间,我会看看是否可以帮助包含它。 - user3591723
对我来说有点奇怪的是,当我正在建立与我知道已经死亡/出现问题的服务器之一的连接时(我无法从终端ssh进入它),ssh.NewClientConn根本没有返回nil客户端的错误。这就是我的主要问题所在,它试图使用一个死掉的服务器调用NewSession,而这个服务器被传递为活动状态。 - user3591723
如果ssh.NewClientConn返回ok,则服务器并非真正死亡,因为它可以完成ssh握手。 - JimB
@JimB 我还没有仔细研究包源代码,但我可以肯定地说,通过ssh.NewClientConn的某些内容不一定能成功启动新会话,这对我来说似乎是奇怪的行为。当我尝试StartSession时遇到ssh:unexpected packet in response to channel open:<nil>时,如果我回头查看ssh.NewClientConn的错误/输出,我肯定已经得到了一个nil错误。如果那是预期的结果,我认为这没有多大意义。 - user3591723
显示剩余2条评论
2个回答

20

使用ssh包透明处理这个问题的一种方法是通过创建一个带有空闲超时的自定义net.Conn连接来为您设置截止日期。然而,这将导致连接上的后台读取超时,因此我们需要使用ssh keepalives来保持连接打开状态。根据您的用例,仅使用ssh keepalives作为死连接的警报可能已经足够了。

// Conn wraps a net.Conn, and sets a deadline for every read
// and write operation.
type Conn struct {
    net.Conn
    ReadTimeout  time.Duration
    WriteTimeout time.Duration
}

func (c *Conn) Read(b []byte) (int, error) {
    err := c.Conn.SetReadDeadline(time.Now().Add(c.ReadTimeout))
    if err != nil {
        return 0, err
    }
    return c.Conn.Read(b)
}

func (c *Conn) Write(b []byte) (int, error) {
    err := c.Conn.SetWriteDeadline(time.Now().Add(c.WriteTimeout))
    if err != nil {
        return 0, err
    }
    return c.Conn.Write(b)
}

接下来您可以使用net.DialTimeoutnet.Dialer获取连接,使用超时时间对其进行包装,然后将其传递到ssh.NewClientConn中。

func SSHDialTimeout(network, addr string, config *ssh.ClientConfig, timeout time.Duration) (*ssh.Client, error) {
    conn, err := net.DialTimeout(network, addr, timeout)
    if err != nil {
        return nil, err
    }

    timeoutConn := &Conn{conn, timeout, timeout}
    c, chans, reqs, err := ssh.NewClientConn(timeoutConn, addr, config)
    if err != nil {
        return nil, err
    }
    client := ssh.NewClient(c, chans, reqs)

    // this sends keepalive packets every 2 seconds
    // there's no useful response from these, so we can just abort if there's an error
    go func() {
        t := time.NewTicker(2 * time.Second)
        defer t.Stop()
        for range t.C {
            _, _, err := client.Conn.SendRequest("keepalive@golang.org", true, nil)
            if err != nil {
                return
            }
        }
    }()
    return client, nil
}

这绝对比我想出来的更明显,能清楚地展示你在做什么。我可能会实现类似的方案。 - user3591723

9

设置 ssh.ClientConfig 的超时时间。

cfg := ssh.ClientConfig{
    User: "root",
    Auth: []ssh.AuthMethod{
        ssh.PublicKeys(signer),
    },
    HostKeyCallback: ssh.FixedHostKey(hostKey),
    Timeout:         15 * time.Second, // max time to establish connection
}

ssh.Dial("tcp", ip+":22", &cfg)

当您调用ssh.Dial时,超时时间将传递给net.DialTimeout

1
我更喜欢这个答案。 - fallais

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接