我们使用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.Socket
和gopackete
;如果还不满足,再考察有没有第三方库已经实现了相关的功能。当然有时候最后两个考量可能互换一下位置也可以。
为什么有时候我们需要收发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可以是udp
、udp4
、udp6
、unixgram
,或者是ip:1
、ip:icmp
这样的ip加protocol号或者protocol名称的方式。protocol的定义在 http://www.iana.org/assignments/protocol-numbers 文档中(你也可以在Linux主机的 /etc/protocols 中读取到,只不过可能不是最新的), 比如ICMP的协议号是1,TCP的协议号是6, UDP的协议号是17,协议号253、254用来测试等。
如果network是udp
、udp4
、udp6
,返回的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")) 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)) return buf.Bytes(), err }
|
读取数据的时候,ReadFrom
会读取ip header、ip payload (UDP packet)、control message (UDP没有control message),所以我们也可以读取和分析返回的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 { 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
已经足够我们使用了,没必要再使用第三方的库了,这里我们就不多做介绍了。