You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.07 08:54:28