Rust Network Tun

RUST Network - Tun

一直想了解加速器的工作原理,看到很多都会提到普通的代理只能提供Tcp的代理,而游戏是走UDP的,一般用Tap设备虚拟网卡和修改路由表的方式来转发游戏的数据到加速服务器

网络协议

开发时经常提到:

  • 二层协议指数据链路层,主要是以太协议,物理链路算是第一层
  • 三层协议就是指网络层,主要是IP协议
  • 四层协议是指传输层,主要是TCP和UDP协议
  • 应用层协议就是一般的应用程序基于TCP或UDP实现的特殊应用功能的协议
层次 作用和协议
Layer 5 应用层application layer 例如HTTPFTPDNS(如BGPRIP这样的路由协议,尽管由于各种各样的原因它们分别运行在TCP和UDP上,仍然可以将它们看作网络层的一部分)
Layer 4 传输层transport layer 例如TCPUDPRTPSCTP(如OSPF这样的路由协议,尽管运行在IP上也可以看作是网络层的一部分)
Layer 3 网络互连层internet layer 对于TCP/IP来说这是因特网协议(IP)(如ICMPIGMP这样的必须协议尽管运行在IP上,也仍然可以看作是网络互连层的一部分;ARP不运行在IP上)
Layer 2 网络链路层Network Access(link) layer 例如以太网Wi-FiMPLS等。

低层协议头包在高层协议外层,例如收到到数据为

1
[链路层以太协议包头][IP包头][TCP包头][应用协议包头][应用数据]

TCP

RFC793 定义了TCP的详细内容

TCP协议头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
TCP Header Format( Note that one tick mark represents one bit position)
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source Port | Destination Port |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Sequence Number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Acknowledgment Number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Data | |U|A|P|R|S|F| |
| Offset| Reserved |R|C|S|S|Y|I| Window |
| | |G|K|H|T|N|N| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Checksum | Urgent Pointer |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Options | Padding |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| data |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

UDP

RFC768定义了UDP协议,很短一份文档

UDP包头

1
2
3
4
5
6
7
8
9
10
11
12

0 7 8 15 16 23 24 31
+--------+--------+--------+--------+
| Source | Destination |
| Port | Port |
+--------+--------+--------+--------+
| | |
| Length | Checksum |
+--------+--------+--------+--------+
|
| data octets ...
+---------------- ...

IP

IP协议分为IPv4 RFC791 和IPv6 RFC8200

IPv4包头为20字节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Version| IHL |Type of Service| Total Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Identification |Flags| Fragment Offset |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Time to Live | Protocol | Header Checksum |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source Address |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Destination Address |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Options | Padding |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

ICMP

RFC 792定义了ICMP

ping命令的协议格式如下

1
2
3
4
5
6
7
8
9
10
Echo or Echo Reply Message
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Type | Code | Checksum |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Identifier | Sequence Number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Data ...
+-+-+-+-+-

Raw Packet编程

Socket

网络应用程序通信时会使用socket来建立主机间的点对点连接,RFC3493 对它有一些扩展描述。一般可以认为socket是在传输层和应用层之间的会话层,因为它建立了两个设备之间会话连接。普通的应用程序使用socket编程时,会设置socket的类型为SOCK_STREAM表示TCP数据传输或SOCK_DGRAM表示UDP数据传输。当然对于抓包应用程序,还可以设置类型为SOCK_RAW,这样获取到数据不会被内核的TCP/IP协议栈处理掉对应的TCP或IP的包头。socket编程学习可以参考https://w3.cs.jmu.edu/kirkpams/OpenCSF/Books/csf/html/Sockets.html

一般应用程序不会使用raw类型的socket,因为原始包中的TCP或IP包头没有被处理,就需要应用程序来处理这些包头,这些不是应用程序关心的协议,所以很少会用SOCK_RAW类型。

