反向隧道必须用 ssh.ServerConfig 启动监听端,而非 ssh.Dial;需配置 PasswordCallback 或 PublicKeyCallback、HostKey,监听 127.0.0.1:2222,仅处理 direct-tcpip 类型 channel,解析目标地址后手动双向转发并设超时。

反向隧道必须用 ssh.ServerConfig 启动监听端,不是 ssh.Dial
很多人一上来就查 ssh.Dial,以为像正向连接一样“连过去再转发”,结果卡在连接建立后没响应。反向隧道本质是:远端(如服务器)主动连回本地(如内网机器),所以本地得先起一个 SSH 服务端,等对方来连。ssh.Dial 只能做客户端,没法接收连接;真正要用的是 ssh.ServerConfig + net.Listen + ssh.NewServerConn 这套组合。
常见错误现象:ssh: handshake failed: ssh: no auth passed 或连接直接 EOF —— 大多因为没配好 ServerConfig 的认证回调(PasswordCallback 或 PublicKeyCallback),或者没处理好密钥验证逻辑。
- 必须实现
ssh.PasswordCallback或ssh.PublicKeyCallback,哪怕只返回nil(测试用) - 私钥需用
ssh.ParsePrivateKey解析,不能直接传 PEM 字节流 -
ssh.ServerConfig的HostKey必须设置,否则握手失败 - 监听地址建议用
127.0.0.1:2222而非:2222,避免暴露到外网
ssh.Channel 类型判断决定是否转发:只处理 direct-tcpip 类型
SSH 协议里,端口转发请求走的是特殊 channel 类型,不是普通 exec 或 shell。收到新 channel 后,必须先检查 ch.GetType() 是否为 "direct-tcpip",否则直接拒绝或关闭,否则会误处理其他 channel 导致连接中断。
使用场景:你希望本地监听的 2222 端口,被远程 ssh -R 2222:localhost:8080 user@local 触发后,把流量转到本机 localhost:8080。这个“转”就是靠识别 direct-tcpip 并解析其目标地址完成的。
立即学习“go语言免费学习笔记(深入)”;
-
ch, requests, err := ssh.NewServerConn(conn, serverConfig)后,对每个ch做if ch.GetType() == "direct-tcpip" { ... } - 用
ssh.ParseDirectTCPIPRequest解析请求里的Addr和Port,别手动拆字符串 - 转发目标地址(如
"localhost:8080")要由远端通过-R指定,本地代码不硬编码 - 记得用
ch.SendRequest("success", false, nil)回复成功,否则 OpenSSH 客户端会重试或报错
转发链路要自己建:从 ssh.Channel 到 net.Conn 需显式桥接
Go 的 crypto/ssh 不提供自动转发能力。拿到 direct-tcpip channel 后,你要手动 dial 目标服务、启动 goroutine 双向拷贝数据。漏掉任一方向(比如只 copy client→target,不 copy target→client),连接就卡死或半开。
性能影响:每个隧道请求都会启两个 goroutine(读+写),并发高时注意 goroutine 泄漏。别用 io.Copy 单向完事,必须成对启动。
- 用
net.Dial连目标服务,地址来自ssh.ParseDirectTCPIPRequest - 启动 goroutine:
go io.Copy(ch, targetConn) - 再启一个:
go io.Copy(targetConn, ch) - 务必加
defer ch.Close()和defer targetConn.Close(),防止 fd 耗尽 - 建议设
ch.SetDeadline,避免僵尸连接占着 channel 不释放
OpenSSH 客户端命令和 Go 服务端参数要严格匹配
本地 Go 程序跑起来后,远端执行 ssh -R 时,端口、用户、密钥路径、服务器地址都得和 Go 服务端监听配置一致。最容易踩的坑是:Go 监听 127.0.0.1:2222,但远端却连 public-ip:2222,或者用了错误的私钥导致认证失败。
错误信息典型例子:Warning: remote port forwarding failed for listen port 2222 —— 先确认 Go 端是否真在监听、防火墙是否放行、ssh -v 是否显示认证成功。
- 远端命令示例:
ssh -N -R 2222:localhost:8080 -p 22 -i id_rsa user@127.0.0.1 -o StrictHostKeyChecking=no -
-N表示不执行远程命令,只做端口转发 -
-R 2222:localhost:8080中的localhost是对远端而言的,即远端本机的 8080 端口 - Go 服务端的
ServerConfig用户名必须和ssh命令里的user一致(或回调里允许任意用户) - 如果远端是 macOS,可能默认禁用
AllowTcpForwarding,需改/etc/ssh/sshd_config
实际部署时,direct-tcpip 请求的目标地址完全由远端控制,本地代码必须校验合法性(比如禁止连 192.168.0.0/16 外网段),否则等于开了个代理入口。这点很容易被忽略。










