如何用Go语言编写代码实现发送和接收原始Socket数据包,并构造IP和TCP头部信息?

2026-05-08 00:402阅读0评论SEO基础
  • 内容介绍
  • 文章标签
  • 相关推荐

本文共计942个文字,预计阅读时间需要4分钟。

如何用Go语言编写代码实现发送和接收原始Socket数据包,并构造IP和TCP头部信息?

在Linux/macOS系统中,非root用户默认无法创建raw socket,这是内核的强制限制,而非Go语言的bug。即使使用了`syscall.SOCK_RAW`和`syscall.IPPROTO_RAW`,在创建socket时也会因为权限不足(`EPERM`)而失败。

  • 临时解决:用 sudo go run main.go(开发调试可用,但别上线)
  • 生产环境更稳妥的方式是给二进制加 CAP_NET_RAW 能力:sudo setcap cap_net_raw+ep ./myprogram,之后普通用户就能运行
  • macOS 还需额外注意:IPPROTO_RAW 不被支持,得用 IPPROTO_TCP + IP_HDRINCL 选项绕过,否则 setsockopt 会失败

syscall.SetsockoptInt 设置 IP_HDRINCL 没生效,发出去的包被内核重写 IP 头

没生效通常是因为调用时机错了——必须在 bind 之前设置,且 socket 类型得是 AF_INET + SOCK_RAW + IPPROTO_RAW(Linux)或 IPPROTO_TCP(macOS)。

  • Linux 示例关键顺序:fd := syscall.Socket(syscall.AF_INET, syscall.SOCK_RAW, syscall.IPPROTO_RAW)syscall.SetsockoptInt(fd, syscall.IPPROTO_IP, syscall.IP_HDRINCL, 1)syscall.Bind(...)
  • macOS 不支持 IPPROTO_RAW,得用 IPPROTO_TCP,但此时 IP_HDRINCL 仍有效;不过你得自己填满整个 IP+TCP 头,内核只负责发包,不校验字段合法性
  • 漏掉 IP_HDRINCL 或设成 0,内核就会自动填充 IP 头(包括 ID、TTL、checksum),你构造的头全被覆盖

手动计算 TCP 校验和总出错,tcp.Checksum 字段发出去后被接收方丢弃

TCP 校验和不是只对 TCP 段算,而是“伪头部 + TCP 头 + TCP 数据”三段拼起来再按 16 位反码求和。伪头部包含源/目的 IP、协议号、TCP 段长度(不含 IP 头),且必须是网络字节序。

  • 常见错误:IP 地址没转成大端(binary.BigEndian.PutUint32),或伪头部里写了 0 填充而不是真实 IP
  • TCP 长度字段是 TCP 头长 + 数据长度(单位字节),不是整个 IP 包长;如果数据长度为奇数,末尾要补 0 字节(只参与校验和计算,不发出去)
  • Go 标准库没有现成的校验和函数,别依赖 golang.org/x/net/ipv4Checksum 方法——它只适合 UDP;TCP 得自己写,或用 github.com/mdlayher/packet 这类专注 raw packet 的库

收到的原始 IP 包里 TCP 头偏移不对,tcp.DataOffset 解析出来是乱值

raw socket 收包时,内核默认不剥离 IP 头(除非你用 AF_PACKET),所以读出来的第一字节就是 IP 头起始。如果你直接当 TCP 头解析,肯定错。

立即学习“go语言免费学习笔记(深入)”;

  • 先解析 IP 头:读前 20 字节(无 option 时),提取 IHL 字段(第 0 字节低 4 位),乘以 4 得 IP 头长度,再跳过这段,才是 TCP 头起点
  • 别硬编码跳 20 字节——有 IP options 时 IHL > 5,比如 IHL=6 表示 IP 头 24 字节,跳少了就解析错 TCP 端口
  • 接收时用 syscall.Recvfrom,缓冲区至少预留 65535 字节(最大 IP 包),否则截断后 TCP 头不完整,DataOffset 就读歪了
