首發於github page 自己動手編寫tcp/ip協議棧1:tcp包解析
tuntap
由於linux內核控制了網絡接口,所以應用層不能直接使用網絡接口來處理網絡包。linux通過提供tuntap虛擬網絡接口的機制,讓用户可以在應用層處理原始的網絡包。
tun使用示例
tuntap可以創建兩種虛擬網絡接口:tun和tap。tap是二層網絡接口,提供mac幀。tun是三層網絡接口,提供ip包。
我們處理tcp,ip協議,只需要使用tun接口,如果要處理arp,icmp協議則需要使用tap接口。這裏只演示tun接口的使用。
test tun
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發送一個簡單的請求測試一下
curl -v http://11.0.0.2/hello
將會得到類似下面的輸出,這就是一個原始的ip包了
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
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可以來解析如下這個包
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
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中代碼實現上也很簡單。
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
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可以來解析如下這個包
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
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協議棧的第一步。
文章中的代碼在這裏查看。