前言

说起来开发自定义网络协议是一件很有趣的事情,就像在宿舍时候给家里打电话,不会讲方言的室友用的就是基于IP协议的TCP协议的HTTP明文传输,会讲方言的室友就使用了自定义的传输协议,就算在公开的网络环境聊天中也没人知道说的是什么。那么会讲方言的时候到底在哪一层做了自定义呢,这是通常是网络开发面临的第一个问题。所以在下就先贴出伟大的TCP/IP协议族定义下网络五层模型,及其代表性协议举例:

本文以下部分将就以上模型基于Go语言对自定义协议进行探究。
实验环境:
发送方:MacOS 10.14
接收方:Ubuntu 16.04
网络拓扑:本文会从以下两种实验环境进行:

  1. 虚拟机模拟下的内网环境,非常简单的点到点传输,环境单纯可靠,可以胡搞因为数据包的传递都是在自己的机器上,非常可控。拓扑如下图所示:
  2. 实际网络环境,表面看上去一片和谐的互联网环境其实非常严苛,就像灿烂星空但是其实冷寂到了极点。自定义协议的传输要经过不可控的网关、骨干网传输,简直就是一场绿野仙踪。拓扑如下图所示:

自定义应用层协议

欢迎来到Level 0,因为在可靠的传输层协助下去开发一个应用层协议是非常简单的事情,就像寄快递一样,只要把东西送到菜鸟裹裹就可以了,不用去思考快递公司到底是走国道还是走省道。应用层上最常用的用户协议大概就是HTTP了,教科书上用一个章节说不完TCP,却能用几页讲清楚HTTP,或者用几个字来说就是请求-响应。我们就结合TCP模仿HTTP做一个基于发起连接-确认连接-请求-响应的具有三次握手的应用层协议。

协议设计

首先用规范化的语言描述一下这个协议,但是不去设计太多麻烦的细节,大体就是这样:

注意以下步骤的前提是传输层的支持,传输层,就选TCP,百年质保,用了都说好。

  1. 发送方向接收方发送“ALOHA”,表示着有客人来了。
  2. 接收方回复“YO”,表示我穿好裤子了,你可以进来了。
  3. 发送方发送自己的请求“REQUEST1”,表示请求的内容是1。
  4. 接收方把1的响应返回给发送方,说“RESPONSE1”。

代码实现

先上接收方(服务端)代码,至于拓扑要选择1还是2关系不大。

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
import (
"net"
"fmt"
)

func main() {
// 在9999端口监听TCP连接
listener, error := net.Listen("tcp4", ":9999")
if error != nil {
fmt.Println("开启监听错误" + error.Error())
return
}
defer listener.Close()

// 开始接收TCP连接,无限循环
for {
connection, error := listener.Accept()
if error != nil {
fmt.Println("建立TCP连接错误" + error.Error())
continue
}

// 和客户端建立TCP连接成功
go handleClient(connection)
}
}

func handleClient(client net.Conn) {
defer client.Close()
// 传输层的连接已经建立成功,接下来就是应用层的事了
// 创建接收缓冲区
buf := make([]byte, 1024)

// 接收客户端数据(第一步)
readLength, error := client.Read(buf)
if error != nil || readLength <= 0 {
fmt.Println("从客户端接收数据错误" + error.Error())
return
}
message := string(buf[:readLength])
if message != "ALOHA" {
fmt.Println("接收到不可识别的数据")
return
}

// 客户端完成了协议第一步,接下来服务端进行协议第二步
writeLength, error := client.Write([]byte("YO"))
if error != nil || writeLength <= 0 {
fmt.Println("向客户端发送数据错误" + error.Error())
return
}

// 接收客户端请求(第三步)
readLength, error = client.Read(buf)
if error != nil || readLength <= 0 {
fmt.Println("从客户端接收数据错误" + error.Error())
return
}
request := string(buf[:readLength])
var response string
switch request {
case "REQUEST1":
response = "RESPONSE1"
case "REQUEST2":
response = "RESPONSE2"
default:
response = "UNKNOWN REQUEST"
}
writeLength, error = client.Write([]byte(response))
if error != nil || writeLength <= 0 {
fmt.Println("向客户端发送响应错误" + error.Error())
return
}

// 协议完成,关闭连接
}

然后是发送方(客户端)代码,反正注释很详细又不难就不多说了。

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
import (
"net"
"fmt"
)

