幾種使用 Go 發送 IP 包的方法

我們使用 Go 標準庫中的net包,很容易發送 UDP 和 TCP 的 packet,以及在它們基礎上開發應用層的程序,比如 HTTP、RPC 等框架和程序,甚至我們可以利用官方擴展包golang.oef/x/net/icmp, 專門進行 icmp packet 的發送和接收,不過,有時候我們想進行更低層次的網絡通訊,這個時候我們就需要藉助一些額外的庫,或者做一些額外的設置,當前相關的介紹 IP 層 packet 收發技術並沒有很好的組織和介紹,本文嘗試介紹幾種收發 IP packet 的方式。

依然,我們介紹 IPv4 相關的技術, IPv6 會單獨一章進行介紹。

在進行 Go 網絡編程的時候,對於技術的選擇,針對常用的場景,我個人有一點點小小的建議:如果標準庫能夠提供相關的功能,那麼就使用標準庫;否則再考察官方擴展庫golang.org/x/net是否能夠滿足需求;如果還不合適,那麼就考慮使用syscall.Socketgopackete;如果還不滿足,再考察有沒有第三方庫已經實現了相關的功能。當然有時候最後兩個考量可能互換一下位置也可以。

爲什麼有時候我們需要收發 IP packet 呢?因爲我們有時候想進行對 IPv4 header 進行詳細的設置或者檢查。如下面的 IPv4 header 的定義:

有時候我們想設置 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)
 for {
  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([]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)
 for {
  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)
 for {

  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已經足夠我們使用了,沒必要再使用第三方的庫了,這裏我們就不多做介紹了。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/1Nv5_b2jM9uiG0tEu1Pr1A