我们使用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 7 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已经足够我们使用了,没必要再使用第三方的库了,这里我们就不多做介绍了。