go标准库不支持原始套接字,x/net/icmp仅负责icmp编解码,需配合x/net/ipv4/ipv6使用且须root权限;直接net.dial("icmp")失败因icmp属网络层,未注册于net包;正确方式是用icmp.listenpacket或ipv4.newrawconn创建原始连接,并注意ip头解析、权限与类型校验。

Go 标准库不支持原始套接字操作,x/net/icmp 是官方维护的、用于构造和解析 ICMP 报文的补充包,但它本身不处理底层 socket 绑定与收发——你需要配合 x/net/ipv4 或 x/net/ipv6 使用,且必须以 root 权限运行。
为什么直接 dial “icmp” 会报 protocol not supported
很多人尝试 net.Dial("icmp", ...) 或 net.ListenPacket("icmp", ...),结果遇到 protocol not supported。这是因为 Go 的 net 包默认不注册 ICMP 协议族;它只支持 TCP/UDP/Unix 等高层协议。ICMP 属于网络层协议,必须通过 raw socket(即 AF_INET + SOCK_RAW)操作,而 x/net/icmp 只负责编码/解码。
- 正确路径是:用
ipv4.NewRawConn或ipv6.NewRawConn创建原始连接,再用icmp.Message序列化/反序列化 - Linux 下需
cap_net_raw权限或 root;macOS 需 root;Windows 不支持 raw socket(除非用 WSL) -
x/net/icmp不提供自动 checksum 计算逻辑——它会在Marshal时帮你算,但你不能跳过这步直接填 0
发送 ICMP Echo Request 的最小可行代码
以下是在 IPv4 下发一个 ping 请求的典型流程,注意 socket 初始化和消息构造顺序:
conn, err := icmp.ListenPacket(ipv4.ProtocolICMP, "0.0.0.0:0")
if err != nil {
log.Fatal(err)
}
defer conn.Close()
// 构造 Echo Request
msg := &icmp.Message{
Type: ipv4.ICMPTypeEcho, // 注意:不是 icmp.EchoRequest(那是旧版常量)
Code: 0,
Body: &icmp.Echo{
ID: os.Getpid() & 0xffff,
Seq: 1,
Data: []byte("hello"),
},
}
b, err := msg.Marshal(nil) // 必须调用 Marshal,它会填 checksum 并返回完整字节
if err != nil {
log.Fatal(err)
}
_, err = conn.WriteTo(b, &net.IPAddr{IP: net.ParseIP("8.8.8.8")})
if err != nil {
log.Fatal(err)
}
-
icmp.ListenPacket内部已封装了ipv4.NewRawConn,比手动创建更简洁,推荐优先使用 -
ipv4.ICMPTypeEcho是正确类型常量;icmp.EchoRequest已废弃,用它会导致 Type 值错误 -
Marshal(nil)第二个参数可传自定义 checksum buffer,一般传nil即可 - 目标地址必须是
*net.IPAddr,不能是*net.UDPAddr或字符串
接收 ICMP Reply 并解析的注意事项
接收端容易忽略 IP 头剥离和 ICMP 类型校验,导致解析失败或 panic:
b := make([]byte, 1500)
n, peer, err := conn.ReadFrom(b)
if err != nil {
log.Fatal(err)
}
// b[:n] 包含完整的 IP+ICMP 数据包,需跳过 IP header
// ipv4.HeaderLenMinimum = 20,但实际可能有 options,所以用 ParseHeader 更稳妥
iph, err := ipv4.ParseHeader(b[:n])
if err != nil {
log.Printf("parse IP header failed: %v", err)
return
}
icmpBuf := b[iph.Len:n] // 提取 ICMP payload
msg, err := icmp.ParseMessage(iph.Protocol, icmpBuf)
if err != nil {
log.Printf("parse ICMP message failed: %v", err)
return
}
if msg.Type == ipv4.ICMPTypeEchoReply && msg.Body != nil {
if echo, ok := msg.Body.(*icmp.Echo); ok {
fmt.Printf("reply from %v, seq=%d, len=%d\n", peer, echo.Seq, len(echo.Data))
}
}
- 原始 socket 收到的是带 IP 头的完整数据包,必须先解析 IP header 才能定位 ICMP 起始位置
-
icmp.ParseMessage第一个参数必须传iph.Protocol(即1),不能硬写ipv4.ProtocolICMP—— 后者是 socket 层协议号,不是 IP header 中的 Protocol 字段值 - 收到的可能是其他 ICMP 类型(如 Destination Unreachable),务必检查
msg.Type再做类型断言 - 不要假设
msg.Body非空;某些 ICMP 类型(如 Timestamp)Body 结构不同,强行断言会 panic
最常被跳过的一步是权限检查和 IP header 解析——写完代码跑不起来,八成卡在这两个地方。另外,x/net/icmp 对 IPv6 的支持更严格,比如 icmp.ListenPacket 在 IPv6 下必须指定具体网卡(如 "fe80::%en0"),否则绑定失败。











