你有没有过这样的烦恼?
公司电脑开着远程桌面,回家想连却连不上;
家里的 NAS 装满了电影,出门在外却只能干瞪眼;
服务器在内网,SSH 都进不去……
别急!今天,咱们就用 Go 代码,手搓一个"穿墙术"工具,让你的内网服务秒变公网可访问!
🧙♂️ 什么是"内网穿透"?
简单说:让外网的人,能访问你藏在内网的服务。
但你家路由器没公网 IP,公司防火墙又严得像保安队长……怎么办?
答案:找一个"中间人"帮忙传话!
这个"中间人"就是我们今天的主角 —— 一个运行在公网服务器上的代理中转站。
而你内网的机器,会主动"联系"这个中间人,说:"我准备好啦,有客人来就喊我!"
🧩 整体架构:三兄弟联手干活
我们的系统由三个角色组成:
| 角色 | 干啥的 | 端口 |
|---|---|---|
| tcpProxyServer | 公网"中间人",负责牵线搭桥 | :1111(客人入口)、:2222(内网客户端入口)、:8678(RPC 控制通道) |
| tpClient | 内网"小弟",主动连接中间人 | 运行在内网机器上 |
| 真实服务 | 比如 RDP、SSH、Web 服务 | 192.168.x.x:3389 等 |
想象一下:
你(外网用户)去酒吧(tcpProxyServer)找朋友(内网服务)。
酒保说:"你朋友在后巷等你,但得对暗号!"
你报上暗号,酒保一挥手:"后巷那位,就是他!"
于是你们成功接头,开始喝酒(数据传输)🍺
🔑 核心原理:暗号配对 + 双向转发
第一步:客人来了!
你用 RDP 连 公网IP:1111 → tcpProxyServer 接受连接,生成一个 暗号(key),然后它把暗号塞进一个"传话筒"(Go 的 channel)。
第二步:内网小弟在线等活!
tpClient 早就连上了 tcpProxyServer 的 RPC 接口(:8678),一直在问:"老板,有活儿吗?" 一旦有客人,RPC 就把暗号返回给 tpClient。
第三步:小弟带着暗号去接头!
tpClient 拿到暗号后,干两件事:
- 连上内网真实服务(比如 192.168.2.28:3389)
- 主动连接 tcpProxyServer 的 :2222 端口,并大声喊出暗号
第四步:酒保核对暗号!
tcpProxyServer 在 :2222 端口等着,收到连接后读取密钥,和之前生成的暗号比对。 ✅ 一致?配对成功! ❌ 不一致?继续等下一个……
第五步:搭桥!双向转发!
一旦配对成功,tcpProxyServer 就在两个连接之间架起一座桥,数据哗哗地流!
🛠️ 使用场景:谁需要这个?
- 远程办公:在家连公司内网的 Windows 远程桌面(RDP)
- 家庭 NAS 访问:出门在外看家里的电影、照片
- 开发调试:本地跑了个 Web 服务,想让同事临时访问
- IoT 设备管理:树莓派在内网,想远程 SSH 控制
💡 只要你有一台带公网 IP 的小服务器(比如腾讯云/阿里云最低配),就能玩转!
🚀 实战演示
1. 编译并启动 tcpProxyServer(在公网服务器上)
go build -o tcpProxyServer.exe tcpProxyServer/main.go
./tcpProxyServer.exe -auth=自定义密钥 -client-port=1111 -bridge-port=2222 -rpc-port=8678
# 输出:业务端口已开启,RPC服务已开启
2. 在内网机器上启动 tpClient
go build -o tpClient.exe tpClient/main.go
# 格式:./tpClient.exe -auth=自定义密钥 -rpc=公网服务器IP:8678 -remote=公网服务器IP:2222 -local=内网服务IP:端口
./tpClient.exe -auth=自定义密钥 -rpc=1.2.3.4:8678 -remote=1.2.3.4:2222 -local=192.168.2.28:3389
3. 外网用户连接
用 RDP 客户端连接:1.2.3.4:1111
✅ 成功进入内网 Windows!
⚠️ 注意事项
- 安全性:当前只靠一个密钥做简单认证,生产环境务必加 TLS + 动态 token!
- 配置灵活:支持通过命令行参数自定义端口和认证密钥
- 使用便捷:客户端支持自动重试和超时设置
💻 源码详解!
tcpProxyServer/main.go
package main
import (
"crypto/md5"
"encoding/hex"
"flag"
"fmt"
"io"
"log"
"net"
"net/http"
"net/rpc"
"time"
)
// 配置项,将从命令行参数读取
var (
// 认证密钥,客户端必须提供此密钥才能获取连接信息
authKey = flag.String("auth", "mgxKey111111156", "客户端认证密钥")
// 客户端连接端口
clientListenPort = flag.String("client-port", "1111", "客户端连接端口")
// 桥接服务端口
bridgeListenPort = flag.String("bridge-port", "2222", "桥接服务端口")
// RPC服务端口
rpcListenPort = flag.String("rpc-port", "8678", "RPC服务端口")
)
// 全局通道,用于传递新客户端的连接密钥
var newClientChan = make(chan string)
func main() {
// 解析命令行参数
flag.Parse()
log.Printf("服务启动配置 - 认证密钥: %s, 客户端端口: %s, 桥接端口: %s, RPC端口: %s",
*authKey, *clientListenPort, *bridgeListenPort, *rpcListenPort)
// 启动RPC服务
go startRpcService()
// 监听客户端连接端口
clientListener, err := net.Listen("tcp", ":"+*clientListenPort)
if err != nil {
log.Fatalf("客户端口监听失败: %v", err)
}
defer clientListener.Close()
// 监听桥接服务端口
bridgeListener, err := net.Listen("tcp", ":"+*bridgeListenPort)
if err != nil {
log.Fatalf("桥接端口监听失败: %v", err)
}
defer bridgeListener.Close()
log.Println("业务端口已开启")
// 主循环,处理客户端和桥接连接
for {
// 接受客户端连接
clientConn, err := clientListener.Accept()
if err != nil {
log.Printf("接受客户端连接失败: %v", err)
continue
}
log.Println("客户进入,等待服务端连接")
// 为客户端生成唯一连接密钥
connKey := generateConnectionKey(clientConn.RemoteAddr().String())
// 将密钥发送到通道,供RPC服务使用
newClientChan <- connKey
var bridgeConn net.Conn
// 等待匹配的桥接连接
for {
bridgeConn, err = bridgeListener.Accept()
if err != nil {
log.Printf("接受桥接连接失败: %v", err)
clientConn.Close()
continue
}
// 读取桥接连接发送的密钥
keyBuffer := make([]byte, 32)
_, err := bridgeConn.Read(keyBuffer)
if err != nil {
log.Printf("读取桥接连接密钥失败: %v", err)
continue
}
// 验证密钥是否匹配
if string(keyBuffer) == connKey {
break
}
// 密钥不匹配,短暂休眠后继续等待
time.Sleep(time.Millisecond * 100)
}
log.Println("服务端开启连接")
// 启动数据转发协程
go forwardData(clientConn, bridgeConn)
}
}
// TcpProxyRpc 提供RPC服务,用于客户端获取连接密钥
type TcpProxyRpc struct {
}
// NewClient 客户端调用此方法获取连接密钥
// 参数:
// - mykey: 客户端提供的认证密钥
// - ret: 返回的连接密钥
func (tpr *TcpProxyRpc) NewClient(mykey string, ret *string) error {
// 验证客户端认证密钥
if mykey != *authKey {
return fmt.Errorf("认证失败: 密钥错误")
}
// 从通道获取下一个可用的连接密钥
*ret = <-newClientChan
return nil
}
// 启动RPC服务,用于客户端获取连接密钥
func startRpcService() {
// 注册RPC服务
rpc.Register(new(TcpProxyRpc))
// 设置使用HTTP协议作为RPC载体
rpc.HandleHTTP()
// 监听RPC端口
listener, err := net.Listen("tcp", ":"+*rpcListenPort)
if err != nil {
log.Fatalf("开启RPC失败: %v", err)
}
// 启动HTTP服务处理RPC请求
log.Printf("RPC服务已开启,监听端口: %s", *rpcListenPort)
err = http.Serve(listener, nil)
if err != nil {
log.Fatalf("监听RPC失败: %v", err)
}
}
// generateConnectionKey 根据输入字符串生成MD5哈希值作为连接密钥
func generateConnectionKey(input string) string {
hasher := md5.New()
hasher.Write([]byte(input))
return hex.EncodeToString(hasher.Sum(nil))
}
// forwardData 在两个连接之间双向转发数据
func forwardData(conn1, conn2 net.Conn) {
defer conn1.Close()
defer conn2.Close()
// 使用通道同步两个协程的结束
done := make(chan struct{}, 2)
// 从conn2转发到conn1
go func() {
_, err := io.Copy(conn1, conn2)
if err != nil && err != io.EOF {
log.Printf("数据转发错误(conn2->conn1): %v", err)
}
done <- struct{}{}
}()
// 从conn1转发到conn2
go func() {
_, err := io.Copy(conn2, conn1)
if err != nil && err != io.EOF {
log.Printf("数据转发错误(conn1->conn2): %v", err)
}
done <- struct{}{}
}()
// 等待任意一个方向的数据传输结束
<-done
}
tpClient/main.go
package main
import (
"flag"
"io"
"log"
"net"
"net/rpc"
"os"
"time"
)
// 配置项,将从命令行参数读取
var (
// RPC服务器地址
rpcServerAddr = flag.String("rpc", "", "RPC服务器地址 (格式: IP:端口)")
// 远程服务器地址
remoteServerAddr = flag.String("remote", "", "远程服务器地址 (格式: IP:端口)")
// 本地监听地址
localListenAddr = flag.String("local", "", "本地监听地址 (格式: IP:端口)")
// 认证密钥,与服务端保持一致
authKey = flag.String("auth", "mgxKey111111156", "客户端认证密钥")
// 连接超时时间(秒)
connTimeoutSeconds = flag.Int("timeout", 5, "连接超时时间(秒)")
// 重试间隔时间(秒)
retryIntervalSeconds = flag.Int("retry", 1, "重试间隔时间(秒)")
)
func main() {
// 解析命令行参数
flag.Parse()
// 检查必需的命令行参数
if *rpcServerAddr == "" || *remoteServerAddr == "" || *localListenAddr == "" {
log.Fatalf("使用方法: %s -rpc=<RPC服务器地址> -remote=<远程服务器地址> -local=<本地监听地址> [-auth=<认证密钥>] [-timeout=<超时时间>] [-retry=<重试间隔>]", os.Args[0])
}
// 根据命令行参数设置超时和重试间隔
connTimeout := time.Duration(*connTimeoutSeconds) * time.Second
retryInterval := time.Duration(*retryIntervalSeconds) * time.Second
log.Printf("客户端启动 - RPC服务器: %s, 远程服务器: %s, 本地监听: %s, 认证密钥: %s, 超时: %v, 重试间隔: %v",
*rpcServerAddr, *remoteServerAddr, *localListenAddr, *authKey, connTimeout, retryInterval)
// 连接RPC服务器
rpcClient, err := rpc.DialHTTP("tcp", *rpcServerAddr)
if err != nil {
log.Fatalf("连接RPC服务器失败: %v", err)
}
defer rpcClient.Close()
// 主循环,不断获取新的连接密钥并建立代理
for {
// 获取连接密钥
var connKey string
err = rpcClient.Call("TcpProxyRpc.NewClient", *authKey, &connKey)
if err != nil {
log.Printf("获取连接密钥失败: %v, 将在%v后重试", err, retryInterval)
time.Sleep(retryInterval)
continue
}
log.Printf("成功获取连接密钥: %s", connKey)
// 启动工作协程处理数据转发
go handleConnection(connKey, *remoteServerAddr, *localListenAddr, connTimeout)
}
}
// handleConnection 处理单个代理连接
// 参数:
// - connKey: 连接密钥,用于与远程服务器建立匹配连接
// - remoteServerAddr: 远程服务器地址
// - localListenAddr: 本地监听地址
// - timeout: 连接超时时间
func handleConnection(connKey, remoteServerAddr, localListenAddr string, timeout time.Duration) {
// 连接远程服务器(tcpProxyServer的桥接端口)
remoteConn, err := net.DialTimeout("tcp", remoteServerAddr, timeout)
if err != nil {
log.Printf("连接远程服务器失败: %v", err)
return
}
log.Printf("已连接到远程服务器: %s", remoteServerAddr)
// 发送连接密钥到远程服务器
_, err = remoteConn.Write([]byte(connKey))
if err != nil {
log.Printf("发送连接密钥失败: %v", err)
remoteConn.Close()
return
}
// 连接本地服务
localConn, err := net.DialTimeout("tcp", localListenAddr, timeout)
if err != nil {
log.Printf("连接本地服务失败: %v", err)
remoteConn.Close()
return
}
log.Printf("已连接到本地服务: %s", localListenAddr)
// 确保连接关闭
defer func() {
remoteConn.Close()
localConn.Close()
log.Printf("连接已关闭 - 远程: %s, 本地: %s", remoteServerAddr, localListenAddr)
}()
// 创建通道用于同步数据转发协程
done := make(chan struct{}, 2)
// 从远程服务器转发数据到本地服务
go func() {
_, err := io.Copy(localConn, remoteConn)
if err != nil && err != io.EOF {
log.Printf("数据转发错误(远程->本地): %v", err)
}
done <- struct{}{}
}()
// 从本地服务转发数据到远程服务器
go func() {
_, err := io.Copy(remoteConn, localConn)
if err != nil && err != io.EOF {
log.Printf("数据转发错误(本地->远程): %v", err)
}
done <- struct{}{}
}()
// 等待任意一个方向的数据传输结束
<-done
}
🎉 结语
你看,内网穿透听起来高大上,其实原理就是"中间人传话 + 暗号配对"。
这个工具虽然简单,但功能完整,支持命令行参数配置,可以轻松实现从外网访问内网服务的需求。特别是对于需要远程访问内网服务器(如3389远程桌面)的场景,非常实用!
下次你想"穿墙",不用再求人开端口了——
自己部署一个,岂不更爽?😎
代码已开源,拿去使用吧!
(记得修改默认的认证密钥哦~)
📌 友情提示:本文仅供学习交流,请勿用于非法用途。网络安全,人人有责!
往期部分文章列表
- 布隆过滤器(go):一个可能犯错但从不撒谎的内存大师
- 自由通讯的魔法:Go从零实现UDP/P2P 聊天工具
- Go语言实现的简易远程传屏工具:让你的屏幕「飞」起来
- 当你的程序学会了“诈尸“:Go 实现 Windows 进程守护术
- 验证码识别API:告别收费接口,迎接免费午餐
- 用 Go 给 Windows 装个"顺风耳":两分钟写个录音小工具
- 无奈!我用go写了个MySQL服务
- 使用 Go + govcl 实现 Windows 资源管理器快捷方式管理器
- 用 Go 手搓一个 NTP 服务:从"时间混乱"到"精准同步"的奇幻之旅
- 用 Go 手搓一个 Java 构建工具:当 IDE 不在身边时的自救指南
- 深入理解 Windows 全局键盘钩子(Hook):拦截 Win 键的 Go 实现
- 用 Go 语言实现《周易》大衍筮法起卦程序
- Go 语言400行代码实现 INI 配置文件解析器:支持注释、转义与类型推断
- 高性能 Go 语言带 TTL 的内存缓存实现:精确过期、自动刷新、并发安全
- Golang + OpenSSL 实现 TLS 安全通信:从私有 CA 到动态证书加载