func main() {
// 接收方的IP和接收端口
server := "148.70.160.43:9999"

// 连接接收方9999端口
connection, error := net.Dial("tcp4", server)
if error != nil {
fmt.Println("连接服务器错误" + error.Error())
return
}
defer connection.Close()

// 连接成功,开始应用层协议
buf := make([]byte, 1024)
// 发送ALOHA(第一步)
writeLength, error := connection.Write([]byte("ALOHA"))
if error != nil || writeLength <= 0 {
fmt.Println("发送ALOHA错误" + error.Error())
return
}

// 接收服务端连接响应YO(第二步)
readLength, error := connection.Read(buf)
message := string(buf[:readLength])
if error != nil || readLength <= 0 || message != "YO" {
fmt.Println("接收YO错误" + error.Error())
return
}

// 发送请求(第三步)
writeLength, error = connection.Write([]byte("REQUEST1"))
if error != nil || writeLength <= 0 {
fmt.Println("发送请求错误" + error.Error())
return
}

// 接收响应(第四步)
readLength, error = connection.Read(buf)
if error != nil || readLength <= 0 {
fmt.Println("接收响应错误" + error.Error())
return
}
response := string(buf[:readLength])
fmt.Println("成功接收响应:" + response)
}

运行效果

发送方运行之后打印得到如下内容:

1
成功接收响应:RESPONSE1

自定义传输层协议

上面的自定义应用层协议有点简单,基本就是煮泡面顶多加个蛋的厨艺,接下来才是展示真正技术的时刻:自定义传输层协议。在应用层做文章和在传输层做文章的主要差别在于,应用层是基于传输层进行开发,而传输层是基于网络层进行开发。因此应用层可以直接使用传输层提供的方便简单的socket进行数据收发。因看似使用简单的socket,底下是操作系统网络内核做了成吨的工作,而在进行传输层开发时便不能再依赖传输层的socket了,因为我们要实现的就是传输层socket,因而这成吨的工作落到了我们肩膀上(不过本文只是提供一个技术实现的入门指引)。
还有一方面就是操作系统认为既然完成了传输层这成吨的工作,而你不用它的工作成果就不对!所以这也是为什么我们基于传输层socket做应用层协议开发那么容易,而要做传输层开发就一步一个坑了,举个例子来说就是操作系统默认承接所有(传输层及以下)数据包的收发处理工作,就像个一手遮天的家长,并没有向用户提供便捷的数据包收发API;当用户收发自定义数据包时,操作系统还有出来捣乱,给你乱发RST之类的。
除了在家里,操作系统是个家长;在外面,什么路由器啊交换机啊还有骨干网的大佬们也很负责地对底层数据包进行全权管理,这也就是为什么之前说到的要分拓扑环境来进行了。
综上所述,接下来要使用RAW SOCKET来进行自定义传输层协议构建,这个RAW的意思就是底层,比传输层提供的socket要底层,传输层socket只能用来构建应用层协议,这个RAW SOCKET才可以用来构建传输层协议,甚至网络层协议。

RAW SOCKET

先抛开拓扑差异,聊一聊基本的技术工具——RAW SOCKET。在发送方,使用RAW SOCKET构建自定义的网络层、传输层报文然后发送给网卡;在接收方,使用RAW SOCKET进行网络层、传输层报文,然后进行解析。在写传输层RAW SOCKET时需要使用golang.org/x/net/ipv4这个包,用来处理IP分组,比如把接收到的byte解析成IP头部等。
下文需要用到的_net_包和_ipv4_包中的一些方法可以直接参阅文档文档

基于虚拟内网拓扑的自定义传输层协议

具体拓扑长什么就上面找图,基本就是发送方和接收方直连。

协议设计

先贴一个IP报头结构如下:

_ipv4_包会做好报头解析工作,所以在这里我们不用做网络层的报头解析,但是报头中有一个字段是我们需要定义的,就是协议字段,标识的使用的上层协议,例如TCP协议是6而UDP协议是17。而本文的自定义传输协议将使用一个没有人用过的编号,就233好了。
网络层的核心是IP地址,而传输层的核心是端口,于是233号协议就简单设计如下设计:

大概是最精简的传输层协议设计了,端口还是必须的,映射进程嘛。至于校验和啦,序列号啦什么的东西,就不赘述了不然可以写书了。

代码实现

接收方的代码如下,不解释了哇码字好累,直接看注释吧:

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
import (
"net"
"fmt"
"golang.org/x/net/ipv4"
)

func main() {
// 用于进行数据包的网卡的IP,一般是en0网卡吧
serverIP := "xx.xx.xx.xx"
// 开启基于IP的传输层233协议报文监听
listener, error := net.ListenPacket("ip4:233", serverIP)
if error != nil {
fmt.Println("开启监听错误" + error.Error())
return
}
defer listener.Close()

// 把监听得到的报文传输到ipv的Conn,用来解析IP报文
connection, error := ipv4.NewRawConn(listener)
if error != nil {
fmt.Println("创建Connection错误" + error.Error())
return
}
defer connection.Close()

// 开启无线循环获取报文
for {
buf := make([]byte, 2000)
// 获取报文同时解析IP报文,得到IP头和IP头后面部分(传输层和应用层)
ipHeader, payload, _, error := connection.ReadFrom(buf)
if error != nil {
fmt.Println("解析IP头错误" + error.Error())
return
}

// 打印IP头
fmt.Println(ipHeader)
// 打印IP头后面的部分
fmt.Println(payload)
}
}

