conn, err := net.Dial("udp", fmt.Sprintf("%s:%d", s.dstIP, s.dstPort)) if err != nil { return fmt.Errorf("failed to dial UDP: %w", err) } defer conn.Close()在此之后,您可以简单地像这样将字节写入 conn:
conn.Write(payload)您可以在文件af_inet.go[4]中找到我们使用这种方法的代码。
fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_RAW, syscall.IPPROTO_RAW) if err != nil { log.Fatalf("Failed to create raw socket: %v", err) } defer syscall.Close(fd) // 堆代码 duidaima.com // Set options: here, we enable IP_HDRINCL to manually include the IP header if err := syscall.SetsockoptInt(fd, syscall.IPPROTO_IP, syscall.IP_HDRINCL, 1); err != nil { log.Fatalf("Failed to set IP_HDRINCL: %v", err) }就是这样,现在我们可以像这样对文件描述符进行原始数据包的读写操作:
err := syscall.Sendto(fd, packet, 0, dstAddr)由于我使用了IPPROTO_RAW,我们绕过了内核网络栈的传输层,内核期望我们提供完整的 IP 数据包。我们使用BuildPacket 函数[6]来实现这一点。工作量略有增加,但原始套接字的好处在于你可以构造任何你想要的数据包。
fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_DGRAM, syscall.IPPROTO_UDP)在此之后,我们可以像之前一样使用Sendto方法简单地将有效负载写入套接字。
err = syscall.Sendto(fd, payload, 0, dstAddr)它看起来类似于原始套接字示例,但存在一些差异。关键区别在于,在这种情况下,我们创建了 UDP 类型的套接字,这意味着我们不需要像之前那样构造完整的数据包(IP 和 UDP 头部)。使用这种方法时,内核根据我们指定的目标 IP 和端口来构造 UDP 头部,并处理将其封装到 IP 数据包的过程。
handle, err := pcap.OpenLive(s.iface, 1500, false, pcap.BlockForever) if err != nil { return fmt.Errorf("could not open device: %w", err) } defer handle.Close()然后我们手动创建数据包[8],类似于前面的原始套接字方法,但在这种情况下,我们包含了以太网头部。之后,我们可以将数据包写入 pcap 句柄,就完成了!
err := handle.WritePacketData(packet)令我惊讶的是,这种方法带来了相当大的性能提升。我们远远超过了每秒一百万个数据包的大关: 1,354,087 个数据包每秒 - 几乎比之前高出 50 万个数据包每秒!注意,在本文的后面,我们将看到一个警告,但值得注意的是,当发送多个流(Go 例程)时,这种方法的工作效果会变差。
fd, err := syscall.Socket(syscall.AF_PACKET, syscall.SOCK_RAW, int(htons(syscall.ETH_P_IP)))这行代码创建一个原始套接字,可以在以太网层发送数据包。使用AF_PACKET时,我们指定SOCK_RAW表示我们对原始网络协议访问感兴趣。通过将协议设置为ETH_P_IP,我们告诉内核我们将处理 IP 数据包。获得套接字描述符后,我们必须将其绑定到网络接口。这一步可确保我们构建的数据包通过正确的网络设备发送出去:
addr := &syscall.SockaddrLinklayer{ Protocol: htons(syscall.ETH_P_IP), Ifindex: ifi.Index, }使用AF_PACKET构建数据包涉及手动创建以太网帧。这包括设置源和目标 MAC 地址以及 EtherType,以指示该帧承载的有效负载类型(在我们的例子中是 IP)。我们使用了与之前 Pcap 方法相同的BuildPacket 函数[9]。
syscall.Sendto(fd, packet, 0, addr)事实证明,AF_PACKET 方法的性能几乎与之前使用 pcap 方法时的性能相同。简单的谷歌搜索显示,libpcap(tcpdump 和 Go pcap 绑定等工具所使用的底层库)在 Linux 平台上使用AF_PACKET进行数据包捕获和注入。所以,这解释了它们的性能相似性。
xsk, err := xdp.NewSocket(link.Attrs().Index, s.queueID, nil)****注意,我们需要提供一个 NIC 队列;这清楚地表明我们正在比以前的方法工作在更低的级别上。完整的代码比其他选择要复杂一些,部分原因是我们需要使用用户空间内存缓冲区(UMEM)来存储数据包数据。这种方法减少了内核在数据包处理中的参与,从而缩短了数据包在系统层中传输的时间。通过直接在驱动程序级别构建和注入数据包。因此,请查看我的代码[12]。
./go-pktgen --dstip 192.168.64.2 --method benchmark \ --duration 5 --payloadsize 64 --iface veth0 +-------------+-----------+------+ | Method | Packets/s | Mb/s | +-------------+-----------+------+ | af_xdp | 2647936 | 1355 | | af_packet | 1368070 | 700 | | af_pcap | 1354087 | 693 | | udp_syscall | 861372 | 441 | | raw_socket | 793781 | 406 | | net_conn | 697277 | 357 | +-------------+-----------+------+重要的是关注每秒数据包数,因为这是软件网络堆栈的限制。Mb/s 数只是数据包大小乘以您可以生成的每秒数据包数。从传统的net.Dial 方法转换到使用 AF_PACKET,可以看到轻松实现了两倍的提升。然后,在使用 AF_XDP 时又实现了另外两倍的提升。如果您对快速发送数据包感兴趣,这确实是很重要的信息!
High-Speed Packet Processing in Go: From net.Dial to AF_XDP: https://atoonk.medium.com/high-speed-packet-transmission-in-go-from-net-dial-to-af-xdp-2699452efef9
参考资料
[1]发送 ICMP ping 消息: https://github.com/atoonk/ping-aws-ips
[2]GitHub 仓库: https://github.com/atoonk/go-pktgen
[3]这里: https://github.com/atoonk/go-pktgen/
[4]af_inet.go: https://github.com/atoonk/go-pktgen/blob/main/pktgen/af_inet.go
[5]rawsocket.go: https://github.com/atoonk/go-pktgen/blob/main/pktgen/rawsocket.go
[6]BuildPacket 函数: https://github.com/atoonk/go-pktgen/blob/main/pktgen/rawsocket.go#L66C17-L66C28
[7]pcap.go: https://github.com/atoonk/go-pktgen/blob/main/pktgen/pcap.go
[8]手动创建数据包: https://github.com/atoonk/go-pktgen/blob/main/pktgen/pcap.go#L51-L64
[9]BuildPacket 函数: https://github.com/atoonk/go-pktgen/blob/main/pktgen/af_packet.go#L56-L68
[10]XDP 的博客文章: https://toonk.io/building-an-xdp-express-data-path-based-bgp-peering-router/index.html
[11]asavie/xdp: http://github.com/asavie/xdp
[12]我的代码: https://github.com/atoonk/go-pktgen/blob/main/pktgen/af_xdp.go#L40-L97
[13]问题: https://github.com/asavie/xdp/issues/31
[14]只支持 RX: https://twitter.com/jtollet/status/1762616103883227490
[15]补丁: https://github.com/csulrong/gopacket/pull/1/files
[16]pktgen 库: https://github.com/atoonk/go-pktgen/tree/main