mirror of
https://github.com/netbirdio/netbird.git
synced 2026-07-28 10:22:38 -04:00
## Describe your changes Make `Test_ConnectPeers` deterministic. Two issues, both surfaced once the privileged suite moved into a `--privileged` Docker container (#6425): 1. The peers used `getLocalIP()` as their WireGuard endpoint, i.e. the host's routable NIC IP (the docker bridge IP `172.17.0.2` in CI). That address might not hairpin reliably inside the container, so the handshake intermittently timed out (flaky). Use loopback (`127.1.0.x`) instead — always self-reachable. 2. On a Linux runner with the WG kernel module the iface uses the eBPF proxy factory. Its manager is a singleton with one shared XDP program + settings map, so bringing up the two ifaces makes the second factory overwrite the first's `wg_port`/`proxy_port` and the handshake is dropped. The test is incompatible with the eBPF factory, so disable it via `NB_DISABLE_EBPF_WG_PROXY` (peers then handshake directly over loopback). Running the suite across all three modes (eBPF / UDP proxy / ICE bind) would need a larger refactor. Also fixes a typo in the `ErrSharedSockStopped` message (`socked` → `socket`). ## Issue ticket number and link No public issue — CI flakiness follow-up to #6871 on `Test_ConnectPeers` (https://github.com/netbirdio/netbird/blob/main/client/iface/iface_test.go). ## Stack <!-- branch-stack --> ### Checklist - [x] Is it a bug fix - [ ] Is a typo/documentation fix - [ ] Is a feature enhancement - [ ] It is a refactor - [ ] Created tests that fail without the change (if possible) > By submitting this pull request, you confirm that you have read and agree to the terms of the [Contributor License Agreement](https://github.com/netbirdio/netbird/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT.md). ## Documentation Select exactly one: - [ ] I added/updated documentation for this change - [x] Documentation is **not needed** for this change (explain why) Test-only change (plus a log-string typo). No public API, CLI, config, or behavior change. ### Docs PR URL (required if "docs added" is checked) Paste the PR link from https://github.com/netbirdio/docs here: N/A <!-- codesmith:footer --> --- <a href="https://app.blacksmith.sh/netbirdio/codesmith/netbird/pr/6884"><picture><source media="(prefers-color-scheme: dark)" srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v2.svg"><source media="(prefers-color-scheme: light)" srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-light-v2.svg"><img alt="View with [code]smith" src="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v2.svg"></picture></a> <a href="https://backend.blacksmith.sh/track/enable-autofix?expires=1787485967&installation_model_id=427504&pr_number=6884&repository=netbirdio%2Fnetbird&return_to=https%3A%2F%2Fgithub.com%2Fnetbirdio%2Fnetbird%2Fpull%2F6884&signature=09fb996715d039bd02775a140e8cc03deca5cb42540790b82a744527264a30ad"><picture><source media="(prefers-color-scheme: dark)" srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-light.svg"><img alt="Autofix with [code]smith" src="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark.svg"></picture></a> <sup>Need help on this PR? Tag <code>@codesmith-bot</code> with what you need. Autofix is disabled.</sup> <!-- codesmith:autofix:disabled --> <!-- /codesmith:footer --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Corrected the “shared socket stopped” error message for clearer output. * **Tests** * Improved peer connection test reliability in privileged CI by disabling the eBPF WireGuard proxy and using fixed loopback UDP endpoints for deterministic setup. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
348 lines
9.2 KiB
Go
348 lines
9.2 KiB
Go
//go:build linux && !android
|
|
|
|
// Inspired by
|
|
// Jason Donenfeld (https://git.zx2c4.com/wireguard-tools/tree/contrib/nat-hole-punching/nat-punch-client.c#n96)
|
|
// and @stv0g in https://github.com/stv0g/cunicu/tree/ebpf-poc/ebpf_poc
|
|
|
|
package sharedsock
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"time"
|
|
|
|
"github.com/google/gopacket"
|
|
"github.com/google/gopacket/layers"
|
|
"github.com/mdlayher/socket"
|
|
log "github.com/sirupsen/logrus"
|
|
"github.com/vishvananda/netlink"
|
|
"golang.org/x/sync/errgroup"
|
|
"golang.org/x/sys/unix"
|
|
|
|
nbnet "github.com/netbirdio/netbird/client/net"
|
|
)
|
|
|
|
// ErrSharedSockStopped indicates that shared socket has been stopped
|
|
var ErrSharedSockStopped = fmt.Errorf("shared socket stopped")
|
|
|
|
// SharedSocket is a net.PacketConn that initiates two raw sockets (ipv4 and ipv6) and listens to UDP packets filtered
|
|
// by BPF instructions (e.g., IncomingSTUNFilter that checks and sends only STUN packets to the listeners (ReadFrom)).
|
|
// It is meant to be used when sharing a port with some other process.
|
|
type SharedSocket struct {
|
|
ctx context.Context
|
|
conn4 *socket.Conn
|
|
conn6 *socket.Conn
|
|
port int
|
|
mtu uint16
|
|
packetDemux chan rcvdPacket
|
|
cancel context.CancelFunc
|
|
}
|
|
|
|
type rcvdPacket struct {
|
|
n int
|
|
addr unix.Sockaddr
|
|
buf []byte
|
|
err error
|
|
}
|
|
|
|
type receiver func(ctx context.Context, p []byte, flags int) (int, unix.Sockaddr, error)
|
|
|
|
var writeSerializerOptions = gopacket.SerializeOptions{
|
|
ComputeChecksums: true,
|
|
FixLengths: true,
|
|
}
|
|
|
|
// Maximum overhead for IP + UDP headers on raw socket
|
|
// IPv4: max 60 bytes (20 base + 40 options) + UDP 8 bytes = 68 bytes
|
|
// IPv6: 40 bytes + UDP 8 bytes = 48 bytes
|
|
// We use the maximum (68) for both IPv4 and IPv6
|
|
const maxIPUDPOverhead = 68
|
|
|
|
// Listen creates an IPv4 and IPv6 raw sockets, starts a reader and routing table routines
|
|
func Listen(port int, filter BPFFilter, mtu uint16) (_ net.PacketConn, err error) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
rawSock := &SharedSocket{
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
mtu: mtu,
|
|
port: port,
|
|
packetDemux: make(chan rcvdPacket),
|
|
}
|
|
|
|
defer func() {
|
|
if err != nil {
|
|
if closeErr := rawSock.Close(); closeErr != nil {
|
|
log.Errorf("Failed to close raw socket: %v", closeErr)
|
|
}
|
|
}
|
|
}()
|
|
|
|
rawSock.conn4, err = socket.Socket(unix.AF_INET, unix.SOCK_RAW, unix.IPPROTO_UDP, "raw_udp4", nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create ipv4 raw socket: %w", err)
|
|
}
|
|
|
|
if err = nbnet.SetSocketMark(rawSock.conn4); err != nil {
|
|
return nil, fmt.Errorf("set SO_MARK on ipv4 socket: %w", err)
|
|
}
|
|
|
|
var sockErr error
|
|
rawSock.conn6, sockErr = socket.Socket(unix.AF_INET6, unix.SOCK_RAW, unix.IPPROTO_UDP, "raw_udp6", nil)
|
|
if sockErr != nil {
|
|
log.Errorf("Failed to create ipv6 raw socket: %v", err)
|
|
} else {
|
|
if err = nbnet.SetSocketMark(rawSock.conn6); err != nil {
|
|
return nil, fmt.Errorf("set SO_MARK on ipv6 socket: %w", err)
|
|
}
|
|
}
|
|
|
|
ipv4Instructions, ipv6Instructions, err := filter.GetInstructions(uint32(rawSock.port))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("getBPFInstructions failed with: %w", err)
|
|
}
|
|
|
|
err = rawSock.conn4.SetBPF(ipv4Instructions)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("socket4.SetBPF failed with: %w", err)
|
|
}
|
|
if rawSock.conn6 != nil {
|
|
err = rawSock.conn6.SetBPF(ipv6Instructions)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("socket6.SetBPF failed with: %w", err)
|
|
}
|
|
}
|
|
|
|
go rawSock.read(rawSock.conn4.Recvfrom)
|
|
if rawSock.conn6 != nil {
|
|
go rawSock.read(rawSock.conn6.Recvfrom)
|
|
}
|
|
|
|
return rawSock, nil
|
|
}
|
|
|
|
// resolveSrc returns the source IP the kernel will pick for a packet sent to
|
|
// dst by these raw sockets, mirroring the fwmark the kernel will see on send.
|
|
func (s *SharedSocket) resolveSrc(dst net.IP) (net.IP, error) {
|
|
opts := &netlink.RouteGetOptions{}
|
|
if nbnet.AdvancedRouting() {
|
|
opts.Mark = nbnet.ControlPlaneMark
|
|
}
|
|
routes, err := netlink.RouteGetWithOptions(dst, opts)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("route get %s: %w", dst, err)
|
|
}
|
|
for _, r := range routes {
|
|
if r.Src != nil {
|
|
return r.Src, nil
|
|
}
|
|
}
|
|
return nil, fmt.Errorf("no source IP for %s", dst)
|
|
}
|
|
|
|
// LocalAddr returns the local address, preferring IPv4 for backward compatibility.
|
|
func (s *SharedSocket) LocalAddr() net.Addr {
|
|
if s.conn4 != nil {
|
|
return &net.UDPAddr{
|
|
IP: net.IPv4zero,
|
|
Port: s.port,
|
|
}
|
|
}
|
|
if s.conn6 != nil {
|
|
return &net.UDPAddr{
|
|
IP: net.IPv6zero,
|
|
Port: s.port,
|
|
}
|
|
}
|
|
return &net.UDPAddr{
|
|
IP: net.IPv4zero,
|
|
Port: s.port,
|
|
}
|
|
}
|
|
|
|
// SetDeadline sets both the read and write deadlines associated with the ipv4 and ipv6 Conn sockets
|
|
func (s *SharedSocket) SetDeadline(t time.Time) error {
|
|
err := s.conn4.SetDeadline(t)
|
|
if err != nil {
|
|
return fmt.Errorf("s.conn4.SetDeadline error: %w", err)
|
|
}
|
|
if s.conn6 == nil {
|
|
return nil
|
|
}
|
|
|
|
err = s.conn6.SetDeadline(t)
|
|
if err != nil {
|
|
return fmt.Errorf("s.conn6.SetDeadline error: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// SetReadDeadline sets the read deadline associated with the ipv4 and ipv6 Conn sockets
|
|
func (s *SharedSocket) SetReadDeadline(t time.Time) error {
|
|
err := s.conn4.SetReadDeadline(t)
|
|
if err != nil {
|
|
return fmt.Errorf("s.conn4.SetReadDeadline error: %w", err)
|
|
}
|
|
if s.conn6 == nil {
|
|
return nil
|
|
}
|
|
|
|
err = s.conn6.SetReadDeadline(t)
|
|
if err != nil {
|
|
return fmt.Errorf("s.conn6.SetReadDeadline error: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// SetWriteDeadline sets the write deadline associated with the ipv4 and ipv6 Conn sockets
|
|
func (s *SharedSocket) SetWriteDeadline(t time.Time) error {
|
|
err := s.conn4.SetWriteDeadline(t)
|
|
if err != nil {
|
|
return fmt.Errorf("s.conn4.SetWriteDeadline error: %w", err)
|
|
}
|
|
if s.conn6 == nil {
|
|
return nil
|
|
}
|
|
|
|
err = s.conn6.SetWriteDeadline(t)
|
|
if err != nil {
|
|
return fmt.Errorf("s.conn6.SetWriteDeadline error: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Close closes the underlying ipv4 and ipv6 conn sockets
|
|
func (s *SharedSocket) Close() error {
|
|
s.cancel()
|
|
errGrp := errgroup.Group{}
|
|
if s.conn4 != nil {
|
|
errGrp.Go(s.conn4.Close)
|
|
}
|
|
|
|
if s.conn6 != nil {
|
|
errGrp.Go(s.conn6.Close)
|
|
}
|
|
return errGrp.Wait()
|
|
}
|
|
|
|
// read start a read loop for a specific receiver and sends the packet to the packetDemux channel
|
|
func (s *SharedSocket) read(receiver receiver) {
|
|
for {
|
|
buf := make([]byte, s.mtu+maxIPUDPOverhead)
|
|
n, addr, err := receiver(s.ctx, buf, 0)
|
|
select {
|
|
case <-s.ctx.Done():
|
|
return
|
|
case s.packetDemux <- rcvdPacket{n, addr, buf[:n], err}:
|
|
}
|
|
}
|
|
}
|
|
|
|
// ReadFrom reads packets received in the packetDemux channel
|
|
func (s *SharedSocket) ReadFrom(b []byte) (int, net.Addr, error) {
|
|
var pkt rcvdPacket
|
|
select {
|
|
case <-s.ctx.Done():
|
|
return -1, nil, ErrSharedSockStopped
|
|
case pkt = <-s.packetDemux:
|
|
}
|
|
|
|
if pkt.err != nil {
|
|
return -1, nil, pkt.err
|
|
}
|
|
var ip4layer layers.IPv4
|
|
var udp layers.UDP
|
|
var payload gopacket.Payload
|
|
var parser *gopacket.DecodingLayerParser
|
|
var ip net.IP
|
|
|
|
if sa, isIPv4 := pkt.addr.(*unix.SockaddrInet4); isIPv4 {
|
|
ip = sa.Addr[:]
|
|
parser = gopacket.NewDecodingLayerParser(layers.LayerTypeIPv4, &ip4layer, &udp, &payload)
|
|
} else if sa, isIPv6 := pkt.addr.(*unix.SockaddrInet6); isIPv6 {
|
|
ip = sa.Addr[:]
|
|
parser = gopacket.NewDecodingLayerParser(layers.LayerTypeUDP, &udp, &payload)
|
|
} else {
|
|
return -1, nil, fmt.Errorf("received invalid address family")
|
|
}
|
|
|
|
decodedLayers := make([]gopacket.LayerType, 0, 3)
|
|
|
|
if err := parser.DecodeLayers(pkt.buf, &decodedLayers); err != nil {
|
|
return 0, nil, err
|
|
}
|
|
|
|
remoteAddr := &net.UDPAddr{
|
|
IP: ip,
|
|
Port: int(udp.SrcPort),
|
|
}
|
|
|
|
n := copy(b, payload)
|
|
return n, remoteAddr, nil
|
|
}
|
|
|
|
// WriteTo builds a UDP packet and writes it using the specific IP version writer
|
|
func (s *SharedSocket) WriteTo(buf []byte, rAddr net.Addr) (n int, err error) {
|
|
rUDPAddr, ok := rAddr.(*net.UDPAddr)
|
|
if !ok {
|
|
return -1, fmt.Errorf("invalid address type")
|
|
}
|
|
|
|
buffer := gopacket.NewSerializeBuffer()
|
|
payload := gopacket.Payload(buf)
|
|
|
|
udp := &layers.UDP{
|
|
SrcPort: layers.UDPPort(s.port),
|
|
DstPort: layers.UDPPort(rUDPAddr.Port),
|
|
}
|
|
|
|
src, err := s.resolveSrc(rUDPAddr.IP)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("resolve source for %s: %w", rUDPAddr.IP, err)
|
|
}
|
|
|
|
rSockAddr, conn, nwLayer := s.getWriterObjects(src, rUDPAddr.IP)
|
|
if conn == nil {
|
|
return 0, fmt.Errorf("no raw socket for %s", rUDPAddr.IP)
|
|
}
|
|
|
|
if err := udp.SetNetworkLayerForChecksum(nwLayer); err != nil {
|
|
return -1, fmt.Errorf("failed to set network layer for checksum: %w", err)
|
|
}
|
|
|
|
if err := gopacket.SerializeLayers(buffer, writeSerializerOptions, udp, payload); err != nil {
|
|
return -1, fmt.Errorf("failed serialize rcvdPacket: %w", err)
|
|
}
|
|
|
|
bufser := buffer.Bytes()
|
|
|
|
return 0, conn.Sendto(context.TODO(), bufser, 0, rSockAddr)
|
|
}
|
|
|
|
// getWriterObjects returns the specific IP version objects that are used to build a packet and send it using the raw socket
|
|
func (s *SharedSocket) getWriterObjects(src, dest net.IP) (sa unix.Sockaddr, conn *socket.Conn, layer gopacket.NetworkLayer) {
|
|
if dest.To4() == nil {
|
|
sa = &unix.SockaddrInet6{}
|
|
copy(sa.(*unix.SockaddrInet6).Addr[:], dest.To16())
|
|
conn = s.conn6
|
|
|
|
layer = &layers.IPv6{
|
|
SrcIP: src,
|
|
DstIP: dest,
|
|
}
|
|
} else {
|
|
sa = &unix.SockaddrInet4{}
|
|
copy(sa.(*unix.SockaddrInet4).Addr[:], dest.To4())
|
|
conn = s.conn4
|
|
layer = &layers.IPv4{
|
|
Version: 4,
|
|
TTL: 64,
|
|
Protocol: layers.IPProtocolUDP,
|
|
SrcIP: src,
|
|
DstIP: dest,
|
|
}
|
|
}
|
|
|
|
return sa, conn, layer
|
|
}
|