然后发送方的代码如下:

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
import (
"net"
"fmt"
"syscall"
"golang.org/x/net/ipv4"
)

func main() {
// 本地地址和目的地址,目前拓扑来看应该是同一个子网里面
source, _ := net.ResolveIPAddr("ip4", "xx.xx.xx.xx")
destination, _ := net.ResolveIPAddr("ip4", "xx.xx.xx.xx")

// 创建raw socket
fd, error := syscall.Socket(syscall.AF_INET, syscall.SOCK_RAW, syscall.IPPROTO_RAW)
if error != nil {
fmt.Print("创建raw socket错误" + error.Error())
return
}
defer syscall.Close(fd)

// 开启IP_HDRINCL
error = syscall.SetsockoptInt(fd, syscall.IPPROTO_IP, syscall.IP_HDRINCL, 1)
if error != nil {
fmt.Println("开启IP_HDRINCL错误" + error.Error())
return
}

// 应用层数据
application := []byte("233")

// 自定义传输层协议头
transmission := []byte{
uint8(12345 >> 8), uint8(12345 % 256), // 源端口
uint8(54321 >> 8), uint8(54321 % 256), // 目的端口
}

// IP头,重要的是协议字段写233号协议
ipHeader := ipv4.Header{
Version: 4,
Len: 20,
TotalLen: len(application) + len(transmission) + 20,
TTL: 64,
Protocol: 233,
Dst: destination.IP.To4(),
Src: source.IP.To4(),
// Flags: ipv4.DontFragment,
}
// 生成字节串
ip, _ := ipHeader.Marshal()

// 组装报文
payload := append(ip, transmission...)
payload = append(payload, application...)

// 通过raw socket发送报文
destinationAddress := syscall.SockaddrInet4{
Port: 0,
Addr: [4]byte{
destination.IP.To4()[0],
destination.IP.To4()[1],
destination.IP.To4()[2],
destination.IP.To4()[3],
},
}
error = syscall.Sendto(fd, payload, 0, &destinationAddress)
if error != nil {
fmt.Println("发送错误" + error.Error())
return
}

fmt.Println("发送成功")
}

运行结果

因为使用了RAW SOCKET,所以无论发送方还是接收方都需要sudo来运行,下面是接收方的运行结果:

1
2
ver=4 hdrlen=20 tos=0x0 totallen=27 id=0x1f5d flags=0x2 fragoff=0x0 ttl=64 proto=233 cksum=0x96f2 src=10.211.55.2 dst=10.211.55.3
[48 57 212 49 50 51 51]

可见_ipv4_包已经把IP头处理好了,当然后面的传输层和应用层数据没法解析,但是能看出传输层的4个字节之后是应用层的“233”。

基于真实互联网拓扑的自定义传输层环境

如果基于上面的代码,把源IP和目的IP修改一下,源地址是你局域网中的本机,而目的地址是远在天边的VPS,运行之后极大概率会发现什么都没发生。这时候就需要感慨一下,看上去互联网环境一片生机勃勃,其实杀机暗涌啊!
个人认为一个主要的原因就是NAT。众所周知NAT的一大功能就是做地址转换,但其实这里面大有文章,包括但不仅限于如下内容:

  • IP地址转换,这是对IP报头的修改。
  • 端口转换,这是对TCP/UDP报头的修改。
  • 对于没有端口的协议怎么处理?(比如ICMP,一般来说用session来对应IP连接)
  • 对于未知协议怎么处理?(比如233号协议,可能是使用session,也可能直接丢弃)
  • 对IP地址、端口修改之后重新计算校验和。
    至于NAT到底是怎么处理233号协议的,基于上面的什么也没有发生,因此很可能233号协议直接被NAT丢弃了,由于发送方到接收方经历了多层NAT(接收方虽然有公网地址,但也是处于NAT下),因此很难说是被哪一层NAT丢弃了。这么说自定义传输层协议的思路受到了挑战,但是还是有办法的,在一定妥协的基础上,虽然不能使用233号协议,但仍然可以使用一些标准协议如UDP或TCP协议进行魔改

协议设计

本文基于UDP协议进行自定义传输层协议设计,因为UDP协议比较容易实现,而且我们知道NAT会对UDP报头做什么样的修改——首先修改端口,然后重新计算校验和。原始的UDP长这样:

UDP是一种无连接不可靠的传输层协议,不过随着互联网设施的发展,UDP的不可靠性也逐渐不那么突出,相反其常用于实时传输如直播等。而TCP协议倒是其超可靠的性质变得臃肿,因此本文自定义传输层协议的目的在于在UDP的基础上增加一定的可靠传输,通过简单增加报文序号的方式,如下所示:

