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:
对于网络程序开发来说,下面几个依赖项一般都是必要的,你需要将这几个依赖项分别添加到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
模块:
这段代码是服务器与客户端之间通用的逻辑,用于数据传输的。
其中主要有三个部分:
Msg
:定义客户端与服务器之间允许发送的数据结构,也可以称之为协议FrameStream
:封装协议,让我们可以便捷的发送自定义数据proxy
:用于将两个Tcp连接之间的数据进行拷贝复制,完成数据代理。
首先是Msg这个结构,里面有两个枚举值:
#[derive(Debug, Serialize, Deserialize)]
pub enum Msg {
InitPort(u16),
Connect(Uuid),
}
第一个InitPort
值的作用是用来初始化端口的,它携带的就是一个端口值,用于客户端首次连接到服务器时、告诉服务器本客户端想要使用服务器的哪个端口,比如端口P
。
而第二个Connect
则是用来标识一个链接的到来、需要服务器与客户端建立一条新的数据传输链路,它携带的值是一个Uuid,用来唯一标识该链接。
之所以要这样做,是原因服务器使用的端口P很可能同时到来许多条链接,需要互相区分。
比如你随便访问一个网站,同一时间可能就会建立数十条Tcp链接用来传输数据。
该消息值的调用逻辑如下:
graph TD
A[用户]--1.访问-->B[服务器端口P]
B--2.发送Connect消息-->C[客户端]
C--3.收到并发送Connect消息,建立一条新的数据链路-->B
B--4.转发用户链路与数据链路之间的数据-->C
C--5.转发数据链路与本地服务之间的数据-->D[本地web服务]
然后是FrameStream
这个结构,首先为其定义了一个字段:
pub struct FrameStream {
frame: Framed<TcpStream, AnyDelimiterCodec>,
}
这个frame字段的类型是通过下面为其实现的new函数内部得到的:
pub fn new(stream: TcpStream) -> Self {
let codec = AnyDelimiterCodec::new(b"\0".to_vec(), b"\0".to_vec());
let frame = Framed::new(stream, codec);
FrameStream { frame }
}
AnyDelimiterCodec
是tokio_util
包中提供的一个功能函数,用于定义数据帧之间的分隔符,也就是当多个Msg一起发送时,我们应该如何分开两个Msg数据?
毕竟在套接字中,所有数据都是字节流,就和粘连在一起的水一样连续的流出,我们需要对其进行划分。
因此这里使用\0
作为分隔符,因为一般我们自己定义的数据中不太可能出现发送字符\0
的情况,当然你也可以换成别的字符。
然后就可以根据这个分隔符来定义一个帧Framed
结构,这同样也是tokio_util
包中提供的一个功能函数,让我们可以直接从套接字的数据流中根据分隔符直接取出一段数据。
我们需要用它从套接字数据流中直接取出一个Msg
结构,而不是我们自己去读数据、然后一步一步解析,那太麻烦了。
然后在发送的时候,我们就可以直接发送一个帧数据,一个帧在这里就代表一条Msg
,发送的方式是先将要发送的msg
序列化为字符串:
pub async fn send(&mut self, msg: &Msg) {
self.frame
.send(serde_json::to_string(msg).unwrap())
.await
.unwrap();
}
序列化时就用到了serde_json
库,它可以将结构体序列化为JSON
格式的字符串,这也是互联网上目前使用范围最广的一个数据传输格式。
而想要用它对rust结构体进行序列化,一个必要的步骤就是在结构体上添加宏Serialize
与Deserialize
:
#[derive(Debug, Serialize, Deserialize)]
pub enum Msg {
这两个宏是序列化基石serde
库中的。
然后接收的时候,也可以这样进行接收:
pub async fn recv(&mut self) -> Option<Msg> {
if let Some(msg) = self.frame.next().await {
let byte_msg = msg.unwrap();
let msg = serde_json::from_slice(&byte_msg).unwrap();
return Some(msg);
}
None
}
先调用next
函数取出下一帧二进制数据,然后使用from_slice
从该二进制数据中反序列化出我们的Msg
结构传回。
至于最后一个函数,则是用来还原的,从帧流中还原为原本的套接字结构:
pub fn stream(self) -> TcpStream {
self.frame.into_inner()
}