Go中鏈路層套接字的實踐


1. 介紹

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

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

2. 服務端

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

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是上面的網絡設備名字:

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的連接句柄,下一步就可以監聽該句柄的數據:

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字節是協議類型:

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM