Ubuntu 22.04 LTS下启用TUN设备GSO实现TCP大报文分段失败的问题求助
Ubuntu 22.04 LTS下启用TUN设备GSO实现TCP大报文分段失败的问题求助
我现在在Ubuntu 22.04 LTS环境下做一个基于TUN设备的网络项目,需求是:通过MTU为65535的TUN设备发送10KB以上的TCP jumbo报文,让内核通过软件GSO或者网卡的TSO(如果硬件支持)自动把这些大报文分段成适配eth0(MTU 1500)的小报文再发送,避免在用户态自己处理分段逻辑。
我查了资料,知道要实现这个需要满足几个条件:
- 创建设备时加上
IFF_VNET_HDR标志 - 通过
TUNSETOFFLOAD启用GSO和校验和卸载 - 往TUN写报文前要正确添加virtio-net头部
但实际操作下来,关掉GSO时整个流程完全正常,一旦启用GSO,连最基础的TCP三次握手都建立不起来。我已经写了Go代码来初始化TUN设备和处理virtio-net头,但是始终找不到问题所在。
有没有朋友成功实现过这种场景?能不能分享一个最小可运行的示例或者排查思路?如果有相关的官方文档或者内核源码注释推荐就更好了!
我的TUN设备初始化代码(Go)
package tun import ( "fmt" "os" "syscall" "unsafe" ) const ( IFF_TUN = 0x0001 IFF_NO_PI = 0x1000 IFF_VNET_HDR = 0x4000 TUNSETIFF = 0x400454ca TUNSETVNETHDRSZ = 0x400454d8 TUNSETOFFLOAD = 0x400454d0 TUN_OFFLOAD_CSUM = 0x01 TUN_OFFLOAD_GSO = 0x02 ) type ifreq struct { Name [16]byte Flags uint16 _ [22]byte } func CreateTUN(name string) (*os.File, error) { f, err := os.OpenFile("/dev/net/tun", os.O_RDWR, 0) if err != nil { return nil, fmt.Errorf("failed to open /dev/net/tun: %v", err) } var ifr ifreq copy(ifr.Name[:], name) ifr.Flags = IFF_TUN | IFF_NO_PI | IFF_VNET_HDR _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, f.Fd(), uintptr(TUNSETIFF), uintptr(unsafe.Pointer(&ifr))) if errno != 0 { return nil, fmt.Errorf("TUNSETIFF failed: %v", errno) } if err := EnableGSO(f.Fd()); err != nil { return nil, err } return f, nil } func EnableGSO(tunFileFd uintptr) error { hdrSize := uint32(12) // virtio-net header size if _, _, errno := syscall.Syscall( syscall.SYS_IOCTL, tunFileFd, uintptr(TUNSETVNETHDRSZ), uintptr(unsafe.Pointer(&hdrSize)), ); errno != 0 { return fmt.Errorf("TUNSETVNETHDRSZ failed: %v", errno) } if _, _, errno := syscall.Syscall( syscall.SYS_IOCTL, tunFileFd, uintptr(TUNSETOFFLOAD), uintptr(TUN_OFFLOAD_GSO|TUN_OFFLOAD_CSUM), ); errno != 0 { return fmt.Errorf("TUNSETOFFLOAD failed: %v", errno) } fmt.Printf("Enabled GSO+CSUM with virtio-net headers on TUN (fd=%d)\n", tunFileFd) return nil }
给报文添加virtio-net头的代码
package main import ( "encoding/binary" "errors" ) const ( VIRTIO_NET_HDR_F_NEEDS_CSUM = 1 VIRTIO_NET_HDR_GSO_NONE = 0 VIRTIO_NET_HDR_GSO_TCPV4 = 1 VIRTIO_NET_HDR_GSO_TCPV6 = 4 TCP_CHECKSUM_OFFSET = 16 ) type VirtioNetHdr struct { Flags uint8 GSOType uint8 HdrLen uint16 GSOSize uint16 CSumStart uint16 CSumOffset uint16 } func PrependVnetHeader(pkt []byte, mtu int) ([]byte, error) { if len(pkt) < 1 { return nil, errors.New("empty packet") } ipVersion := (pkt[0] & 0xF0) >> 4 var hdr VirtioNetHdr switch ipVersion { case 4: // IPv4 if len(pkt) < 20 { return nil, errors.New("invalid IPv4 packet") } if pkt[9] != 6 { // Only TCP break } ipHeaderLen := int(pkt[0]&0x0F) * 4 tcpLen := len(pkt) - ipHeaderLen hdr.Flags = VIRTIO_NET_HDR_F_NEEDS_CSUM hdr.HdrLen = uint16(ipHeaderLen + 20) hdr.CSumStart = uint16(ipHeaderLen) hdr.CSumOffset = TCP_CHECKSUM_OFFSET if tcpLen > mtu { hdr.GSOType = VIRTIO_NET_HDR_GSO_TCPV4 hdr.GSOSize = uint16(mtu) } else { hdr.GSOType = VIRTIO_NET_HDR_GSO_NONE } case 6: // IPv6 if len(pkt) < 40 { return nil, errors.New("invalid IPv6 packet") } if pkt[6] != 6 { // Only TCP break } ipHeaderLen := 40 tcpLen := len(pkt) - ipHeaderLen hdr.Flags = VIRTIO_NET_HDR_F_NEEDS_CSUM hdr.HdrLen = uint16(ipHeaderLen + 20) hdr.CSumStart = uint16(ipHeaderLen) hdr.CSumOffset = TCP_CHECKSUM_OFFSET if tcpLen > mtu { hdr.GSOType = VIRTIO_NET_HDR_GSO_TCPV6 hdr.GSOSize = uint16(mtu) } else { hdr.GSOType = VIRTIO_NET_HDR_GSO_NONE } } // Serialize header + packet buf := make([]byte, 12+len(pkt)) buf[0] = hdr.Flags buf[1] = hdr.GSOType binary.LittleEndian.PutUint16(buf[2:4], hdr.HdrLen) binary.LittleEndian.PutUint16(buf[4:6], hdr.GSOSize) binary.LittleEndian.PutUint16(buf[6:8], hdr.CSumStart) binary.LittleEndian.PutUint16(buf[8:10], hdr.CSumOffset) // buf[10:12] reserved = 0 copy(buf[12:], pkt) return buf, nil }
eth0上的tcpdump抓包结果
root@2458c936acdb:/# tcpdump -i eth0 port 80 -nn -vv -xx tcpdump: listening on eth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes 20:57:13.100414 IP (tos 0x0, ttl 63, id 60084, offset 0, flags [DF], proto TCP (6), length 60) 172.17.0.2.56018 > 150.171.28.10.80: Flags [S], cksum 0xafdf (correct), seq 3841106198, win 64240, options [mss 63900,sackOK,TS val 2498122859 ecr 0,nop,wscale 7], length 0 0x0000: 0242 fe82 3318 32e1 1aa3 4686 0800 4500 0x0010: 003c eab4 4000 3f06 f23e ac11 0002 96ab 0x0020: 1c0a dad2 0050 e4f2 a116 0000 0000 a002 0x0030: faf0 afdf 0000 0204 f99c 0402
我注意到SYN包里的MSS是63900(对应TUN的MTU 65535 - 20(IP) - 20(TCP)),但eth0的MTU只有1500,正常MSS应该是1460,这会不会是问题所在?但我以为GSO会自动处理这个,把大的MSS的报文分段成1500的MTU。
内容来源于stack exchange
相关产品推荐
相关产品推荐