如果是为了学习网络协议,特别是底层协议,就需要获取到网卡传给内核的原始数据包。由于应用程序在用户空间无法获取到内核空间的数据,应用程序拿到的网络数据一般(除了SOCK_RAW)都是经过内核的协议栈处理过的TCP或UDP协议上的数据,这些数据的TCP或UDP的包头已经被内核处理掉了,应用直接拿到的就是数据而不包括协议头。

虚拟网络设备

linux类的系统中提供了Tap/Tun虚拟网卡设备,它可以在用户空间接收和传输原始数据包,可以看作是一个简单的从物理介质上收发数据的点对点或以太设备。

tun_network
tun_network

使用虚拟网卡的基本步骤:

  1. 创建虚拟网卡设备,一般网卡名称为Tap0或Tun0
  2. 给虚拟网卡配置ip地址,掩码,网关信息,可能还需要路由信息,让指定ip的访问都通过这个网卡传输
  3. 网络应用程序中打开这个虚拟网卡,得到对应的设备描述符,通过描述符读写数据
  4. 例如主机A的浏览器需要从服务器B下载文件,但是主机A不能直接访问到服务器B,通过配置路由表,让对服务器B的访问都通过虚拟网卡Tun0传输,此时浏览器像B地址的请求,内核会发送给虚拟网卡Tun0
  5. 网络应用程序收到内核给Tun0发来的IP数据包,并将IP数据包数据包加密压缩处理后发送给代理服务器P
  6. 代理服务器P收到数据包,解压解密后,向服务器B发送请求,并得到B的应答
  7. 代理服务器P将服务器B的应答压缩加密后,发送回网络应用程序
  8. 网络应用程序通过Tun0网卡把解压和解密后数据发送给浏览器

整个过程中内核会把tun0当作真实的物理网卡

Tap和Tun区别

Tap工作在2层网络,它的数据包从以太帧开始

Tun工作在3层网络,它的数据包从IP包开始

因此,如果想要自己实现TCP或UDP协议,使用tun就足够了,如果想实现ARP协议,需要Tap设备,参看编写网络协议栈之Ethernet & ARP Protocol

wintun

linux内核默认支持了tun/tap虚拟网卡,windows可以通过wintun来创建tun网卡。

wintun是WireGuard软件中使用的为windows内核实现的tun虚拟网卡设备,使用方法和linux的tun相同。

rust使用wintun

crate wintun 是对wintun动态库的rust封装,项目中有使用这个crate的例子程序

1
2
[dependencies]
wintun = "0.4.0"

ICMP by Rust

ICMP虽然和IP在同一层,但是它也是由IP包头里面打包的。ping命令就是ICMP的一个重要功能。

[IP Header][ICMP Header][ICMP Data]

通过使用socket的SOCK_RAW类型也可以实现ping命令,参看Linux下实现ping程序

为了学习tun和rust参考Implementing ICMP in Ruststudy-udp 来实现ICMP的ping命令应答。

下图为ping -4 www.baidu.com执行后的数据包,可以看到IP包包头20字节,ICMP的 Echo包共40字节

icmp_packet
icmp_packet

工程依赖使用wintun和etherparse,后者用来解析ip包

1
2
3
[dependencies]
wintun = "0.4.0"
etherparse = "0.13.0"

下载wintun的压缩包,解压后wintun目录放在项目的根目录中。程序运行后,执行ping 172.250.68.100就可以看到收到的数据包和应答。如果ping虚拟网卡自己的ip则不会收到包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
use std::sync::{atomic::{AtomicBool, Ordering}, Arc};
// 根据平台获取dll位置
pub fn get_wintun_bin_relative_path() -> Result<std::path::PathBuf, Box<dyn std::error::Error>> {
let dll_path = if cfg!(target_arch = "x86") {
"wintun/bin/x86/wintun.dll"
} else if cfg!(target_arch = "x86_64") {
"wintun/bin/amd64/wintun.dll"
} else if cfg!(target_arch = "arm") {
"wintun/bin/arm/wintun.dll"
} else if cfg!(target_arch = "aarch64") {
"wintun/bin/arm64/wintun.dll"
} else {
return Err("Unsupported architecture".into());
};
Ok(dll_path.into())
}

