穿墙术大揭秘:用 Go 手搓一个“内网穿透“神器!

93 阅读10分钟

你有没有过这样的烦恼?

公司电脑开着远程桌面,回家想连却连不上;

家里的 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 拿到暗号后,干两件事:

  1. 连上内网真实服务(比如 192.168.2.28:3389)
  2. 主动连接 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远程桌面)的场景,非常实用!

下次你想"穿墙",不用再求人开端口了——

自己部署一个,岂不更爽?😎

代码已开源,拿去使用吧!

(记得修改默认的认证密钥哦~)

📌 友情提示:本文仅供学习交流,请勿用于非法用途。网络安全,人人有责!

往期部分文章列表