在UDP的基础上添加了报文序号和ACK序号。其中报文序号用于标识当前报文在会话中的序号;而ACK序号和TCP中的ACK用途相同,用于接收方向发送方确认报文接收,否则重传(这些具体操作本文就不写了,之展示基本的代码)。

代码实现

接收方代码其实和之前用的差不多,需要修改的地方有两个:

  1. 监听的传输层报文的代码需要修改,之前是监听233号协议,需要改成监听UDP协议:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 开启基于IP的传输层UDP协议报文监听
    listener, error := net.ListenPacket("ip4:udp", "xx.xx.xx.xx")
    if error != nil {
    fmt.Println("开启监听错误" + error.Error())
    return
    }

    // 开启ipv4的Conn
    ...
  2. 在每次收到数据包之后,除了解析IP报头外,添加自定义传输层协议头的代码。由于代码比较简单,就是把byte解析数字等的记流水帐代码。

发送方代码也基本差不多,只要修改创建传输层报头和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
// 开启RAW SOCKET
...
// 开启IP)HDRINCL
...

// 应用层数据
application := []byte("2333")

// 自定义传输层协议
sourcePort := rand.Uint32()
transmission := []byte{
uint8(sourcePort >> 8), uint8(sourcePort % 256), // 源端口
uint8(54321 >> 8), uint8(54321 % 256), // 目的端口
uint8(0), uint8(len(application) + 12), // 长度(应用层数据长度+传输层报头长度)
uint8(0), uint8(0), // 校验和,先填0,一会计算
uint8(0), uint8(1), // 报文序号1
uint8(0), uint8(0), // ACK序号0
}

// 计算校验和
checksum := udpCheckSum(
source.IP.To4(),
destination.IP.To4(),
len(application) + 12,
append(transmission, application...))

transmission[6] = uint8(checksum >> 8)
transmission[7] = uint8(checksum % 256)

// 组装报文与发送
...

其中计算UDP校验和的函数:

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
func udpCheckSum(source []byte, destination []byte, udpLength int, udp []byte) uint16 {
// 创建伪首部
header := []byte{
source[0], source[1], source[2], source[3],
destination[0], destination[1], destination[2], destination[3],
0, 17, 0, uint8(udpLength),
}

total := append(header, udp...)

// 计算校验和
var (
sum uint32
length int = len(total)
index int
)
//以每16位为单位进行求和,直到所有的字节全部求完或者只剩下一个8位字节(如果剩余一个8位字节说明字节数为奇数个)
for length > 1 {
sum += uint32(total[index])<<8 + uint32(total[index+1])
index += 2
length -= 2
}
//如果字节数为奇数个,要加上最后剩下的那个8位字节
if length > 0 {
sum += uint32(total[index])
}
//加上高16位进位的部分
sum += (sum >> 16)
//别忘了返回的时候先求反
return uint16(^sum)
}

运行效果

接收方的运行结果:

1
2
ver=4 hdrlen=20 tos=0x68 totallen=36 id=0xf498 flags=0x2 fragoff=0x0 ttl=51 proto=17 cksum=0x62b6 src=xx.xx.xx.xx dst=172.27.16.5
[5 221 212 49 0 16 208 69 0 1 0 0 50 51 51 51]

可见到NAT之后,目的端口依然是54321(212256+49),而源端口不再是12345而是1501(5256+221)。当然上面的IP报头的IP地址也都变了。总之由于使用的还是公认的协议,只是改个端口而已不至于被当成未知报文丢弃。当然了也能看到应用层的数据“2333”(50 51 51 51)。
在发送方抓包可以发现在发出UDP报文之后,接收方返回了一个ICMP报文:

这就是操作系统自作多情的行为了,因此虽然成功发出了自定义的传输层协议报文,但距离真实的可用,还有一定距离。

自定义网络层协议

其实自定义网络层协议和自定义传输层协议差不多,在上面自定义传输层协议的时候,其实我们已经在ipv4包的帮助下自己构建IP分组。互联网环境对自定义传输层报文的要求已经非常严苛了,对自定义网络层协议更是如此,实际上,互联网是就是基于IP协议构建的网络,因此自定义的网络层协议也只能是IP协议的扩展,就和上面对UDP协议进行扩展一样。使用ipv4包可以控制自己的IP分组,当然也可以在其byte的基础上进行数据扩展,至于扩展之后的自定义网络层协议是不是IP协议,那得看是谁来定义了。
而如果在虚拟的直连拓扑上,自定义网络层协议也许还是可行的,但是意义就不大了,因为只能用于直连环境,跨一个路由器都做不到。所以也就觉得没有必要深入研究了。


Site by 喂草。
using hexo blog framework
with theme Noone.
蜀ICP备19016566号.