事情说清了就结束。最常卡住的其实是权限和 IP 头偏移这两步,其他都能查文档补,但这俩错一点,包就发不出去或收不到。

本文共计942个文字,预计阅读时间需要4分钟。

如何用Go语言编写代码实现发送和接收原始Socket数据包,并构造IP和TCP头部信息?

在Linux/macOS系统中,非root用户默认无法创建raw socket,这是内核的强制限制,而非Go语言的bug。即使使用了`syscall.SOCK_RAW`和`syscall.IPPROTO_RAW`,在创建socket时也会因为权限不足(`EPERM`)而失败。

  • 临时解决:用 sudo go run main.go(开发调试可用,但别上线)
  • 生产环境更稳妥的方式是给二进制加 CAP_NET_RAW 能力:sudo setcap cap_net_raw+ep ./myprogram,之后普通用户就能运行
  • macOS 还需额外注意:IPPROTO_RAW 不被支持,得用 IPPROTO_TCP + IP_HDRINCL 选项绕过,否则 setsockopt 会失败

syscall.SetsockoptInt 设置 IP_HDRINCL 没生效,发出去的包被内核重写 IP 头

没生效通常是因为调用时机错了——必须在 bind 之前设置,且 socket 类型得是 AF_INET + SOCK_RAW + IPPROTO_RAW(Linux)或 IPPROTO_TCP(macOS)。

  • Linux 示例关键顺序:fd := syscall.Socket(syscall.AF_INET, syscall.SOCK_RAW, syscall.IPPROTO_RAW)syscall.SetsockoptInt(fd, syscall.IPPROTO_IP, syscall.IP_HDRINCL, 1)syscall.Bind(...)
  • macOS 不支持 IPPROTO_RAW,得用 IPPROTO_TCP,但此时 IP_HDRINCL 仍有效;不过你得自己填满整个 IP+TCP 头,内核只负责发包,不校验字段合法性
  • 漏掉 IP_HDRINCL 或设成 0,内核就会自动填充 IP 头(包括 ID、TTL、checksum),你构造的头全被覆盖

手动计算 TCP 校验和总出错,tcp.Checksum 字段发出去后被接收方丢弃

TCP 校验和不是只对 TCP 段算,而是“伪头部 + TCP 头 + TCP 数据”三段拼起来再按 16 位反码求和。伪头部包含源/目的 IP、协议号、TCP 段长度(不含 IP 头),且必须是网络字节序。

  • 常见错误:IP 地址没转成大端(binary.BigEndian.PutUint32),或伪头部里写了 0 填充而不是真实 IP
  • TCP 长度字段是 TCP 头长 + 数据长度(单位字节),不是整个 IP 包长;如果数据长度为奇数,末尾要补 0 字节(只参与校验和计算,不发出去)
  • Go 标准库没有现成的校验和函数,别依赖 golang.org/x/net/ipv4Checksum 方法——它只适合 UDP;TCP 得自己写,或用 github.com/mdlayher/packet 这类专注 raw packet 的库

收到的原始 IP 包里 TCP 头偏移不对,tcp.DataOffset 解析出来是乱值

raw socket 收包时,内核默认不剥离 IP 头(除非你用 AF_PACKET),所以读出来的第一字节就是 IP 头起始。如果你直接当 TCP 头解析,肯定错。

立即学习“go语言免费学习笔记(深入)”;

  • 先解析 IP 头:读前 20 字节(无 option 时),提取 IHL 字段(第 0 字节低 4 位),乘以 4 得 IP 头长度,再跳过这段,才是 TCP 头起点
  • 别硬编码跳 20 字节——有 IP options 时 IHL > 5,比如 IHL=6 表示 IP 头 24 字节,跳少了就解析错 TCP 端口
  • 接收时用 syscall.Recvfrom,缓冲区至少预留 65535 字节(最大 IP 包),否则截断后 TCP 头不完整,DataOffset 就读歪了
事情说清了就结束。最常卡住的其实是权限和 IP 头偏移这两步,其他都能查文档补,但这俩错一点,包就发不出去或收不到。