INTERACTIVE LAB

rustVIP1 个可运行示例0 次浏览 更新于 2026-09-28

Rust:内网穿透

使用 Rust 理解内网穿透原理,并实现包含工作空间、无锁优化和错误处理的示例项目。

1.前言

rust是一门非常适合写命令行工具的语言,本文将结合网络基础,带大家完成一个基本的内网穿透工具。

如果你对网络本身还不熟悉的,可以先参考文章: 网络编程

由于rust本身已经把很多网络细节封装好了,所以学习网络编程最好的方式其实是从C/C++入门:C++网络编程详解

有了基本的网络基础之后,我们就可以来开发一个最简单的内网穿透工具,其最终的效果就是,你在本地起一个web服务,远在异地的同学也能直接访问你本地启动的这个web网站。

2.内网穿透原理

众所周知的事实是,由于ipv4地址不够分配,所以当下绝大部分人的设备都处于局域网中,也就是常见的192.168.xxx.xxx这类网段。

它的基本原理是,在一个区域内只有一个顶层设备拥有公共ip地址,该区域内部的所有数据都通过这个公共ip地址收发数据。

由于这个公共ip设备下有许许多多的局域网设备,所以外网是无法直接通过单个公共ip找到局域网内部某台设备的。

一个基本的、身处内网的设备去访问公网web服务的过程如下:

graph LR A[内网设备A]-->E[公网ip设备] B[内网设备B]-->E[公网ip设备] n[内网设备n]-->E[公网ip设备E] E-->F[拥有公网ip的服务F]

只有当内网设备首先向拥有公网ip的设备E发送向外部请求数据的请求时,外部的数据才能进入、并被设备E所识别,并将数据转发给局域网设备。

这就代表着,只要我们能在一个公网ip设备上起一个服务,然后等待内网设备首先向我们发送数据,那么我们就可以向内网设备回复数据了,流程如下:

graph LR A[内网设备A]--1.连接服务F-->E[公网ip设备E] E--2.转发请求给F-->F[拥有公网ip的服务F] F--3.返回数据-->E E--4.转发返回的数据-->A

上面只是一个简单的演示,事实上只要完成了1、2步,那么后续F设备就能任意向内网设备A发送数据了。

而这就是我们实现内网穿透的关键。

有了这里的基础之后,我们就可以将公网ip设备E省略掉了,因为它的作用仅仅只是一个转发而已。

现在假设我们在内网设备A的8888端口上启动了一个web服务,想要将其暴露出去就可以像下面这样做:

graph TD B[内网穿透客户端(安装于内网设备)]--1.连接服务器-->C[内网穿透服务器(安装于公网ip服务器F)]

上面的内网穿透客户端与服务器便是我们将要实现的工具,客户端安装于内网设备,其启动的时候自动连接到服务器,服务器要存放在一个拥有公网ip的设备上,比如云服务器,这可以去腾讯云、阿里云、华为云等服务器厂商租借。

完成了上面这个步骤之后,下面就简单了。

我们想要访问内网暴露的服务,实际上只需要访问设备F上的服务器程序,它将我们的访问请求转发给客户端,最后由客户端转发给本地web服务。

所以内网穿透的基本原理就是:在拥有一台含公共ip的云服务器基础之上,在服务器上安装内网穿透服务器程序,然后让处于内网的设备安装内网穿透客户端并主动的连上来,之后任何发送给服务器的流量就都可以转发给客户端,客户端处于内网之中,可以继续将该流量发送给真正需要暴露的服务。

一个更加具体的例子如下:

graph TD A[内网穿透客户端] --1.请求连接占用服务器的1234端口--> B[内网穿透服务器,ip为1.2.3.4] C[用户] --2.访问公网1.2.3.4:1234-->B B--3.转发流量-->A A--4.转发流量-->D[内网8888端口web服务] D--5.响应请求-->A A--6.转发流量-->B B--7.转发流量-->C

虽然图看起来比较麻烦,但实际上就是中间多了内网穿透客户端与服务器这两步的转发而已。

而我们本文要实现的便是这个转发过程。

3.丐版实现

为了能让大家更好的理解整个流程,本节先来实现一个最简单的丐版,不去考虑任何性能、易用性问题。

完整项目的下载地址为:proxy-rs-simple

首先创建一个目录,名字就叫proxy,然后在该目录中添加两个rust项目,一个为服务器server、一个为客户端client:

image.png

对于网络程序开发来说,下面几个依赖项一般都是必要的,你需要将这几个依赖项分别添加到Cargo.toml这个配置文件中:

tokio = { version = "1.41.0", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

tokio为异步运行基础,serde是数据结构库解析基础,serde_json为json数据格式的解析。

由于客户端与服务器需要互相通信,虽然我们可以自己写通信协议与代码逻辑,但那太麻烦了,所以我们可以直接使用现成的库:

tokio-util = { version = "0.7.12", features = ["codec"] }

这个库是tokio异步官方提供的单元功能库,启用codec特性就能使用其内的数据解析函数。

而该库内部很多函数还需要下面这个异步基础库:

futures = "0.3.31"

最后,还需要一个Uuid库用于生成唯一标识来确认每条链接:

uuid = { version = "1.11.0", features = ["serde", "v4"] }

3.1 share

然后我们就可以来写一个通用的、客户端与服务器之间消息通信的逻辑代码,将其作为一个share模块:

image.png

这段代码是服务器与客户端之间通用的逻辑,用于数据传输的。

下面的 Rust 代码均为 share、server 或 client crate 中的片段。它们依赖 Tokio、Serde、Uuid 等外部库,并且需要多个 crate 通过 TCP 协作,无法在站点单文件沙箱中独立运行;完整工程请按本文的工作空间说明运行。

其中主要有三个部分:

  • Msg:定义客户端与服务器之间允许发送的数据结构,也可以称之为协议
  • FrameStream:封装协议,让我们可以便捷的发送自定义数据
  • proxy:用于将两个Tcp连接之间的数据进行拷贝复制,完成数据代理。

首先是Msg这个结构,里面有两个枚举值:

代码引用nat-protocolsrc/main.rs
第 1–7 行
1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Serialize, Deserialize, PartialEq)]
4enum Msg {
5 InitPort(u16),
6 Connect(u128),
7}
只读片段,不影响运行示例

第一个InitPort值的作用是用来初始化端口的,它携带的就是一个端口值,用于客户端首次连接到服务器时、告诉服务器本客户端想要使用服务器的哪个端口,比如端口P。

而第二个Connect则是用来标识一个链接的到来、需要服务器与客户端建立一条新的数据传输链路,它携带的值是一个Uuid,用来唯一标识该链接。

之所以要这样做,是原因服务器使用的端口P很可能同时到来许多条链接,需要互相区分。

比如你随便访问一个网站,同一时间可能就会建立数十条Tcp链接用来传输数据。

下面用标准库演示同一协议建模思路;它不包含真实 TCP 转发,但可以直接运行:

rust
待运行⌘↵
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize, PartialEq)]
enum Msg {
    InitPort(u16),
    Connect(u128),
}

fn encode(msg: &Msg) -> String {
    serde_json::to_string(msg).unwrap()
}

fn decode(data: &str) -> Msg {
    serde_json::from_str(data).unwrap()
}

fn describe(msg: Msg) -> String {
src/main.rs
该实验为 VIP 专享内容,
请开通后继续阅读后续 70% 并运行完整示例~