26.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_jsonjson数据格式的解析。