// 初始化Tun网适配器
fn init_tun_nic() -> Arc<wintun::Adapter> {
let dll_path = get_wintun_bin_relative_path().unwrap();
let wintun = unsafe { wintun::load_from_path(dll_path).expect("load dll failed") };
// 打开虚拟网卡
let adapter = match wintun::Adapter::open(&wintun, "NetProto") {
Ok(a) => a,
Err(_) => wintun::Adapter::create(&wintun, "NetProto", "Work", None).expect("Create tun adapter failed"),
};

let version = wintun::get_running_driver_version(&wintun).unwrap();
println!("Using wintun version: {:?}", version);

// set the address for the tun nic
let index = adapter.get_adapter_index().unwrap();
let set_metric = format!("netsh interface ip set interface {} metric=255", index);
let set_gateway = format!(
"netsh interface ip set address {} static 172.250.68.50/24 gateway=172.250.68.1", index);
println!("{}", set_gateway);

// 添加路由表,让172.250.68.50/24子网下的流量都走172.250.68.1虚拟网卡
let set_route = format!("netsh interface ip add route 172.250.68.50/24 {} 172.250.68.1", index);

// execute the command
std::process::Command::new("cmd")
.arg("/C")
.arg(set_metric)
.output()
.unwrap();
std::process::Command::new("cmd")
.arg("/C")
.arg(set_gateway)
.output()
.unwrap();
// 执行添加路由命令
std::process::Command::new("cmd")
.arg("/C")
.arg(set_route)
.output()
.unwrap();

adapter
}

// 计算校验和
fn calculate_checksum(data: &mut [u8]) {
let mut f = 0;
let mut chk: u32 = 0;
while f + 2 <= data.len() {
chk += u16::from_le_bytes(data[f..f+2].try_into().unwrap()) as u32;
f += 2;
}
//chk &= 0xffffffff; // unneccesary
while chk > 0xffff {
chk = (chk & 0xffff) + (chk >> 2*8);
}
let mut chk = chk as u16;
chk = !chk & 0xffff;
// endianness
//chk = chk >> 8 | ((chk & 0xff) << 8);
data[3] = (chk >> 8) as u8;
data[2] = (chk & 0xff) as u8;
}

const ICMP_ECHO_REQUEST : u8 = 8;
const ICMP_ECHO_REPLY : u8 = 0;

// ICMP数据包
pub struct ICMPPacket <'a> {
ip: etherparse::Ipv4Header,
icmp_id: u16,
seq_no: u16,
data: &'a [u8],
}

impl<'a> ICMPPacket <'a> {
pub fn start(iph: etherparse::Ipv4HeaderSlice, data: &'a [u8]) -> std::io::Result<Option<Self>> {
let mut packet = ICMPPacket {
ip: etherparse::Ipv4Header::new(
0,
64,
etherparse::IpNumber::Icmp as u8,
[ // 应答的源和目的地址要对调
iph.destination()[0],
iph.destination()[1],
iph.destination()[2],
iph.destination()[3],
],
[
iph.source()[0],
iph.source()[1],
iph.source()[2],
iph.source()[3],
],
),
icmp_id: u16::from_be_bytes(data[4..6].try_into().unwrap()),
seq_no: u16::from_be_bytes(data[6..8].try_into().unwrap()),
data: data,
};
Ok(Some(packet))
}

pub fn build_response(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
use std::io::Write;
// IP header
self.ip.set_payload_len(self.data.len());
let mut unwritten = &mut buf[..];
self.ip.write(&mut unwritten);
// 实际测试,IP头20字节,ICMP头8字节,数据32字节,共40字节
let mut icmp_reply = [0u8; 40];
icmp_reply[0] = ICMP_ECHO_REPLY; // type
icmp_reply[1] = 0; // code - always 0?

icmp_reply[2] = 0x00; // checksum = 2 & 3, empty for now
icmp_reply[3] = 0x00; //
icmp_reply[4] = ((self.icmp_id >> 8) & 0xff) as u8; // id = 4 & 5
icmp_reply[5] = (self.icmp_id & 0xff) as u8;
icmp_reply[6] = ((self.seq_no >> 8) & 0xff) as u8; // seq_no = 6 & 7
icmp_reply[7] = (self.seq_no & 0xff) as u8;
icmp_reply[8..self.data.len()].clone_from_slice(&self.data[8..]);

// finally we substitute the checksum
calculate_checksum(&mut icmp_reply);
unwritten.write(&icmp_reply);
Ok(unwritten.len())
}
}

