幾種使用 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.Socket
和gopackete
;如果還不滿足,再考察有沒有第三方庫已經實現了相關的功能。當然有時候最後兩個考量可能互換一下位置也可以。
爲什麼有時候我們需要收發 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 可以是udp
、udp4
、udp6
、unixgram
, 或者是ip:1
、ip:icmp
這樣的 ip 加 protocol 號或者 protocol 名稱的方式。protocol 的定義在 http://www.iana.org/assignments/protocol-numbers 文檔中 (你也可以在 Linux 主機的 /etc/protocols 中讀取到,只不過可能不是最新的), 比如 ICMP 的協議號是 1,TCP 的協議號是 6, UDP 的協議號是 17, 協議號 253、254 用來測試等。
如果 network 是udp
、udp4
、udp6
,返回的 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(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)
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