You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

OpenSSH本地端口转发实现机制及go-ssh服务端未接收direct-tcpip问题

问题描述
  • 使用go-ssh搭建了sshd服务监听SSH客户端
  • 执行命令:ssh -L 2345:127.0.0.1:5244 127.0.0.1 -p 2022 -N
  • 现象:能通过127.0.0.1:2345访问网页,但服务端未收到任何客户端消息(包括"direct-tcpip")
  • 需求:了解OpenSSH本地端口转发的实现逻辑,同时让服务端能接收到"direct-tcpip"消息

用户提供的服务端代码:

sshConn, ch, globalRequestCh, err := ssh.NewServerConn(conn, config)
if err != nil {
    log.Println("Handshake  err", err)
    return
}

log.Println("sshd serving", "ssh client", sshConn.RemoteAddr())

    
go func() {
// handle newChan
for newChannel := range ch {
    log.Println("new channel", "type", newChannel.ChannelType())
    switch newChannel.ChannelType() {
    case "session":
                
    case "direct-tcpip":
                
    default:

        }
}
}()

// handle globalRequest
for globalRequest := range globalRequestCh {
log.Println("global request", "type", globalRequest.Type)
switch globalRequest.Type {
    case "tcpip-forward":

    case "cancel-tcpip-forward":

    default:

    }
}

OpenSSH本地端口转发的实现逻辑

本地端口转发(-L参数)的核心流程:

  1. 客户端在本地监听指定端口(示例中的2345)
  2. 当有本地请求连接到2345端口时,客户端向SSH服务端发起**"direct-tcpip"类型的通道请求**
  3. 服务端接收请求后,连接到目标地址(示例中的127.0.0.1:5244)
  4. 服务端将目标地址的响应数据通过SSH通道回传给客户端,客户端再转发给本地请求方,完成双向数据传输

服务端未收到"direct-tcpip"消息的原因及解决方法

你的代码仅监听了通道请求,但未处理direct-tcpip通道的确认逻辑,导致go-ssh底层可能自动完成了转发流程,服务端未捕获到日志。要让服务端收到并处理该消息,需修改代码如下:

修改后的代码示例

import (
    "io"
    "log"
    "net"
    "golang.org/x/crypto/ssh"
)

// 省略conn和config的初始化逻辑
sshConn, ch, globalRequestCh, err := ssh.NewServerConn(conn, config)
if err != nil {
    log.Println("Handshake err", err)
    return
}

log.Println("sshd serving", "ssh client", sshConn.RemoteAddr())

go func() {
    // 处理新通道请求
    for newChannel := range ch {
        log.Println("new channel", "type", newChannel.ChannelType())
        switch newChannel.ChannelType() {
        case "session":
            // 拒绝不需要的session通道
            newChannel.Reject(ssh.UnknownChannelType, "session not supported")
        case "direct-tcpip":
            // 解析direct-tcpip请求参数
            reqInfo, ok := newChannel.ExtraData().(*ssh.DirectTCPIPRequest)
            if !ok {
                newChannel.Reject(ssh.Prohibited, "invalid direct-tcpip request")
                continue
            }
            log.Printf("received direct-tcpip request: from %s to %s:%d", 
                reqInfo.OriginAddr.String(), reqInfo.TargetAddr.IP, reqInfo.TargetAddr.Port)
            
            // 连接到目标服务地址
            targetConn, err := net.Dial("tcp", reqInfo.TargetAddr.String())
            if err != nil {
                log.Println("dial target err:", err)
                newChannel.Reject(ssh.ConnectionFailed, err.Error())
                continue
            }
            
            // 接受通道请求,这是触发客户端转发的关键步骤
            channel, reqs, err := newChannel.Accept()
            if err != nil {
                log.Println("accept channel err:", err)
                targetConn.Close()
                continue
            }
            
            // 忽略通道内的额外请求
            go ssh.DiscardRequests(reqs)
            
            // 双向转发数据,完成端口转发流程
            go func() {
                io.Copy(channel, targetConn)
                channel.CloseWrite()
                targetConn.Close()
            }()
            go func() {
                io.Copy(targetConn, channel)
                targetConn.CloseWrite()
                channel.Close()
            }()
        default:
            log.Println("unknown channel type:", newChannel.ChannelType())
            newChannel.Reject(ssh.UnknownChannelType, "unknown channel type")
        }
    }
}()

// 处理全局请求,本地转发不需要这些请求,直接拒绝
for globalRequest := range globalRequestCh {
    log.Println("global request", "type", globalRequest.Type)
    switch globalRequest.Type {
    case "tcpip-forward", "cancel-tcpip-forward":
        globalRequest.Reply(false, nil)
    default:
        globalRequest.Reply(false, nil)
    }
}

关键修改点

  1. 解析请求参数:提取direct-tcpip请求的源地址和目标地址,打印日志确认请求到达
  2. 接受通道请求:必须调用newChannel.Accept(),否则客户端会认为服务端未响应,可能触发底层默认处理
  3. 实现数据转发:通过双向io.Copy完成本地请求与目标服务的数据交互
  4. 明确拒绝无关请求:对session通道和全局转发请求直接拒绝,避免底层静默处理导致日志丢失

修改后,服务端会打印"direct-tcpip"相关日志,同时正常完成端口转发功能。

内容的提问来源于stack exchange,提问作者cwww3

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.27 17:27:05