mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-19 08:46:38 +00:00
Add packet capture to debug bundle and CLI
This commit is contained in:
193
util/capture/afpacket_linux.go
Normal file
193
util/capture/afpacket_linux.go
Normal file
@@ -0,0 +1,193 @@
|
||||
package capture
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// htons converts a uint16 from host to network (big-endian) byte order.
|
||||
func htons(v uint16) uint16 {
|
||||
var buf [2]byte
|
||||
binary.BigEndian.PutUint16(buf[:], v)
|
||||
return binary.NativeEndian.Uint16(buf[:])
|
||||
}
|
||||
|
||||
// AFPacketCapture reads raw packets from a network interface using an
|
||||
// AF_PACKET socket. This is the kernel-mode fallback when FilteredDevice is
|
||||
// not available (kernel WireGuard). Linux only.
|
||||
//
|
||||
// It implements device.PacketCapture so it can be set on a Session, but it
|
||||
// drives its own read loop rather than being called from FilteredDevice.
|
||||
// Call Start to begin and Stop to end.
|
||||
type AFPacketCapture struct {
|
||||
ifaceName string
|
||||
sess *Session
|
||||
fd int
|
||||
mu sync.Mutex
|
||||
stopped chan struct{}
|
||||
started atomic.Bool
|
||||
closed atomic.Bool
|
||||
}
|
||||
|
||||
// NewAFPacketCapture creates a capture bound to the given interface.
|
||||
// The session receives packets via Offer.
|
||||
func NewAFPacketCapture(ifaceName string, sess *Session) *AFPacketCapture {
|
||||
return &AFPacketCapture{
|
||||
ifaceName: ifaceName,
|
||||
sess: sess,
|
||||
fd: -1,
|
||||
stopped: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// Start opens the AF_PACKET socket and begins reading packets.
|
||||
// Packets are fed to the session via Offer. Returns immediately;
|
||||
// the read loop runs in a goroutine.
|
||||
func (c *AFPacketCapture) Start() error {
|
||||
if c.sess == nil {
|
||||
return errors.New("nil capture session")
|
||||
}
|
||||
if c.started.Load() {
|
||||
return errors.New("capture already started")
|
||||
}
|
||||
|
||||
iface, err := net.InterfaceByName(c.ifaceName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("interface %s: %w", c.ifaceName, err)
|
||||
}
|
||||
|
||||
fd, err := unix.Socket(unix.AF_PACKET, unix.SOCK_DGRAM|unix.SOCK_NONBLOCK|unix.SOCK_CLOEXEC, int(htons(unix.ETH_P_ALL)))
|
||||
if err != nil {
|
||||
return fmt.Errorf("create AF_PACKET socket: %w", err)
|
||||
}
|
||||
|
||||
addr := &unix.SockaddrLinklayer{
|
||||
Protocol: htons(unix.ETH_P_ALL),
|
||||
Ifindex: iface.Index,
|
||||
}
|
||||
if err := unix.Bind(fd, addr); err != nil {
|
||||
unix.Close(fd)
|
||||
return fmt.Errorf("bind to %s: %w", c.ifaceName, err)
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
c.fd = fd
|
||||
c.mu.Unlock()
|
||||
|
||||
c.started.Store(true)
|
||||
go c.readLoop(fd)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop closes the socket and waits for the read loop to exit. Idempotent.
|
||||
func (c *AFPacketCapture) Stop() {
|
||||
if !c.closed.CompareAndSwap(false, true) {
|
||||
if c.started.Load() {
|
||||
<-c.stopped
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
fd := c.fd
|
||||
c.fd = -1
|
||||
c.mu.Unlock()
|
||||
|
||||
if fd >= 0 {
|
||||
unix.Close(fd)
|
||||
}
|
||||
|
||||
if c.started.Load() {
|
||||
<-c.stopped
|
||||
}
|
||||
}
|
||||
|
||||
func (c *AFPacketCapture) readLoop(fd int) {
|
||||
defer close(c.stopped)
|
||||
|
||||
buf := make([]byte, 65536)
|
||||
pollFds := []unix.PollFd{{Fd: int32(fd), Events: unix.POLLIN}}
|
||||
|
||||
for {
|
||||
if c.closed.Load() {
|
||||
return
|
||||
}
|
||||
|
||||
ok, err := c.pollOnce(pollFds)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
c.recvAndOffer(fd, buf)
|
||||
}
|
||||
}
|
||||
|
||||
// pollOnce waits for data on the fd. Returns true if data is ready, false for timeout/retry.
|
||||
// Returns an error to signal the loop should exit.
|
||||
func (c *AFPacketCapture) pollOnce(pollFds []unix.PollFd) (bool, error) {
|
||||
n, err := unix.Poll(pollFds, 200)
|
||||
if err != nil {
|
||||
if errors.Is(err, unix.EINTR) {
|
||||
return false, nil
|
||||
}
|
||||
if c.closed.Load() {
|
||||
return false, errors.New("closed")
|
||||
}
|
||||
log.Debugf("af_packet poll: %v", err)
|
||||
return false, err
|
||||
}
|
||||
if n == 0 {
|
||||
return false, nil
|
||||
}
|
||||
if pollFds[0].Revents&(unix.POLLERR|unix.POLLHUP|unix.POLLNVAL) != 0 {
|
||||
return false, errors.New("fd error")
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (c *AFPacketCapture) recvAndOffer(fd int, buf []byte) {
|
||||
nr, from, err := unix.Recvfrom(fd, buf, 0)
|
||||
if err != nil {
|
||||
if errors.Is(err, unix.EAGAIN) || errors.Is(err, unix.EINTR) {
|
||||
return
|
||||
}
|
||||
if !c.closed.Load() {
|
||||
log.Debugf("af_packet recvfrom: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if nr < 1 {
|
||||
return
|
||||
}
|
||||
|
||||
ver := buf[0] >> 4
|
||||
if ver != 4 && ver != 6 {
|
||||
return
|
||||
}
|
||||
|
||||
// The kernel sets Pkttype on AF_PACKET sockets:
|
||||
// PACKET_HOST(0) = addressed to us (inbound)
|
||||
// PACKET_OUTGOING(4) = sent by us (outbound)
|
||||
outbound := false
|
||||
if sa, ok := from.(*unix.SockaddrLinklayer); ok {
|
||||
outbound = sa.Pkttype == unix.PACKET_OUTGOING
|
||||
}
|
||||
c.sess.Offer(buf[:nr], outbound)
|
||||
}
|
||||
|
||||
// Offer satisfies device.PacketCapture but is unused: the AFPacketCapture
|
||||
// drives its own read loop. This exists only so the type signature is
|
||||
// compatible if someone tries to set it as a PacketCapture.
|
||||
func (c *AFPacketCapture) Offer([]byte, bool) {
|
||||
// unused: AFPacketCapture drives its own read loop
|
||||
}
|
||||
Reference in New Issue
Block a user