go可实现轻量级内网穿透工具,核心是内网客户端主动连接公网服务端建立长连接隧道,通过http connect方法+hijack获取raw net.conn双向转发流量,需注意超时控制、心跳保活、连接复用及正确监听0.0.0.0。

Go 本身不提供内网穿透能力,但可以用它写一个轻量级的反向代理隧道——核心是让内网机器主动连出、外网服务器中转流量,绕过 NAT 和防火墙限制。这不需要复杂协议或加密库,用标准 net/http 和 net 就能跑通基础逻辑。
为什么不用现成工具而自己写?
当你只需要转发 HTTP/HTTPS 流量、控制连接生命周期、嵌入到已有服务中,或调试穿透链路时,自己实现比套一层 frp/ngrok 更透明。关键点不是“替代专业工具”,而是快速验证穿透路径是否通、哪些环节会断、header 怎么透传。
常见错误现象:dial tcp: i/o timeout(内网 client 连不上公网 server)、http: proxy error: context canceled(连接被意外关闭)、404 Not Found(server 端路由没配对)。
- 必须确保公网 server 有固定 IP 或域名,且 80/443(或自定义端口)已放行
- 内网 client 必须能 outbound 连上 server 的监听端口,不依赖 inbound 开放
- 不要在 client 端用
http.ListenAndServe,那是监听本地端口,穿透场景下 client 是发起方
server 端:用 http.Server 接收 tunnel 请求
server 不直接暴露服务端口给外网用户,而是接收来自 client 的长连接(HTTP Upgrade 到 raw TCP),再把数据流转发给真实后端。重点在于复用 http.HandlerFunc 拦截特定 path 的 upgrade 请求。
示例关键逻辑:
http.HandleFunc("/tunnel", func(w http.ResponseWriter, r *http.Request) {
if r.Method != "CONNECT" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
h := w.(http.Hijacker)
conn, _, err := h.Hijack()
if err != nil {
return
}
// 此时 conn 是 raw net.Conn,可双向读写
// 后续从 conn.Read 得到的数据,转发给目标服务(如 localhost:8080)
})
- 用
CONNECT方法更符合隧道语义,比自定义 POST + base64 payload 更易调试 - 必须调用
Hijack(),否则无法脱离 HTTP 协议栈;调用后原ResponseWriter失效 - 别漏掉
conn.Close()和超时控制,否则连接堆积会耗尽 fd
client 端:主动建立并维持隧道连接
client 的职责是定时重连、复用连接、把本地端口流量塞进隧道。不要用 http.Get,要用 http.Client.Do 发起 CONNECT 请求,并拿到底层 net.Conn。
示例片段:
req, _ := http.NewRequest("CONNECT", "http://your-server.com:8080/tunnel", nil)
req.Header.Set("X-Local-Addr", "127.0.0.1:3000") // 告诉 server 转发到哪
resp, err := http.DefaultClient.Do(req)
if err != nil || resp.StatusCode != 200 {
// 重试逻辑放这里
}
conn, _, _ := resp.Body.(io.ReadWriteCloser).(interface{ Conn() net.Conn }).Conn()
-
X-Local-Addr是自定义 header,用于 server 端解析目标地址,避免硬编码 - 务必检查
resp.StatusCode,很多问题卡在 server 返回 502/400 却没打印 - client 本地监听用
net.Listen("tcp", ":3000"),accept 后把每个conn的数据写进隧道conn,注意 goroutine 分流和 close 传递
容易被忽略的细节
隧道稳定性的瓶颈往往不在协议层,而在连接管理:HTTP keep-alive 默认不保活长连接,TCP 层没有心跳,NAT 设备会 30–300 秒清空空闲连接表项。
- server 端需设置
http.Server.IdleTimeout> 5m,否则连接会被静默断开 - client 应定期往隧道写空字节(如 \x00)或发 HTTP OPTIONS,防止中间设备丢连接
- 不要共享一个
http.Client实例处理多个隧道,TLS 配置和连接池可能互相干扰 - 日志里至少记录每次 CONNECT 的 remote addr 和
X-Local-Addr,排查时能立刻定位哪条链路挂了











