tuntap
由于linux内核控制了网络接口,所以应用层不能直接使用网络接口来处理网络包。linux通过提供tuntap虚拟网络接口的机制,让用户可以在应用层处理原始的网络包。
tun使用示例
tuntap可以创建两种虚拟网络接口:tun和tap。tap是二层网络接口,提供mac帧。tun是三层网络接口,提供ip包。
我们处理tcp,ip协议,只需要使用tun接口,如果要处理arp,icmp协议则需要使用tap接口。这里只演示tun接口的使用。
test tun
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
|
func Test_tun(t *testing.T) {
args := struct {
cidr string
name string
}{
cidr: "11.0.0.1/24",
name: "testtun1",
}
fd, err := CreateTunTap(args.name, syscall.IFF_TUN|syscall.IFF_NO_PI)
if err != nil {
log.Fatalln(err)
}
out, err := exec.Command("ip", "addr", "add", args.cidr, "dev", args.name).CombinedOutput()
if err != nil {
log.Fatalln(err)
}
fmt.Println(out)
out, err = exec.Command("ip", "link", "set", args.name, "up").CombinedOutput()
if err != nil {
log.Fatalln(err)
}
fmt.Println(out)
buf := make([]byte, 1024)
for {
n, err := syscall.Read(fd, buf)
if err != nil {
log.Fatalln(err)
}
fmt.Println(hex.Dump(buf[:n]))
}
}
|
使用curl发送一个简单的请求测试一下
1
|
curl -v http://11.0.0.2/hello
|
将会得到类似下面的输出,这就是一个原始的ip包了
1
2
3
4
|
00000000 45 00 00 3c 80 40 40 00 40 06 a4 79 0b 00 00 01 |E..<.@@.@..y....|
00000010 0b 00 00 02 bb f8 00 50 08 a8 4a 04 00 00 00 00 |.......P..J.....|
00000020 a0 02 fa f0 67 67 00 00 02 04 05 b4 04 02 08 0a |....gg..........|
00000030 bf b6 00 fa 00 00 00 00 01 03 03 07 |............|
|
解析ip包
直接看rfc791的对ip包的格式定义
rfc791#section-3.1
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 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
对照着rfc可以来解析如下这个包
1
2
3
4
|
00000000 45 00 00 3c 80 40 40 00 40 06 a4 79 0b 00 00 01 |E..<.@@.@..y....|
00000010 0b 00 00 02 bb f8 00 50 08 a8 4a 04 00 00 00 00 |.......P..J.....|
00000020 a0 02 fa f0 67 67 00 00 02 04 05 b4 04 02 08 0a |....gg..........|
00000030 bf b6 00 fa 00 00 00 00 01 03 03 07 |............|
|
结果如下
IP 偏移量 |
TCP 偏移量 |
字节值 |
描述 |
4/8 |
|
0x4 |
IP 版本:IPv4 |
1 |
|
0x5 |
IP 首部长度为 5个32位数字:5 * 4 = 20 字节 |
2 |
|
0x00 |
服务类型 |
4 |
|
0x003c |
总长度为 60 字节 |
6 |
|
0x8040 |
IP 标识 |
6 + 3/8 |
|
010 |
标志位:0: 保留位;必须为 0,1: 禁止分片 (DF) 0: 还有更多分片 (MF) |
8 |
|
0 0000 0000 0000 |
分片偏移量:此处为 0 |
9 |
|
0x40 |
生存时间:64 秒 |
10 |
|
0x06 |
协议:0x06 表示 TCP |
12 |
|
0xa479 |
首部校验和 |
16 |
|
0x0b 00 00 01 |
源 IP 地址:11.0.0.1 |
20 |
|
0x0b 00 00 02 |
目的 IP 地址:11.0.0.2 |
解析的代码如下
ip.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
func (i *IPPack) Decode(data []byte) (*IPPack, error) {
header := &IPHeader{
Version: data[0] >> 4,
HeaderLength: (data[0] & 0x0f) * 4,
TypeOfService: data[1],
TotalLength: binary.BigEndian.Uint16(data[2:4]),
Identification: binary.BigEndian.Uint16(data[4:6]),
Flags: data[6] >> 5,
FragmentOffset: binary.BigEndian.Uint16(data[6:8]) & 0x1fff,
TimeToLive: data[8],
Protocol: data[9],
HeaderChecksum: binary.BigEndian.Uint16(data[10:12]),
SrcIP: net.IP(data[12:16]),
DstIP: net.IP(data[16:20]),
}
header.Options = data[20:header.HeaderLength]
i.IPHeader = header
payload, err := i.Payload.Decode(data[header.HeaderLength:])
if err != nil {
return nil, err
}
i.Payload = payload
return i, nil
}
|
需要注意的有以下几点
网络字节序
网络字节序都是大端的。大端和小端有些时候容易搞混,从网络包解析的场景来说就是解析包时一个数据的高位字节排在前面。
例如0x1234
,大端表示为0x1234
,小端表示为0x3412
。可以发现大端表示法和我们日常书写的顺序一致。
golang中代码实现上也很简单。
1
2
3
4
5
6
7
8
9
|
func (bigEndian) Uint16(b []byte) uint16 {
_ = b[1] // bounds check hint to compiler; see golang.org/issue/14808
return uint16(b[1]) | uint16(b[0])<<8
}
func (littleEndian) Uint16(b []byte) uint16 {
_ = b[1] // bounds check hint to compiler; see golang.org/issue/14808
return uint16(b[0]) | uint16(b[1])<<8
}
|
ip头长度
ip包头的长度单位是32位数字,所以需要乘以4才是字节数。rfc原话是
IHL: 4 bits
Internet Header Length is the length of the internet header in 32
bit words, and thus points to the beginning of the data. Note that
the minimum value for a correct header is 5.
解析tcp包
rfc9293#name-header-format
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
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 | |C|E|U|A|P|R|S|F| |
| Offset| Rsrvd |W|C|R|C|S|S|Y|I| Window |
| | |R|E|G|K|H|T|N|N| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Checksum | Urgent Pointer |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| [Options] |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| :
: Data :
: |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
对照着rfc可以来解析如下这个包
1
2
3
4
|
00000000 45 00 00 3c 80 40 40 00 40 06 a4 79 0b 00 00 01 |E..<.@@.@..y....|
00000010 0b 00 00 02 bb f8 00 50 08 a8 4a 04 00 00 00 00 |.......P..J.....|
00000020 a0 02 fa f0 67 67 00 00 02 04 05 b4 04 02 08 0a |....gg..........|
00000030 bf b6 00 fa 00 00 00 00 01 03 03 07 |............|
|
结果如下
IP 偏移量 |
TCP 偏移量 |
字节值 |
描述 |
22 |
2 |
0xbbf8 |
源端口:48120 |
24 |
4 |
0x0050 |
目的端口:80 |
28 |
8 |
0x08a84a04 |
序列号:145246724 |
32 |
12 |
0x00000000 |
确认号:0 |
33 + 4/8 |
13 + 4/8 |
0xa |
首部长度为10个32位数字:10 * 4 = 40 字节 |
33 + 10/8 |
13 + 10/8 |
0000 00 |
保留位 |
34 |
14 |
00 0010 |
标志位 URG:0 ACK:0 PSH:0 RST:0 SYN:1 FIN:0,所以是syn包 |
36 |
16 |
0xfaf0 |
窗口大小:64240 |
38 |
18 |
0x6767 |
校验和 |
40 |
20 |
0x0000 |
紧急指针 |
60 |
40 |
|
TCP 选项和填充 |
解析的代码如下
tcp.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
func (t *TcpPack) Decode(data []byte) (NetworkPacket, error) {
header := &TcpHeader{
SrcPort: binary.BigEndian.Uint16(data[0:2]),
DstPort: binary.BigEndian.Uint16(data[2:4]),
SequenceNumber: binary.BigEndian.Uint32(data[4:8]),
AckNumber: binary.BigEndian.Uint32(data[8:12]),
DataOffset: (data[12] >> 4) * 4,
Reserved: data[12] & 0x0F,
Flags: data[13],
WindowSize: binary.BigEndian.Uint16(data[14:16]),
Checksum: binary.BigEndian.Uint16(data[16:18]),
UrgentPointer: binary.BigEndian.Uint16(data[18:20]),
}
header.Options = data[20:header.DataOffset]
t.TcpHeader = header
payload, err := t.Payload.Decode(data[header.DataOffset:])
if err != nil {
return nil, err
}
t.Payload = payload
return t, nil
}
|
有了解析ip包的经验后解析tcp包就简单了,需要注意的点和解析ip包时类似,就不做赘述了。
总结
本次我们学习了tuntap中的tun的使用方法,并使用tun接口解析了ip包和tcp包,这是我们自己实现tcp/ip协议栈的第一步。
文章中的代码在这里查看。