
本文详解在 go 中通过 af_packet 原始套接字构造并发送完整以太网帧(含自定义 mac 头、ip 头和 tcp 头)的正确方法,指出常见绑定错误,并提供可运行的完整示例代码与关键注意事项。
本文详解在 go 中通过 af_packet 原始套接字构造并发送完整以太网帧(含自定义 mac 头、ip 头和 tcp 头)的正确方法,指出常见绑定错误,并提供可运行的完整示例代码与关键注意事项。
在 Go 中发送带自定义以太网头部的原始数据包,核心在于绕过内核网络协议栈的自动封装,直接向数据链路层写入完整帧(Ethernet Frame)。这要求开发者手动构造:目标/源 MAC 地址(14 字节以太网头)、IPv4 头、TCP 头及有效载荷——任何依赖 syscall.Bind() 绑定到 SockaddrInet4 的尝试都是错误的,因为 AF_PACKET 套接字不接受 IP 层地址绑定;它面向的是网络接口(如 eth0),需通过 SockaddrLinklayer 指定设备和协议类型。
以下是修正后的完整可运行示例(需 root 权限,且目标接口名需适配,如 eth0 或 enp0s3):
package main
import (
"fmt"
"net"
"syscall"
"unsafe"
)
// SockaddrLinklayer 是 AF_PACKET 所需的 socket 地址结构(对应 C struct sockaddr_ll)
type SockaddrLinklayer struct {
Protocol uint16
Ifindex int
Hatype uint16
Pkttype uint8
Halen uint8
Addr [8]byte
}
func main() {
fmt.Println("=== Sending raw Ethernet frame with custom headers ===")
// 1. 创建 AF_PACKET 原始套接字(需 CAP_NET_RAW 或 root)
fd, err := syscall.Socket(syscall.AF_PACKET, syscall.SOCK_RAW, syscall.SOCK_CLOEXEC, syscall.ETH_P_ALL)
if err != nil {
panic(fmt.Sprintf("socket creation failed: %v", err))
}
defer syscall.Close(fd)
// 2. 获取目标网络接口索引(例如 eth0)
ifi, err := net.InterfaceByName("eth0")
if err != nil {
panic(fmt.Sprintf("failed to find interface 'eth0': %v", err))
}
// 3. 构造 SockaddrLinklayer:指定接口、协议(ETH_P_IP)、广播类型等
sll := &SockaddrLinklayer{
Protocol: htons(syscall.ETH_P_IP), // 发送 IPv4 数据包
Ifindex: ifi.Index,
Hatype: 1, // ARPHRD_ETHER
Pkttype: 0, // PACKET_HOST(发往本机)或 4(PACKET_BROADCAST)等
Halen: 6, // MAC 地址长度
// Addr 字段:此处可填目标 MAC(如全零表示广播),但实际帧中 MAC 已内嵌于 pkt
}
// 4. 构造完整以太网帧(示例:ARP 请求仅作示意;真实 TCP 需严格校验和)
// 注意:以下为简化演示帧(Ethernet + IPv4 + ICMP),生产环境需动态计算 IP/TCP 校验和
pkt := []byte{
// Ethernet Header (14 bytes)
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // dst MAC (broadcast)
0x00, 0x11, 0x22, 0x33, 0x44, 0x55, // src MAC (dummy)
0x08, 0x00, // EtherType: IPv4
// IPv4 Header (20 bytes, minimal)
0x45, 0x00, 0x00, 0x1c, // Version, IHL, TOS, Total Length=28
0x00, 0x00, 0x00, 0x00, // ID, Flags, Frag Offset
0x40, 0x00, 0x40, 0x01, // TTL=64, Protocol=ICMP, Header Checksum (placeholder)
0xc0, 0xa8, 0x01, 0x01, // Src IP: 192.168.1.1
0xc0, 0xa8, 0x01, 0x02, // Dst IP: 192.168.1.2
// ICMP Echo Request (8 bytes)
0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
}
// ⚠️ 关键:不能调用 syscall.Bind()!AF_PACKET 使用 sendto() 或 write() 配合 SockaddrLinklayer
// 正确方式:使用 syscall.Sendto 写入帧并指定目标链路层地址
err = syscall.Sendto(fd, pkt, 0, sll)
if err != nil {
panic(fmt.Sprintf("sendto failed: %v", err))
}
fmt.Printf("✅ Successfully sent %d-byte raw frame on interface %s\n", len(pkt), ifi.Name)
}
// htons 将主机字节序转为网络字节序(大端)
func htons(i uint16) uint16 {
return (i<<8)&0xff00 | i>>8
}关键注意事项:
- ✅ 权限要求:必须以 root 或具备 CAP_NET_RAW 能力运行(sudo go run main.go)。
- ✅ 接口有效性:确保 eth0 存在且已启用(可用 ip link show 验证);虚拟机中常为 enp0s3 等。
- ✅ 校验和处理:内核默认不计算 IP/TCP 校验和(除非启用 IP_HDRINCL)。生产环境务必手动计算并填充,否则目标主机将丢弃无效包。推荐使用 gopacket 库(如 layers.IPv4.SetNetworkLayerForChecksum())辅助生成。
- ❌ 禁止绑定 IP 地址:syscall.Bind(fd, &SockaddrInet4{...}) 对 AF_PACKET 无效,会导致 invalid argument 错误。
- ❌ 避免 syscall.Write 直接写入:虽部分内核支持,但 syscall.Sendto + SockaddrLinklayer 是标准、可靠且可指定接口的方式。
进阶建议:
对于复杂协议构造(如 TCP 三次握手),强烈推荐使用成熟库替代手写二进制:
掌握原始套接字的本质——“把字节精准投递到网卡驱动”——是构建高性能网络工具(如扫描器、协议分析器)的基础。务必从最小可行帧(如 ARP 或 ICMP)开始验证,再逐步扩展至 TCP。


















