Go:x/crypto/ssh 的全局请求通道竞争

现象

在自己的程序中使用类似下面的代码监听 SSH 的全局请求:

go
for {
  select {
  case <-ctx.Done():
    return nil
  case req, ok := <-globalReqs:
    if !ok {
      return nil
    }
    // Handle request
    fmt.Println("Received global request:", req.Type)
    req.Reply(true, nil)
  }
}

发现发来的请求有时候没有接收到, 而对端的提示是 reject, 甚至出现 reject, 成功 如此交替出现的情况 (那很公平调度了).

其实很明显是有啥东西和自己的代码在抢同一个通道, 但是因为我一直在找发送端的代码问题, 为这个问题浪费了一晚上的生命, 甚至怀疑这库有 bug

原因

用自定义的 dialer 创建 ssh 客户端时, 代码是像这样的:

go
dialer := &net.Dialer{Timeout: 30 * time.Second}
conn, err := dialer.DialContext(ctx, "tcp", addr)
if err != nil {
  return err
}
sshconn, chans, globalReqs, err := ssh.NewClientConn(conn, addr, conf)
if err != nil {
  return fmt.Errorf("failed to establish SSH connection: %w", err)
}
client := ssh.NewClient(sshconn, chans, reqs)

查看 ssh.NewClient 源码:

go
// NewClient creates a Client on top of the given connection.
func NewClient(c Conn, chans <-chan NewChannel, reqs <-chan *Request) *Client {
	conn := &Client{
		Conn:            c,
		channelHandlers: make(map[string]chan NewChannel, 1),
	}

	go conn.handleGlobalRequests(reqs)
	go conn.handleChannelOpens(chans)
	go func() {
		conn.Wait()
		conn.forwards.closeAll()
	}()
	return conn
}

可以看到它用 handleGlobalRequests 来 handle 了全局请求通道, 这个函数是:

go
func (c *Client) handleGlobalRequests(incoming <-chan *Request) {
	for r := range incoming {
		// This handles keepalive messages and matches
		// the behaviour of OpenSSH.
		r.Reply(false, nil)
	}
}

破案 😇

解决

主要是 ssh.NewClientConn(conn, addr, conf) 紧接着就是 ssh.NewClient 带来了点迷惑性, 前者的返回正好是后者所需的所有参数.

ssh.NewClient<- chan *Request 传个 nil 就行了


Q.E.D.
使用 WireGuard 为 IPV6-Only 机器添加 IPV4 网络