From 03f63b029bb45fb7e7e713a1d75e3d0606a73e4c Mon Sep 17 00:00:00 2001 From: WGH Date: Sat, 25 Sep 2021 22:37:59 +0300 Subject: [PATCH] Make pcapgo EthernetHandle use runtime poller As of now, the EthernetHandle fd is kept in the blocking mode. Which means when capturing traffic, entire OS thread is blocked in recvmsg syscall most of the time. It's possible to use custom fds (i.e. not created by net package) with the runtime poller, so using them will be as efficient as it is with e.g. TCP sockets. This commit does exactly that. --- pcapgo/capture.go | 111 +++++++++++++++++++++++++++++++++------------- 1 file changed, 81 insertions(+), 30 deletions(-) diff --git a/pcapgo/capture.go b/pcapgo/capture.go index a68e5416f..15559cdbc 100644 --- a/pcapgo/capture.go +++ b/pcapgo/capture.go @@ -10,7 +10,7 @@ package pcapgo import ( "fmt" "net" - "runtime" + "os" "sync" "syscall" "time" @@ -31,7 +31,7 @@ func htons(data uint16) uint16 { return data<<8 | data>>8 } // EthernetHandle holds shared buffers and file descriptor of af_packet socket type EthernetHandle struct { - fd int + file *os.File buffer []byte oob []byte ancil []interface{} @@ -62,8 +62,31 @@ func (h *EthernetHandle) readOne() (ci gopacket.CaptureInfo, vlan int, haveVlan msg.SetControllen(len(h.oob)) } - // use msg_trunc so we know packet size without auxdata, which might be missing - n, _, e := syscall.Syscall(unix.SYS_RECVMSG, uintptr(h.fd), uintptr(unsafe.Pointer(&msg)), uintptr(unix.MSG_TRUNC)) + rawConn, err := h.file.SyscallConn() + if err != nil { + return + } + + var ( + e syscall.Errno + n uintptr + ) + err = rawConn.Read(func(fd uintptr) bool { + // use msg_trunc so we know packet size without auxdata, which might be missing + n, _, e = syscall.Syscall(unix.SYS_RECVMSG, fd, uintptr(unsafe.Pointer(&msg)), uintptr(unix.MSG_TRUNC)) + switch e { + case 0: + return true + case syscall.EAGAIN: + return false + default: + // some other error + return true + } + }) + if err != nil { + return + } if e != 0 { return gopacket.CaptureInfo{}, 0, false, fmt.Errorf("couldn't read packet: %s", e) @@ -160,11 +183,7 @@ func (h *EthernetHandle) ZeroCopyReadPacketData() ([]byte, gopacket.CaptureInfo, // Close closes the underlying socket func (h *EthernetHandle) Close() { - if h.fd != -1 { - unix.Close(h.fd) - h.fd = -1 - runtime.SetFinalizer(h, nil) - } + h.file.Close() } // SetCaptureLength sets the maximum capture length to the given value @@ -183,22 +202,34 @@ func (h *EthernetHandle) GetCaptureLength() int { // SetBPF attaches the given BPF filter to the socket. After this, only the packets for which the filter returns a value greater than zero are received. // If a filter was already attached, it will be overwritten. To remove the filter, provide an empty slice. -func (h *EthernetHandle) SetBPF(filter []bpf.RawInstruction) error { - if len(filter) == 0 { - return unix.SetsockoptInt(h.fd, unix.SOL_SOCKET, unix.SO_DETACH_FILTER, 0) - } - f := make([]unix.SockFilter, len(filter)) - for i := range filter { - f[i].Code = filter[i].Op - f[i].Jf = filter[i].Jf - f[i].Jt = filter[i].Jt - f[i].K = filter[i].K +func (h *EthernetHandle) SetBPF(filter []bpf.RawInstruction) (err error) { + rawConn, err := h.file.SyscallConn() + if err != nil { + return } - fprog := &unix.SockFprog{ - Len: uint16(len(filter)), - Filter: &f[0], + + err2 := rawConn.Control(func(fd uintptr) { + if len(filter) == 0 { + err = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_DETACH_FILTER, 0) + return + } + f := make([]unix.SockFilter, len(filter)) + for i := range filter { + f[i].Code = filter[i].Op + f[i].Jf = filter[i].Jf + f[i].Jt = filter[i].Jt + f[i].K = filter[i].K + } + fprog := &unix.SockFprog{ + Len: uint16(len(filter)), + Filter: &f[0], + } + err = unix.SetsockoptSockFprog(int(fd), unix.SOL_SOCKET, unix.SO_ATTACH_FILTER, fprog) + }) + if err2 != nil { + return err2 } - return unix.SetsockoptSockFprog(h.fd, unix.SOL_SOCKET, unix.SO_ATTACH_FILTER, fprog) + return } // LocalAddr returns the local network address @@ -212,7 +243,12 @@ func (h *EthernetHandle) LocalAddr() net.HardwareAddr { } // SetPromiscuous sets promiscous mode to the required value. If it is enabled, traffic not destined for the interface will also be captured. -func (h *EthernetHandle) SetPromiscuous(b bool) error { +func (h *EthernetHandle) SetPromiscuous(b bool) (err error) { + rawConn, err := h.file.SyscallConn() + if err != nil { + return + } + mreq := unix.PacketMreq{ Ifindex: int32(h.intf), Type: unix.PACKET_MR_PROMISC, @@ -223,12 +259,28 @@ func (h *EthernetHandle) SetPromiscuous(b bool) error { opt = unix.PACKET_DROP_MEMBERSHIP } - return unix.SetsockoptPacketMreq(h.fd, unix.SOL_PACKET, opt, &mreq) + err2 := rawConn.Control(func(fd uintptr) { + err = unix.SetsockoptPacketMreq(int(fd), unix.SOL_PACKET, opt, &mreq) + }) + if err2 != nil { + return err2 + } + return } // Stats returns number of packets and dropped packets. This will be the number of packets/dropped packets since the last call to stats (not the cummulative sum!). -func (h *EthernetHandle) Stats() (*unix.TpacketStats, error) { - return unix.GetsockoptTpacketStats(h.fd, unix.SOL_PACKET, unix.PACKET_STATISTICS) +func (h *EthernetHandle) Stats() (res *unix.TpacketStats, err error) { + rawConn, err := h.file.SyscallConn() + if err != nil { + return + } + err2 := rawConn.Control(func(fd uintptr) { + res, err = unix.GetsockoptTpacketStats(int(fd), unix.SOL_PACKET, unix.PACKET_STATISTICS) + }) + if err2 != nil { + return nil, err2 + } + return } // NewEthernetHandle implements pcap.OpenLive for network devices. @@ -240,7 +292,7 @@ func NewEthernetHandle(ifname string) (*EthernetHandle, error) { return nil, fmt.Errorf("couldn't query interface %s: %s", ifname, err) } - fd, err := unix.Socket(unix.AF_PACKET, unix.SOCK_RAW, int(htons(unix.ETH_P_ALL))) + fd, err := unix.Socket(unix.AF_PACKET, unix.SOCK_RAW|unix.SOCK_CLOEXEC|unix.SOCK_NONBLOCK, int(htons(unix.ETH_P_ALL))) if err != nil { return nil, fmt.Errorf("couldn't open packet socket: %s", err) } @@ -274,13 +326,12 @@ func NewEthernetHandle(ifname string) (*EthernetHandle, error) { } handle := &EthernetHandle{ - fd: fd, + file: os.NewFile(uintptr(fd), ""), buffer: make([]byte, intf.MTU), oob: make([]byte, ooblen), ancil: make([]interface{}, 1), intf: intf.Index, addr: intf.HardwareAddr, } - runtime.SetFinalizer(handle, (*EthernetHandle).Close) return handle, nil }