几种使用Go发送IP包的方法

我们使用Go标准库中的net包,很容易发送UDP和TCP的packet,以及在它们基础上开发应用层的程序,比如HTTP、RPC等框架和程序,甚至我们可以利用官方扩展包golang.org/x/net/icmp,专门进行icmp packet的发送和接收,不过,有时候我们想进行更低层次的网络通讯,这个时候我们就需要借助一些额外的库,或者做一些额外的设置,当前相关的介绍IP层packet收发技术并没有很好的组织和介绍,本文尝试介绍几种收发IP packet的方式。

依然,我们介绍IPv4相关的技术, IPv6会单独一章进行介绍。

在进行Go网络编程的时候,对于技术的选择,针对常用的场景,我个人有一点点小小的建议:如果标准库能够提供相关的功能,那么就使用标准库;否则再考察官方扩展库golang.org/x/net是否能够满足需求;如果还不合适,那么就考虑使用syscall.Socketgopackete;如果还不满足,再考察有没有第三方库已经实现了相关的功能。当然有时候最后两个考量可能互换一下位置也可以。

为什么有时候我们需要收发IP packet呢?因为我们有时候想进行对IPv4 header进行详细的设置或者检查。如下面的IPv4 header的定义:

有时候我们想设置TOS、Identification、TTL、Options,我们就必须能够自己组装IPv4 packet,并能够发送出去;读取亦然。

使用标准库

使用 net.ListenPacket/net.ListenPacket 探索

标准库提供了一种读写IP packet的方法,可以实现一半的读写的能力,它是通过func ListenPacket(network, address string) (PacketConn, error)函数实现,其中network可以是udpudp4udp6unixgram,或者是ip:1ip:icmp这样的ip加protocol号或者protocol名称的方式。protocol的定义在 http://www.iana.org/assignments/protocol-numbers 文档中(你也可以在Linux主机的 /etc/protocols 中读取到,只不过可能不是最新的), 比如ICMP的协议号是1,TCP的协议号是6, UDP的协议号是17,协议号253、254用来测试等。

如果network是udpudp4udp6,返回的PacketConn底层是*net.UDPConn,如果network是以ip为前缀,那么返回的PacketConn是*net.IPConn,在这种情况下,你可以使用明确的func ListenIP(network string, laddr *IPAddr) (*IPConn, error)

下面是一个使用net.ListenPacket的客户端的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func main() {
conn, err := net.ListenPacket("ip4:udp", "127.0.0.1") // 本地地址
if err != nil {
fmt.Println("DialIP failed:", err)
return
}
data, _ := encodeUDPPacket("127.0.0.1", "192.168.0.1", []byte("hello world")) // 生成一个UDP包
if _, err := conn.WriteTo(data, &net.IPAddr{IP: net.ParseIP("192.168.0.1")}); err != nil {
panic(err)
}
buf := make([]byte, 1024)
n, peer, err := conn.ReadFrom(buf)
if err != nil {
panic(err)
}
fmt.Printf("received response from %s: %s\n", peer.String(), buf[8:n])
}

这个例子一开始产生一个PacketConn,实际是一个*net.IPConn, 但是需要注意的是,这里的conn发送的是 UDP层的包,并不包含IP层,下面这个例子定义了IP层,只是用来计算checksum,实际并没有用途:

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 encodeUDPPacket(localIP, dstIP string, payload []byte) ([]byte, error) {
ip := &layers.IPv4{
SrcIP: net.ParseIP(localIP),
DstIP: net.ParseIP(dstIP),
Version: 4,
Protocol: layers.IPProtocolUDP,
}
udp := &layers.UDP{
SrcPort: layers.UDPPort(0),
DstPort: layers.UDPPort(8972),
}
udp.SetNetworkLayerForChecksum(ip)
buf := gopacket.NewSerializeBuffer()
opts := gopacket.SerializeOptions{
ComputeChecksums: true,
FixLengths: true,
}
err := gopacket.SerializeLayers(buf, opts, udp, gopacket.Payload(payload))
return buf.Bytes(), err
}

同样的,服务端读取到消息后,也是只返回IPv4 header下层的protocol层数据,IPv header数据被剥除掉了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func main() {
conn, err := net.ListenPacket("ip4:udp", "192.168.0.1")
if err != nil {
panic(err)
}
buf := make([]byte, 1024)
for {
n, peer, err := conn.ReadFrom(buf)
if err != nil {
panic(err)
}
fmt.Printf("received request from %s: %s\n", peer.String(), buf[8:n])
data, _ := encodeUDPPacket("192.168.0.1", "127.0.0.1", []byte("hello world"))
_, err = conn.WriteTo(data, &net.IPAddr{IP: net.ParseIP("127.0.0.1")})
if err != nil {
panic(err)
}
}
}

