Files
netbird/util/capture/afpacket_linux.go
2026-04-15 19:19:09 +02:00

194 lines
4.4 KiB
Go

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
}