Go 如何使用原始套接字捕獲網(wǎng)卡流量
Go 捕獲網(wǎng)卡流量使用最多的庫為 github.com/google/gopacket,需要依賴 libpcap 導(dǎo)致必須開啟 CGO 才能夠進行編譯。
為了減少對環(huán)境的依賴可以使用原始套接字捕獲網(wǎng)卡流量,然后使用 gopacket 的協(xié)議解析功能,這樣就省去了解析這部分的工作量,正確性也可以得到保證,同時 CGO 也可以關(guān)閉。
cilium 里有一個原始套接字打開的測試用例:
// 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])) }
但是這個示例有一個問題,只能拿到本機流量。
捕獲經(jīng)過網(wǎng)橋的非本機流量
通過 tcpdump 是可以抓到經(jīng)過網(wǎng)橋的轉(zhuǎn)發(fā)流量的,我們使用 strace 對 tcpdump 進行跟蹤分析
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
看到有一個 setsockopt(PACKET_MR_PROMISC) 設(shè)置,看起來是開啟的混雜模式,查看資料看到這是一個針對套接字級別的混雜模式。
由于之前看過 suricata 的代碼,看看它是怎么做的,直接在 suricata 的倉庫里面搜索 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 實現(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 的話,需要設(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 的實現(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 實現(xiàn)
完整的實現(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) 簡單地對 fd 進行監(jiān)聽,使用 recvmsg(2) 來讀取數(shù)據(jù),包括 VLAN tag。
實現(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é)
以上代碼都在實際的場景中使用,只是稍微修改了一點細節(jié)以及使用 epoll(2) 來監(jiān)聽,結(jié)合 sync.Pool 和精簡了解析邏輯,性能尚可能夠滿足要求。
參考
- https://github.com/OISF/suricata/blob/ce727cf4b1ccbac1679272f14cbfa529bc23ebc6/src/source-af-packet.c#L1926,suricata 捕獲網(wǎng)卡流量的實現(xiàn)
- https://man7.org/linux/man-pages/man7/packet.7.html, packet 文檔
到此這篇關(guān)于Go 使用原始套接字捕獲網(wǎng)卡流量的文章就介紹到這了,更多相關(guān)Go原始套接字內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
更換GORM默認SQLite驅(qū)動出現(xiàn)的問題解決分析
這篇文章主要為大家介紹了更換GORM默認SQLite驅(qū)動出現(xiàn)的問題解決分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2024-01-01一文搞懂Golang 時間和日期相關(guān)函數(shù)
這篇文章主要介紹了Golang 時間和日期相關(guān)函數(shù),本文通過實例代碼給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-12-12Go內(nèi)存分配之結(jié)構(gòu)體優(yōu)化技巧
這篇文章主要為大家詳細介紹了Go語言內(nèi)存分配之結(jié)構(gòu)體優(yōu)化技巧的相關(guān)知識,文中的示例代碼講解詳細,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2023-11-11Goland調(diào)節(jié)字體大小的設(shè)置(編輯區(qū),terminal區(qū),頁面字體)
這篇文章主要介紹了Goland調(diào)節(jié)字體大小的設(shè)置(編輯區(qū),terminal區(qū),頁面字體),具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-12-12