static RUNNING: AtomicBool = AtomicBool::new(true);

fn main_loop(adapter: Arc<wintun::Adapter>) {
let session = Arc::new(adapter.start_session(wintun::MAX_RING_CAPACITY).expect("new session failed"));

let reader_session = session.clone();
let writer_session = session.clone();

let reader = std::thread::spawn(move || {
while RUNNING.load(Ordering::Relaxed) {
let packet = reader_session.receive_blocking();
if let Err(err) = packet {
println!("Error reading packet: {:?}", err);
break;
}
let packet = packet?;
let bytes = packet.bytes();
let len = bytes.len();
match etherparse::Ipv4HeaderSlice::from_slice(&bytes[..len]) {
Ok(iph) => {
let src = iph.source_addr();
let dst = iph.destination_addr();
let proto = iph.protocol();
// 只处理ICMP
if proto != etherparse::IpNumber::Icmp as u8 {
continue;
}
println!("Read packet size {} bytes. Source: {:?}, Destination: {:?}, Protocol: {:?}", len, src, dst, proto);
let data = &bytes[0..];
let hex_string = data.iter().map(|byte| format!("{:02x}", byte)).collect::<Vec<String>>().join(" ");
println!("Read packet size {} bytes. Header data: {:?}", len, hex_string);
//Read packet size 60 bytes. Header data: "45 00 00 3c b3 be 00 00 80 01 a4 77 ac fa 44 32 ac fa 44 64 08 00 4b 4d 00 01 02 0e 61 62 63 64 65 66 67 68 69 6a 6b 6c 6d 6e 6f 70 71 72 73 74
//75 76 77 61 62 63 64 65 66 67 68 69"
let iph_len = iph.slice().len() as u16;
println!("ip header len: {}", iph_len); //ip header len: 20
let data_buf = &bytes[iph.slice().len()..len];

// 应答数据
if let Some(mut packet) = ICMPPacket::start(
iph,
data_buf,// ping要求原包应答
).unwrap() {
let resp_len = iph_len + data_buf.len() as u16;
let mut write_pack = writer_session.allocate_send_packet(resp_len).unwrap();
let mut buf = write_pack.bytes_mut();
packet.build_response(&mut buf).unwrap();
writer_session.send_packet(write_pack);
println!("responded to type# {} packet from {} data len {}", proto, src, resp_len);
}
}
Err(e) => {
// 其他网络包 ignoring weird packet Ipv4UnexpectedVersion(6)
//eprintln!("ignoring weird packet {:?}", e);
}
}
}
Ok::<(), wintun::Error>(())
});

println!("Press enter to stop session");
let mut line = String::new();
let _ = std::io::stdin().read_line(&mut line);
println!("Shutting down session");

RUNNING.store(false, Ordering::Relaxed);
session.shutdown().unwrap();
let _ = reader.join().map_err(|err| wintun::Error::from(format!("{:?}", err))).unwrap();

println!("Shutdown complete");
}

fn main() {
let adapter = init_tun_nic();
main_loop(adapter);
}
0%