7

几种使用Go发送IP包的方法

 1 year ago
source link: https://colobu.com/2023/05/13/send-IP-packets-in-Go/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

几种使用Go发送IP包的方法

我们使用Go标准库中的net包,很容易发送UDP和TCP的packet,以及在它们基础上开发应用层的程序,比如HTTP、RPC等框架和程序,甚至我们可以利用官方扩展包golang.org/x/net/icmp,专门进行icmp packet的发送和接收,不过,有时候我们想进行更低层次的网络通讯,这个时候我们就需要借助一些额外的库,或者做一些额外的设置,当前相关的介绍IP层packet收发技术并没有很好的组织和介绍,本文尝试介绍几种收发IP packet的方式。

依然,我们介绍IPv4相关的技术, IPv6会单独一章进进行介绍。

在进行Go网络编程的时候,对于技术的选择,针对常用的场景,我个人有一点点小小的建议:如果标准库能够提供相关的功能,那么就使用标准库;否则再考察官方扩展库golang.org/x/net是否能够满足需求;如果还不合适,那么就考虑使用syscall.Socketgopackete;如果还不满足,再考察有没有第三方库已经实现了相关的功能。当然有时候最后两个考量可能互换一下位置也可以。

为什么有时候我们需要收发IP packet呢?因为我们有时候想进行对IPv4 header进行详细的设置或者检查。如下面的IPv4 header的定义:

ipv4-packet.png

有时候我们想设置TOS、Identification、TTL、Options,我们就必须能够自己组装IPv4 packet,并能够发送出去;读取亦然。

使用标准库

使用 net.ListenPacket/net.ListenPacket 探索

标准库提供了一种读写IP packet的方法,可以实现一半的读写的能力,它是通过func ListenPacket(network, address string) (PacketConn, error)函数实现,其中network可以是udpudp4udp6unixgram,或者是ip:1ip:icmp这样的ip加protocol号或者protocol名称的方式。protocol的定义在 http://www.iana.org/assignments/protocol-numbers 文档中(你也可以在Linux主机的 /etc/protocols 中读取到,只不过可能不是最新的), 比如ICMP的协议号是1,TCP的协议号是6, UDP的协议号是17,协议号253、254用来测试等。

如果network是udpudp4udp6,返回的PacketConn底层是*net.UDPConn,如果network是以ip为前缀,那么返回的PacketConn是*net.IPConn,在这种情况下,你可以使用明确的func ListenIP(network string, laddr *IPAddr) (*IPConn, error)

下面是一个使用net.ListenPacket的客户端的例子:

