Go 如何使用原始套接字捕獲網(wǎng)卡流量
Go 捕獲網(wǎng)卡流量使用最多的庫(kù)為 github.com/google/gopacket,需要依賴(lài) libpcap 導(dǎo)致必須開(kāi)啟 CGO 才能夠進(jìn)行編譯。
為了減少對(duì)環(huán)境的依賴(lài)可以使用原始套接字捕獲網(wǎng)卡流量,然后使用 gopacket 的協(xié)議解析功能,這樣就省去了解析這部分的工作量,正確性也可以得到保證,同時(shí) CGO 也可以關(guān)閉。
cilium 里有一個(gè)原始套接字打開(kāi)的測(cè)試用例:
// Both openRawSock and htons are available in
// https://github.com/cilium/ebpf/blob/master/example_sock_elf_test.go.
// MIT license.
func OpenRawSocket(index int) (int, error) {
sock, err := syscall.Socket(syscall.AF_PACKET, syscall.SOCK_RAW|syscall.SOCK_NONBLOCK|syscall.SOCK_CLOEXEC, int(htons(syscall.ETH_P_ALL)))
if err != nil {
return 0, err
}
sll := syscall.SockaddrLinklayer{Ifindex: index, Protocol: htons(syscall.ETH_P_ALL)}
if err := syscall.Bind(sock, &sll); err != nil {
syscall.Close(sock)
return 0, err
}
return sock, nil
}
// htons converts the unsigned short integer hostshort from host byte order to network byte order.
func htons(i uint16) uint16 {
b := make([]byte, 2)
binary.BigEndian.PutUint16(b, i)
return *(*uint16)(unsafe.Pointer(&b[0]))
}但是這個(gè)示例有一個(gè)問(wèn)題,只能拿到本機(jī)流量。
捕獲經(jīng)過(guò)網(wǎng)橋的非本機(jī)流量
通過(guò) tcpdump 是可以抓到經(jīng)過(guò)網(wǎng)橋的轉(zhuǎn)發(fā)流量的,我們使用 strace 對(duì) tcpdump 進(jìn)行跟蹤分析
root@localhost:~# strace -f tcpdump -i b_2_0 arp -nne
...
socket(AF_PACKET, SOCK_RAW, htons(0 /* ETH_P_??? */)) = 4
ioctl(4, SIOCGIFINDEX, {ifr_name="lo", ifr_ifindex=1}) = 0
ioctl(4, SIOCGIFHWADDR, {ifr_name="b_2_0", ifr_hwaddr={sa_family=ARPHRD_ETHER, sa_data=4e:59:d6:32:f6:42}}) = 0
newfstatat(AT_FDCWD, "/sys/class/net/b_2_0/wireless", 0x7ffdf063bc50, 0) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/sys/class/net/b_2_0/dsa/tagging", O_RDONLY) = -1 ENOENT (No such file or directory)
ioctl(4, SIOCGIFINDEX, {ifr_name="b_2_0", ifr_ifindex=6053}) = 0
bind(4, {sa_family=AF_PACKET, sll_protocol=htons(0 /* ETH_P_??? */), sll_ifindex=if_nametoindex("b_2_0"), sll_hatype=ARPHRD_NETROM, sll_pkttype=PACKET_HOST, sll_halen=0}, 20) = 0
getsockopt(4, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
setsockopt(4, SOL_PACKET, PACKET_ADD_MEMBERSHIP, {mr_ifindex=if_nametoindex("b_2_0"), mr_type=PACKET_MR_PROMISC, mr_alen=0, mr_address=4e:59:d6:32:f6:42}, 16) = 0
getsockopt(4, SOL_SOCKET, SO_BPF_EXTENSIONS, [64], [4]) = 0
mmap(NULL, 266240, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fec47cbe000
看到有一個(gè) setsockopt(PACKET_MR_PROMISC) 設(shè)置,看起來(lái)是開(kāi)啟的混雜模式,查看資料看到這是一個(gè)針對(duì)套接字級(jí)別的混雜模式。
由于之前看過(guò) suricata 的代碼,看看它是怎么做的,直接在 suricata 的倉(cāng)庫(kù)里面搜索 PACKET_MR_PROMISC 關(guān)鍵字,出現(xiàn)代碼
memset(&sock_params, 0, sizeof(sock_params));
sock_params.mr_type = PACKET_MR_PROMISC;
sock_params.mr_ifindex = bind_address.sll_ifindex;
r = setsockopt(ptv->socket, SOL_PACKET, PACKET_ADD_MEMBERSHIP,(void *)&sock_params, sizeof(sock_params));
if (r < 0) {
SCLogError("%s: failed to set promisc mode: %s", devname, strerror(errno));
goto socket_err;
}套接字設(shè)置混雜模式的 Go 實(shí)現(xiàn)如下
// Set socket level PROMISC mode
err = unix.SetsockoptPacketMreq(sock, syscall.SOL_PACKET, syscall.PACKET_ADD_MEMBERSHIP, &unix.PacketMreq{Type: unix.PACKET_MR_PROMISC, Ifindex: int32(index)})
if err != nil {
syscall.Close(sock)
return 0, err
}捕獲 VLAN 流量
目前只能拿到普通的以太網(wǎng)流量,如果還需要拿到 VLAN Id 的話(huà),需要設(shè)置 PACKET_AUXDATA,參考 man packet
PACKET_AUXDATA (since Linux 2.6.21)
If this binary option is enabled, the packet socket passes
a metadata structure along with each packet in the
recvmsg(2) control field. The structure can be read with
cmsg(3). It is defined as
struct tpacket_auxdata {
__u32 tp_status;
__u32 tp_len; /* packet length */
__u32 tp_snaplen; /* captured length */
__u16 tp_mac;
__u16 tp_net;
__u16 tp_vlan_tci;
__u16 tp_vlan_tpid; /* Since Linux 3.14; earlier, these
were unused padding bytes */
};Go 的實(shí)現(xiàn)如下
// Enable PACKET_AUXDATA option for VLAN
if err := syscall.SetsockoptInt(sock, syscall.SOL_PACKET, unix.PACKET_AUXDATA, 1); err != nil {
syscall.Close(sock)
return 0, err
}完整的 OpenRawSocket 實(shí)現(xiàn)
完整的實(shí)現(xiàn)如下
func OpenRawSocket(index int) (int, error) {
sock, err := syscall.Socket(syscall.AF_PACKET, syscall.SOCK_RAW|syscall.SOCK_NONBLOCK|syscall.SOCK_CLOEXEC, int(htons(syscall.ETH_P_ALL)))
if err != nil {
return 0, err
}
// Enable PACKET_AUXDATA option for VLAN
if err := syscall.SetsockoptInt(sock, syscall.SOL_PACKET, unix.PACKET_AUXDATA, 1); err != nil {
syscall.Close(sock)
return 0, err
}
// Set socket level PROMISC mode
err = unix.SetsockoptPacketMreq(sock, syscall.SOL_PACKET, syscall.PACKET_ADD_MEMBERSHIP, &unix.PacketMreq{Type: unix.PACKET_MR_PROMISC, Ifindex: int32(index)})
if err != nil {
syscall.Close(sock)
return 0, err
}
sll := syscall.SockaddrLinklayer{Ifindex: index, Protocol: htons(syscall.ETH_P_ALL)}
if err := syscall.Bind(sock, &sll); err != nil {
syscall.Close(sock)
return 0, err
}
return sock, nil
}從 fd 中讀取數(shù)據(jù)
這里使用 select(2) 簡(jiǎn)單地對(duì) fd 進(jìn)行監(jiān)聽(tīng),使用 recvmsg(2) 來(lái)讀取數(shù)據(jù),包括 VLAN tag。
實(shí)現(xiàn)如下
package pcap
import (
"context"
"syscall"
)
func FD_SET(fd int, p *syscall.FdSet) { p.Bits[fd/64] |= 1 << (uint(fd) % 64) }
func FD_CLR(fd int, p *syscall.FdSet) { p.Bits[fd/64] &^= 1 << (uint(fd) % 64) }
func FD_ISSET(fd int, p *syscall.FdSet) bool { return p.Bits[fd/64]&(1<<(uint(fd)%64)) != 0 }
func FD_ZERO(p *syscall.FdSet) {
for i := range p.Bits {
p.Bits[i] = 0
}
}
type RecvmsgHandler func(buf []byte, n int, oob []byte, oobn int, err error) error
func RecvmsgLoop(ctx context.Context, sockfd int, fn RecvmsgHandler) error {
buf := make([]byte, 1024*64)
oob := make([]byte, syscall.CmsgSpace(1024))
readfds := syscall.FdSet{}
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
FD_ZERO(&readfds)
FD_SET(sockfd, &readfds)
tv := syscall.Timeval{Sec: 0, Usec: 100000} // 100ms
nfds, err := syscall.Select(sockfd+1, &readfds, nil, nil, &tv)
if err != nil {
continue
}
if nfds > 0 && FD_ISSET(sockfd, &readfds) {
n, oobn, _, _, err := syscall.Recvmsg(sockfd, buf, oob, 0)
err = fn(buf, n, oob, oobn, err)
if err != nil {
return err
}
}
}
}VLAN 數(shù)據(jù)的解析邏輯如下
func decodeVlanIdByAuxData(oob []byte) (uint16, error) {
msgs, err := syscall.ParseSocketControlMessage(oob)
if err != nil {
return 0, err
}
for _, m := range msgs {
if m.Header.Level == syscall.SOL_PACKET && m.Header.Type == 8 && len(m.Data) >= 20 {
auxdata := unix.TpacketAuxdata{
Status: binary.LittleEndian.Uint32(m.Data[0:4]),
Vlan_tci: binary.LittleEndian.Uint16(m.Data[16:18]),
}
if auxdata.Status&unix.TP_STATUS_VLAN_VALID != 0 {
return auxdata.Vlan_tci, nil
}
}
}
return 0, nil
}總結(jié)
以上代碼都在實(shí)際的場(chǎng)景中使用,只是稍微修改了一點(diǎn)細(xì)節(jié)以及使用 epoll(2) 來(lái)監(jiān)聽(tīng),結(jié)合 sync.Pool 和精簡(jiǎn)了解析邏輯,性能尚可能夠滿(mǎn)足要求。
參考
- https://github.com/OISF/suricata/blob/ce727cf4b1ccbac1679272f14cbfa529bc23ebc6/src/source-af-packet.c#L1926,suricata 捕獲網(wǎng)卡流量的實(shí)現(xiàn)
- https://man7.org/linux/man-pages/man7/packet.7.html, packet 文檔
到此這篇關(guān)于Go 使用原始套接字捕獲網(wǎng)卡流量的文章就介紹到這了,更多相關(guān)Go原始套接字內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
更換GORM默認(rèn)SQLite驅(qū)動(dòng)出現(xiàn)的問(wèn)題解決分析
這篇文章主要為大家介紹了更換GORM默認(rèn)SQLite驅(qū)動(dòng)出現(xiàn)的問(wèn)題解決分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2024-01-01
一文搞懂Golang 時(shí)間和日期相關(guān)函數(shù)
這篇文章主要介紹了Golang 時(shí)間和日期相關(guān)函數(shù),本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-12-12
Go的固定時(shí)長(zhǎng)定時(shí)器和周期性時(shí)長(zhǎng)定時(shí)器
本文主要介紹了Go的固定時(shí)長(zhǎng)定時(shí)器和周期性時(shí)長(zhǎng)定時(shí)器,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-08-08
Go內(nèi)存分配之結(jié)構(gòu)體優(yōu)化技巧
這篇文章主要為大家詳細(xì)介紹了Go語(yǔ)言?xún)?nèi)存分配之結(jié)構(gòu)體優(yōu)化技巧的相關(guān)知識(shí),文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2023-11-11
Golang學(xué)習(xí)筆記(一):簡(jiǎn)介
這篇文章主要介紹了Golang學(xué)習(xí)筆記(一):簡(jiǎn)介,本文講解了Go語(yǔ)言最主要的特性、安裝、環(huán)境變量設(shè)置、整體目錄結(jié)構(gòu)、Helloworld、go命令、調(diào)試、編輯器設(shè)置等內(nèi)容,需要的朋友可以參考下2015-05-05
通過(guò)案例簡(jiǎn)單聊聊為什么說(shuō)Go中的字符串是不能被修改的
在接觸Go這么語(yǔ)言,可能你經(jīng)常會(huì)聽(tīng)到這樣一句話(huà),對(duì)于字符串不能修改,可能你很納悶,日常開(kāi)發(fā)中我們對(duì)字符串進(jìn)行修改也是很正常的,為什么又說(shuō)Go中的字符串不能進(jìn)行修改呢,本文就來(lái)通過(guò)實(shí)際案例給大家演示,為什么Go中的字符串不能進(jìn)行修改2023-07-07
Goland調(diào)節(jié)字體大小的設(shè)置(編輯區(qū),terminal區(qū),頁(yè)面字體)
這篇文章主要介紹了Goland調(diào)節(jié)字體大小的設(shè)置(編輯區(qū),terminal區(qū),頁(yè)面字體),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-12-12

