Go中鏈路層套接字的實踐

1. 介紹

接上次的博客,按照約定的劃分,還有一層鏈路層socket。這一層就能夠自定義鏈路層的協議頭部(header)了,下面是目前主流的Ethernet 2(以太網)標準的頭部:
Ethernet_Type_II_Frame_format.png
相比IP和TCP的頭部,以太網的頭部要簡單些,僅有目標MAC地址,源MAC地址,數據協議類型(好比常見的IP和ARP協議)。html

但多了尾部的FCS(幀校驗序列),用的是CRC校驗法。若是校驗錯誤,直接丟棄掉,不會送到上層的協議棧中,鏈路層只保證數據幀的正確性(丟掉錯誤的)。具體數據報的完整性由上層控制,好比TCP重傳。
鏈路層最大長度是1518字節,除去18字節的頭部和尾部,只剩1500字節,也就是MTU(最大傳輸單元)的由來,並約定最小傳輸長度64字節。linux

2. 服務端

ifonfig 查看本機的網絡設備(網卡):git

eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 172.17.0.2  netmask 255.255.0.0  broadcast 172.17.255.255
        ether 02:42:ac:11:00:02  txqueuelen 0  (Ethernet)

經過Go提供的net拿到網絡接口設備的詳細信息,eth0是上面的網絡設備名字:github

ifi, err := net.InterfaceByName("eth0")
util.CheckError(err)

而後使用原始套接字綁定到該網絡設備上:算法

fd, err := syscall.Socket(syscall.AF_PACKET, syscall.SOCK_RAW, int(wire.Htons(0x800)))

AF_PACKET是Linux 2.2加入的功能,能夠在網絡設備上接收發送數據包。其第二個參數 SOCK_RAW 表示帶有鏈路層的頭部,還有個可選值 SOCK_DGRAM 會移除掉頭部。第三個則對應頭部中協議類型(ehter type),好比只接收 IP 協議的數據,也能夠接收全部的。可在Linux中if_ether文件查看相應的值。好比:網絡

#define ETH_P_IP    0x0800      /* Internet Protocol packet 
#define ETH_P_IPV6  0x86DD      /* IPv6 over bluebook       */
#define ETH_P_SNAP  0x0005      /* Internal only        */

Htons函數是把網絡字節序轉成當前機器字節序。這裏已經拿到鏈路層socket的鏈接句柄,下一步就能夠監聽該句柄的數據:socket

for {
    buf := make([]byte, 1514)
    n, _, _ := syscall.Recvfrom(fd, buf, 0)
    header := wire.ParseHeader(buf[0:14])
    fmt.Println(header)
}

這時候全部到這機器上的IP協議流量都能監聽到,無論UDP,TCP,ICMP等上層協議。啓動程序,嘗試在另外臺機器ping下,獲得:函數

root@4b56d41e5168:/ethernet# go run main.go
[2018-07-16T00:32:32.215Z] INFO 02:42:ac:11:00:02
DestinationAddress: 02:42:ac:11:00:02 SourceAddress: 02:42:ac:11:00:03 EtherType: ipv4

另外臺機器:性能

root@3348477f42e8:/# ping 172.17.0.2
PING 172.17.0.2 (172.17.0.2) 56(84) bytes of data.
64 bytes from 172.17.0.2: icmp_seq=1 ttl=64 time=0.202 ms

3. 協議頭部

上面例子代碼中,定義了1514的字節slice來接收一次以太網的數據,而後取出前14個字節來解析頭部。協議尾部的4字節不須要處理,在發送數據的時候由網絡設備並添加,接收的時候由設備校驗並去除。在之前的有些計算機中,是須要本身添加或移除尾部的,後面可介紹下該校驗算法。 ParseHeader解析頭部也很簡單,前6個字節是目標Mac地址,中間6字節是源Mac地址,後2字節是協議類型:ui

func ParseHeader(buf []byte) *Header {
    header := new(Header)
    var hd net.HardwareAddr
    hd = buf[0:6]
    header.DestinationAddress = hd
    hd = buf[6:12]
    header.SourceAddress = hd
    header.EtherType = binary.BigEndian.Uint16(buf[12:14])
    return header
}

ping使用的是ICMP協議,和TCP/UDP同級,因此根據接收到的數據繼續解IP協議頭部,ICMP協議頭部。包含關係如圖:
ICMP-datagram-transmission.jpg

Go官方有相應的庫能夠解析:

ip4header, _ := ipv4.ParseHeader(buf[14:34])
fmt.Println("ipv4 header: ", ip4header)
icmpPayload := buf[34:]
msg, _ := icmp.ParseMessage(1, icmpPayload)
fmt.Println("icmp: ", msg)

IP頭部20字節,ICMP頭部8個字節,輸出以下:

root@4b56d41e5168://ethernet# go run main.go
[2018-07-16T00:36:03.033Z] INFO 02:42:ac:11:00:02
DestinationAddress: 02:42:ac:11:00:02 SourceAddress: 02:42:ac:11:00:03 EtherType: ipv4
ipv4 header:  ver=4 hdrlen=20 tos=0x0 totallen=84 id=0x97ab flags=0x2 fragoff=0x0 ttl=64 proto=1 cksum=0x4ad6 src=172.17.0.3 dst=172.17.0.2
icmp:  &{echo 0 12964 0xc4200807e0}

4. 客戶端

上面代碼是服務端解析以太網協議頭部,也能夠自定義發送時頭部:
創建socket句柄:

var ohter = net.HardwareAddr{0x02, 0x42, 0xac, 0x11, 0x00, 0x02}
var etherType uint16 = 52428
fd, err := syscall.Socket(syscall.AF_PACKET, syscall.SOCK_RAW, int(wire.Htons(etherType)))

構建以太網頭部,而後發送監聽的機器上:

for {
        payload := []byte("msg")
        minPayload := len(payload)
        if minPayload < 46 {
            minPayload = 46
        }
        b := make([]byte, 14+minPayload)
        header := &wire.Header{
            DestinationAddress: broadcast,
            SourceAddress:      ifi.HardwareAddr,
            EtherType:          etherType,
        }
        copy(b[0:14], header.Marshal())
        copy(b[14:14+len(payload)], payload)

        var baddr [8]byte
        copy(baddr[:], broadcast)
        to := &syscall.SockaddrLinklayer{
            Ifindex:  ifi.Index,
            Halen:    6,
            Addr:     baddr,
            Protocol: wire.Htons(etherType),
        }
        err = syscall.Sendto(fd, b, 0, to)
        util.CheckError(err)
        time.Sleep(time.Second)
    }
}

監聽端輸出:

root@4b56d41e5168:/ethernet# go run main.go
[2018-07-16T15:25:46.745Z] INFO 02:42:ac:11:00:02
DestinationAddress: 02:42:ac:11:00:02 SourceAddress: 02:42:ac:11:00:03 EtherType: unknow52428
DestinationAddress: 02:42:ac:11:00:02 SourceAddress: 02:42:ac:11:00:03 EtherType: unknow52428

5. 總結

基於此就能夠抓取數據鏈路層的流量,而後對流量進行深刻分析等。還有一種方式是基於packet_mmap的共享內存抓包方式,性能更好些。文中例子代碼在examples,參考: https://github.com/spotify/linux/blob/master/include/linux/if_ether.h http://man7.org/linux/man-pages/man7/packet.7.html

相關文章
相關標籤/搜索