func main() {
conn, err := net.ListenPacket("ip4:udp", "127.0.0.1") // 本地地址
if err != nil {
fmt.Println("DialIP failed:", err)
return
data, _ := encodeUDPPacket("127.0.0.1", "192.168.0.1", []byte("hello world")) // 生成一个UDP包
if _, err := conn.WriteTo(data, &net.IPAddr{IP: net.ParseIP("192.168.0.1")}); err != nil {
panic(err)
buf := make([]byte, 1024)
n, peer, err := conn.ReadFrom(buf)
if err != nil {
panic(err)
fmt.Printf("received response from %s: %s\n", peer.String(), buf[8:n])

这个例子一开始产生一个PacketConn,实际是一个*net.IPConn, 但是需要注意的是,这里的conn发送的是 UDP层的包,并不包含IP层,下面这个例子定义了IP层,只是用来计算checksum,实际并没有用途:

func encodeUDPPacket(localIP, dstIP string, payload []byte) ([]byte, error) {
ip := &layers.IPv4{
SrcIP: net.ParseIP(localIP),
DstIP: net.ParseIP(dstIP),
Version: 4,
Protocol: layers.IPProtocolUDP,
udp := &layers.UDP{
SrcPort: layers.UDPPort(0),
DstPort: layers.UDPPort(8972),
udp.SetNetworkLayerForChecksum(ip)
buf := gopacket.NewSerializeBuffer()
opts := gopacket.SerializeOptions{
ComputeChecksums: true,
FixLengths: true,
err := gopacket.SerializeLayers(buf, opts, udp, gopacket.Payload(payload))
return buf.Bytes(), err

同样的,服务端读取到消息后,也是只返回IPv4 header下层的protocol层数据,IPv header数据被剥除掉了:

func main() {
conn, err := net.ListenPacket("ip4:udp", "192.168.0.1")
if err != nil {
panic(err)
buf := make([]byte, 1024)
n, peer, err := conn.ReadFrom(buf)
if err != nil {
panic(err)
fmt.Printf("received request from %s: %s\n", peer.String(), buf[8:n])
data, _ := encodeUDPPacket("192.168.0.1", "127.0.0.1", []byte("hello world"))
_, err = conn.WriteTo(data, &net.IPAddr{IP: net.ParseIP("127.0.0.1")})
if err != nil {
panic(err)

注意这里conn.ReadFrom(buf)读取到的数据包含UDP header,但是不包含IP header, UDP的header是8个字节,所以buf[8:n]就是payload的数据。

如果你看go标准库的源码,你可以看到Go收到IP packet后,会调用stripIPv4Header剥去IPv4 header:

func (c *IPConn) readFrom(b []byte) (int, *IPAddr, error) {
var addr *IPAddr
n, sa, err := c.fd.readFrom(b)
switch sa := sa.(type) {
case *syscall.SockaddrInet4:
addr = &IPAddr{IP: sa.Addr[0:]}
n = stripIPv4Header(n, b)
case *syscall.SockaddrInet6:
addr = &IPAddr{IP: sa.Addr[0:], Zone: zoneCache.name(int(sa.ZoneId))}
return n, addr, err
func stripIPv4Header(n int, b []byte) int {
if len(b) < 20 {
return n
l := int(b[0]&0x0f) << 2
if 20 > l || l > len(b) {
return n
if b[0]>>4 != 4 {
return n
copy(b, b[l:])
return n - l

使用 ipv4.RawConn 收发IP packet

最简单的方式,是使用ipv4.NewRawConn(conn)net.PacketConn转换成*ipv4.RawConn,如下面的客户端代码:

func main() {
conn, err := net.ListenPacket("ip4:udp", "127.0.0.1")
if err != nil {
fmt.Println("DialIP failed:", err)
return
rc, err := ipv4.NewRawConn(conn)
if err != nil {
panic(err)
data, _ := encodeUDPPacket("127.0.0.1", "192.168.0.1", []byte("hello world"))
if _, err := rc.WriteToIP(data, &net.IPAddr{IP: net.ParseIP("192.168.0.1")}); err != nil {
panic(err)
rbuf := make([]byte, 1024)
_, payload, _, err := rc.ReadFrom(rbuf)
if err != nil {
panic(err)
fmt.Printf("received response: %s\n", payload[8:])

注意这里的encodeUDPPacket实现和上面的例子中的实现就不一样了,它包含ip header的数据:

func encodeUDPPacket(localIP, dstIP string, payload []byte) ([]byte, error) {
ip := &layers.IPv4{
udp := &layers.UDP{
udp.SetNetworkLayerForChecksum(ip)
buf := gopacket.NewSerializeBuffer()
opts := gopacket.SerializeOptions{
ComputeChecksums: true,
FixLengths: true,
err := gopacket.SerializeLayers(buf, opts, ip, udp, gopacket.Payload(payload)) // 注意这里包含ip
return buf.Bytes(), err

读取数据的时候,ReadFrom会读取ip header、ip payload (UDP packet)、control message (UDP没有control message),所以我们也可以读取和分析返回的IP header。

使用 SyscallConn 实现读取IP header

使用标准库的(*net.IPConn).SyscallConn() 可以实现写数据时发送UDP(或者其他ip protocol)包的数据,但是在读取数据的时候,把IPv4 header读取出来。

func main() {
conn, err := net.ListenPacket("ip4:udp", "127.0.0.1")
if err != nil {
fmt.Println("DialIP failed:", err)
return
sc, err := conn.(*net.IPConn).SyscallConn()
if err != nil {
panic(err)
var addr syscall.SockaddrInet4
copy(addr.Addr[:], net.ParseIP("192.168.0.1").To4())
addr.Port = 8972
data, _ := encodeUDPPacket("127.0.0.1", "192.168.0.1", []byte("hello world"))
err = sc.Write(func(fd uintptr) bool {
// 将 UDP 数据包写入 Socket
err := syscall.Sendto(int(fd), data, 0, &addr)
if err != nil {
panic(err)
return err == nil
if err != nil {
panic(err)
var n int
buf := make([]byte, 1024)
err = sc.Read(func(fd uintptr) bool {
var err error
n, err = syscall.Read(int(fd), buf)
if err != nil {
return false
return true
if err != nil {
panic(err)
iph, err := ipv4.ParseHeader(buf[:n])
if err != nil {
panic(err)
fmt.Printf("received response from %s: %s\n", iph.Src.String(), buf[ipv4.HeaderLen+8:])

为什么发送的时候没有办法设置IPv4 header, 读取的时候却能读取到IPv4 header呢?这和底层使用的Socket有关,注意我们标准库在针对IPConn的建立时,使用的是syscall.AF_INET和syscall.SOCK_RAW, protocol创建的socket,默认情况下我们值需要填写ip payload数据(protocol的数据),内核协议栈会自动生成IP header,但是读取时会把ip header读取返回,所以Go的行为和Socket一致,标准库为了读写一致,读取出来的数据还把IPv4 header给剥除掉了。

那么为啥ipv4.RawConn能够发送IPv4 header的数据呢?这是因为它对Socket进行了设置:

func NewRawConn(c net.PacketConn) (*RawConn, error) {
so, ok := sockOpts[ssoHeaderPrepend]
return r, nil

ssoHeaderPrepend 选项就是设置IP_HDRINCL:

ssoHeaderPrepend: {Option: socket.Option{Level: iana.ProtocolIP, Name: unix.IP_HDRINCL, Len: 4}},

所以即使你不使用ipv4.RawConn,你也可以针对标准库的*net.IPConn进行设置,让它支持可以手工写IPv4 header:

err = syscall.SetsockoptInt(fd, syscall.IPPROTO_IP, syscall.IP_HDRINCL, 1)

当然为了同时支持读写IPv4 header,还是转换成*ipv4.RawConn最方便。

使用 syscall.Socket 收发 IP packet

最简单的,类似C等其他语言访问系统调用,我们可以实现收发IPv4 packet。有时候你在开发网络程序时,一点都不用担心技术上的障碍,大不了我们使用最原始的系统调用来实现网络通讯。

下面这个例子建立了一个Socket,这里protocol我们没有使用UDP,其实你可以改造成UDP代码。

注意我们需要设置IP_HDRINCL为1,我们手工设置IPv4 header,而不是让内核协议栈帮我们设置。

func main() {
fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_RAW, syscall.IPPROTO_RAW)
if err != nil {
fmt.Println("socket failed:", err)
return
defer syscall.Close(fd)
err = syscall.SetsockoptInt(fd, syscall.IPPROTO_IP, syscall.IP_HDRINCL, 1)
if err != nil {
panic(err)
// 本地地址
addr := syscall.SockaddrInet4{Addr: [4]byte{127, 0, 0, 1}}
// 发送自定义协议数据包
ip4 := &layers.IPv4{
SrcIP: net.ParseIP("127.0.0.1"),
DstIP: net.ParseIP("192.168.0.1"),
Version: 4,
TTL: 64,
Protocol: syscall.IPPROTO_RAW,
pbuf := gopacket.NewSerializeBuffer()
opts := gopacket.SerializeOptions{
ComputeChecksums: true,
FixLengths: true,
payload := []byte("hello world")
err = gopacket.SerializeLayers(pbuf, opts, ip4, gopacket.Payload(payload))
if err != nil {
fmt.Println("serialize failed:", err)
return
if err := syscall.Sendto(fd, pbuf.Bytes(), 0, &addr); err != nil {
fmt.Println("sendto failed:", err)
return
buf := make([]byte, 1024)
n, peer, err := syscall.Recvfrom(fd, buf, 0)
if err != nil {
fmt.Println("recvfrom failed:", err)
return
raddr := net.IP(peer.(*syscall.SockaddrInet4).Addr[:]).String()
if raddr != "192.168.0.1" {
continue
iph, err := ipv4.ParseHeader(buf[:n])
if err != nil {
fmt.Println("parse ipv4 header failed:", err)
return
fmt.Printf("received response from %s: %s\n", raddr, string(buf[iph.Len:n]))
break
func htons(i uint16) uint16 {
return (i<<8)&0xff00 | i>>8

而服务器端代码如下,注意这里我们为了值关注我们程序自己的数据包,使用了bpf做了filter筛选,会提高性能:

func main() {
fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_RAW, syscall.IPPROTO_RAW)
if err != nil {
fmt.Println("socket failed:", err)
return
defer syscall.Close(fd)
err = syscall.SetsockoptInt(fd, syscall.IPPROTO_IP, syscall.IP_HDRINCL, 1)
if err != nil {
panic(err)
filter.applyTo(fd)
// 接收自定义协议数据包
buf := make([]byte, 1024)
n, peer, err := syscall.Recvfrom(fd, buf, 0)
if err != nil {
fmt.Println("recvfrom failed:", err)
return
iph, err := ipv4.ParseHeader(buf[:n])
if err != nil {
fmt.Println("parse header failed:", err)
return
if string(buf[iph.Len:n]) != "hello world" {
continue
fmt.Printf("received request from %s: %s\n", iph.Src.String(), string(buf[iph.Len:n]))
iph.Src, iph.Dst = iph.Dst, iph.Src
replayIpHeader, _ := iph.Marshal()
copy(buf[:iph.Len], replayIpHeader)
if err := syscall.Sendto(fd, buf[:n], 0, peer); err != nil {
fmt.Println("sendto failed:", err)
return

当然,还可以使用第三方的库比如gopacket收发IPv4的包,只不过*ipv4.RawConn已经足够我们使用了,没必要再使用第三方的库了,这里我们就不多做介绍了。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK