
本文详解在 go 中通过 af_packet 原始套接字构造并发送完整以太网帧(含自定义 mac 头、ip 头和 tcp 头)的正确方法,指出常见绑定错误,并提供可运行的跨平台兼容示例。
本文详解在 go 中通过 af_packet 原始套接字构造并发送完整以太网帧(含自定义 mac 头、ip 头和 tcp 头)的正确方法,指出常见绑定错误,并提供可运行的跨平台兼容示例。
在 Go 中发送带自定义以太网头部的原始数据包,核心在于绕过内核协议栈的自动封装,直接向数据链路层写入完整帧(Ethernet + IP + TCP + Payload)。原代码存在两个根本性错误:
- 错误调用 syscall.Bind():AF_PACKET 套接字不接受 SockaddrInet4 绑定——该结构用于 IPv4 网络层套接字(如 AF_INET),而 AF_PACKET 需使用 SockaddrLinklayer 指定网卡索引与协议类型;
- 未指定目标网卡接口:syscall.Write() 对原始套接字写入时,必须先通过 syscall.Sendto() 或正确绑定 SockaddrLinklayer,否则内核无法识别出口设备,报错 no such device or address。
以下是修正后的完整实现(Linux 专用,需 root 权限):
package main
import (
"fmt"
"net"
"syscall"
"unsafe"
)
// SockaddrLinklayer 是 AF_PACKET 套接字所需的地址结构
type SockaddrLinklayer struct {
Family uint16
Protocol uint16
Ifindex int32
Hatype uint16
Pkttype uint8
Halen uint8
Addr [8]byte
}
func main() {
// 1. 创建 AF_PACKET 原始套接字(需 root)
fd, err := syscall.Socket(syscall.AF_PACKET, syscall.SOCK_RAW, syscall.IPPROTO_RAW)
if err != nil {
panic(fmt.Sprintf("socket creation failed: %v", err))
}
defer syscall.Close(fd)
// 2. 获取网卡索引(例如 eth0)
iface, err := net.InterfaceByName("eth0")
if err != nil {
panic(fmt.Sprintf("failed to find interface eth0: %v", err))
}
// 3. 构造 SockaddrLinklayer 地址:绑定到指定网卡及以太网协议
ll := &SockaddrLinklayer{
Family: syscall.AF_PACKET,
Protocol: htons(syscall.ETH_P_IP), // 发送 IPv4 包
Ifindex: int32(iface.Index),
Pkttype: 0, // PACKET_HOST:发往本机?此处设 0 表示从该接口发出
}
// 4. 绑定到网卡(关键!不可用 SockaddrInet4)
if err := syscall.Bind(fd, ll); err != nil {
panic(fmt.Sprintf("bind to interface failed: %v", err))
}
// 5. 构造完整以太网帧(目的MAC | 源MAC | 类型 | IP+TCP...)
// 示例:目的MAC=00:11:22:33:44:55,源MAC=aa:bb:cc:dd:ee:ff,ETH_TYPE=0x0800 (IPv4)
pkt := []byte{
// Ethernet Header (14 bytes)
0x00, 0x11, 0x22, 0x33, 0x44, 0x55, // DST MAC
0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, // SRC MAC
0x08, 0x00, // EtherType: IPv4
// IP Header (20 bytes, minimal)
0x45, 0x00, 0x00, 0x3c, // Version/IHL, TOS, Total Length
0x00, 0x00, 0x00, 0x00, // ID, Flags+Fragment Offset
0x40, 0x06, 0xb5, 0x94, // TTL, Protocol(TCP), Header Checksum
192, 168, 1, 100, // Src IP (192.168.1.100)
192, 168, 1, 200, // Dst IP (192.168.1.200)
// TCP Header (20 bytes, no options)
0x1f, 0x90, 0x00, 0x50, // Src Port(8080), Dst Port(80)
0x00, 0x00, 0x00, 0x00, // Seq Number
0x00, 0x00, 0x00, 0x00, // Ack Number
0x50, 0x02, 0x7d, 0x00, // Data Offset, Flags, Window Size
0x00, 0x00, 0x00, 0x00, // Checksum, Urgent Pointer
}
// 6. 发送帧(注意:此处使用 Sendto,因已绑定网卡,也可用 Write)
n, err := syscall.Sendto(fd, pkt, 0, ll)
if err != nil {
panic(fmt.Sprintf("sendto failed: %v, written %d bytes", err, n))
}
fmt.Printf("✅ Successfully sent %d-byte raw frame via %s\n", n, iface.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 查看),避免 no such device;
- ✅ 帧完整性:以太网帧长度不得低于 64 字节(含 FCS),不足需填充;IP/TCP 校验和需手动计算(示例中省略,生产环境务必实现);
- ⚠️ 跨平台限制:AF_PACKET 仅 Linux 支持;macOS/Windows 需借助 pcap 库(如 github.com/google/gopacket)或 AF_INET + SOCK_RAW(无以太网头控制权);
- ? 安全提示:原始套接字能力敏感,避免在非可信环境中开放。
总结:Go 原生 syscall 可精准操控数据链路层,但需严格遵循 AF_PACKET 的地址结构规范,放弃对网络层地址(如 SockaddrInet4)的误用,并确保帧格式、权限与接口配置三者协同。对于复杂协议栈操作,推荐结合 gopacket 等成熟库提升健壮性与可维护性。


















