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参数)的核心流程:
- 客户端在本地监听指定端口(示例中的2345)
- 当有本地请求连接到2345端口时,客户端向SSH服务端发起**"direct-tcpip"类型的通道请求**
- 服务端接收请求后,连接到目标地址(示例中的127.0.0.1:5244)
- 服务端将目标地址的响应数据通过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) } }
关键修改点
- 解析请求参数:提取
direct-tcpip请求的源地址和目标地址,打印日志确认请求到达 - 接受通道请求:必须调用
newChannel.Accept(),否则客户端会认为服务端未响应,可能触发底层默认处理 - 实现数据转发:通过双向
io.Copy完成本地请求与目标服务的数据交互 - 明确拒绝无关请求:对session通道和全局转发请求直接拒绝,避免底层静默处理导致日志丢失
修改后,服务端会打印"direct-tcpip"相关日志,同时正常完成端口转发功能。
内容的提问来源于stack exchange,提问作者cwww3
相关产品推荐
相关产品推荐

