1. 介紹
接上次的博客,按照約定的划分,還有一層鏈路層socket。這一層就可以自定義鏈路層的協議頭部(header)了,下面是目前主流的Ethernet 2(以太網)標准的頭部:
相比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協議頭部。包含關系如圖:
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