注意这里conn.ReadFrom(buf)读取到的数据包含UDP header,但是不包含IP header, UDP的header是8个字节,所以buf[8:n]就是payload的数据。

如果你看go标准库的源码,你可以看到Go收到IP packet后,会调用stripIPv4Header剥去IPv4 header:

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
func (c *IPConn) readFrom(b []byte) (int, *IPAddr, error) {
var addr *IPAddr
n, sa, err := c.fd.readFrom(b)
switch sa := sa.(type) {
case *syscall.SockaddrInet4:
addr = &IPAddr{IP: sa.Addr[0:]}
n = stripIPv4Header(n, b)
case *syscall.SockaddrInet6:
addr = &IPAddr{IP: sa.Addr[0:], Zone: zoneCache.name(int(sa.ZoneId))}
}
return n, addr, err
}
func stripIPv4Header(n int, b []byte) int {
if len(b) < 20 {
return n
}
l := int(b[0]&0x0f) << 2
if 20 > l || l > len(b) {
return n
}
if b[0]>>4 != 4 {
return n
}
copy(b, b[l:])
return n - l
}

使用 ipv4.RawConn 收发IP packet

最简单的方式,是使用ipv4.NewRawConn(conn)net.PacketConn转换成*ipv4.RawConn,如下面的客户端代码:

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 main() {
conn, err := net.ListenPacket("ip4:udp", "127.0.0.1")
if err != nil {
fmt.Println("DialIP failed:", err)
return
}
rc, err := ipv4.NewRawConn(conn)
if err != nil {
panic(err)
}
data, _ := encodeUDPPacket("127.0.0.1", "192.168.0.1", []byte("hello world"))
if _, err := rc.WriteToIP(data, &net.IPAddr{IP: net.ParseIP("192.168.0.1")}); err != nil {
panic(err)
}
rbuf := make([]byte, 1024)
_, payload, _, err := rc.ReadFrom(rbuf)
if err != nil {
panic(err)
}
fmt.Printf("received response: %s\n", payload[8:])
}

注意这里的encodeUDPPacket实现和上面的例子中的实现就不一样了,它包含ip header的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func encodeUDPPacket(localIP, dstIP string, payload []byte) ([]byte, error) {
ip := &layers.IPv4{
...
}
udp := &layers.UDP{
...
}
udp.SetNetworkLayerForChecksum(ip)
buf := gopacket.NewSerializeBuffer()
opts := gopacket.SerializeOptions{
ComputeChecksums: true,
FixLengths: true,
}
err := gopacket.SerializeLayers(buf, opts, ip, udp, gopacket.Payload(payload)) // 注意这里包含ip
return buf.Bytes(), err
}

读取数据的时候,ReadFrom会读取ip header、ip payload (UDP packet)、control message (UDP没有control message),所以我们也可以读取和分析返回的IP header。

使用 SyscallConn 实现读取IP header

使用标准库的(*net.IPConn).SyscallConn() 可以实现写数据时发送UDP(或者其他ip protocol)包的数据,但是在读取数据的时候,把IPv4 header读取出来。

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
func main() {
conn, err := net.ListenPacket("ip4:udp", "127.0.0.1")
if err != nil {
fmt.Println("DialIP failed:", err)
return
}
sc, err := conn.(*net.IPConn).SyscallConn()
if err != nil {
panic(err)
}
var addr syscall.SockaddrInet4
copy(addr.Addr[:], net.ParseIP("192.168.0.1").To4())
addr.Port = 8972
data, _ := encodeUDPPacket("127.0.0.1", "192.168.0.1", []byte("hello world"))
err = sc.Write(func(fd uintptr) bool {
// 将 UDP 数据包写入 Socket
err := syscall.Sendto(int(fd), data, 0, &addr)
if err != nil {
panic(err)
}
return err == nil
})
if err != nil {
panic(err)
}
var n int
buf := make([]byte, 1024)
err = sc.Read(func(fd uintptr) bool {
var err error
n, err = syscall.Read(int(fd), buf)
if err != nil {
return false
}
return true
})
if err != nil {
panic(err)
}
iph, err := ipv4.ParseHeader(buf[:n])
if err != nil {
panic(err)
}
fmt.Printf("received response from %s: %s\n", iph.Src.String(), buf[ipv4.HeaderLen+8:])
}

为什么发送的时候没有办法设置IPv4 header, 读取的时候却能读取到IPv4 header呢?这和底层使用的Socket有关,注意我们标准库在针对IPConn的建立时,使用的是syscall.AF_INET和syscall.SOCK_RAW, protocol创建的socket,默认情况下我们值需要填写ip payload数据(protocol的数据),内核协议栈会自动生成IP header,但是读取时会把ip header读取返回,所以Go的行为和Socket一致,标准库为了读写一致,读取出来的数据还把IPv4 header给剥除掉了。

那么为啥ipv4.RawConn能够发送IPv4 header的数据呢?这是因为它对Socket进行了设置:

1
2
3
4
5
6
func NewRawConn(c net.PacketConn) (*RawConn, error) {
...
so, ok := sockOpts[ssoHeaderPrepend]
...
return r, nil
}

ssoHeaderPrepend 选项就是设置IP_HDRINCL:

1
ssoHeaderPrepend: {Option: socket.Option{Level: iana.ProtocolIP, Name: unix.IP_HDRINCL, Len: 4}},

所以即使你不使用ipv4.RawConn,你也可以针对标准库的*net.IPConn进行设置,让它支持可以手工写IPv4 header:

1
err = syscall.SetsockoptInt(fd, syscall.IPPROTO_IP, syscall.IP_HDRINCL, 1)

当然为了同时支持读写IPv4 header,还是转换成*ipv4.RawConn最方便。

使用 syscall.Socket 收发 IP packet

最简单的,类似C等其他语言访问系统调用,我们可以实现收发IPv4 packet。有时候你在开发网络程序时,一点都不用担心技术上的障碍,大不了我们使用最原始的系统调用来实现网络通讯。

下面这个例子建立了一个Socket,这里protocol我们没有使用UDP,其实你可以改造成UDP代码。

注意我们需要设置IP_HDRINCL为1,我们手工设置IPv4 header,而不是让内核协议栈帮我们设置。

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
func main() {
fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_RAW, syscall.IPPROTO_RAW)
if err != nil {
fmt.Println("socket failed:", err)
return
}
defer syscall.Close(fd)
err = syscall.SetsockoptInt(fd, syscall.IPPROTO_IP, syscall.IP_HDRINCL, 1)
if err != nil {
panic(err)
}
// 本地地址
addr := syscall.SockaddrInet4{Addr: [4]byte{127, 0, 0, 1}}
// 发送自定义协议数据包
ip4 := &layers.IPv4{
SrcIP: net.ParseIP("127.0.0.1"),
DstIP: net.ParseIP("192.168.0.1"),
Version: 4,
TTL: 64,
Protocol: syscall.IPPROTO_RAW,
}
pbuf := gopacket.NewSerializeBuffer()
opts := gopacket.SerializeOptions{
ComputeChecksums: true,
FixLengths: true,
}
payload := []byte("hello world")
err = gopacket.SerializeLayers(pbuf, opts, ip4, gopacket.Payload(payload))
if err != nil {
fmt.Println("serialize failed:", err)
return
}
if err := syscall.Sendto(fd, pbuf.Bytes(), 0, &addr); err != nil {
fmt.Println("sendto failed:", err)
return
}
buf := make([]byte, 1024)
for {
n, peer, err := syscall.Recvfrom(fd, buf, 0)
if err != nil {
fmt.Println("recvfrom failed:", err)
return
}
raddr := net.IP(peer.(*syscall.SockaddrInet4).Addr[:]).String()
if raddr != "192.168.0.1" {
continue
}
iph, err := ipv4.ParseHeader(buf[:n])
if err != nil {
fmt.Println("parse ipv4 header failed:", err)
return
}
fmt.Printf("received response from %s: %s\n", raddr, string(buf[iph.Len:n]))
break
}
}
func htons(i uint16) uint16 {
return (i<<8)&0xff00 | i>>8
}

而服务器端代码如下,注意这里我们为了值关注我们程序自己的数据包,使用了bpf做了filter筛选,会提高性能:

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
func main() {
fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_RAW, syscall.IPPROTO_RAW)
if err != nil {
fmt.Println("socket failed:", err)
return
}
defer syscall.Close(fd)
err = syscall.SetsockoptInt(fd, syscall.IPPROTO_IP, syscall.IP_HDRINCL, 1)
if err != nil {
panic(err)
}
filter.applyTo(fd)
// 接收自定义协议数据包
buf := make([]byte, 1024)
for {
n, peer, err := syscall.Recvfrom(fd, buf, 0)
if err != nil {
fmt.Println("recvfrom failed:", err)
return
}
iph, err := ipv4.ParseHeader(buf[:n])
if err != nil {
fmt.Println("parse header failed:", err)
return
}
if string(buf[iph.Len:n]) != "hello world" {
continue
}
fmt.Printf("received request from %s: %s\n", iph.Src.String(), string(buf[iph.Len:n]))
iph.Src, iph.Dst = iph.Dst, iph.Src
replayIpHeader, _ := iph.Marshal()
copy(buf[:iph.Len], replayIpHeader)
if err := syscall.Sendto(fd, buf[:n], 0, peer); err != nil {
fmt.Println("sendto failed:", err)
return
}
}
}

当然,还可以使用第三方的库比如gopacket收发IPv4的包,只不过*ipv4.RawConn已经足够我们使用了,没必要再使用第三方的库了,这里我们就不多做介绍了。