mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-16 07:16:38 +00:00
* Add IPv6 support to UDP WireGuard proxy Add IPv6 packet header support in UDP raw socket proxy to handle both IPv4 and IPv6 source addresses. Refactor error handling in proxy bind implementations to validate endpoints before acquiring locks.
309 lines
7.4 KiB
Go
309 lines
7.4 KiB
Go
//go:build linux && !android
|
|
|
|
package ebpf
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"sync"
|
|
|
|
"github.com/google/gopacket"
|
|
"github.com/google/gopacket/layers"
|
|
"github.com/hashicorp/go-multierror"
|
|
"github.com/pion/transport/v3"
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
nberrors "github.com/netbirdio/netbird/client/errors"
|
|
"github.com/netbirdio/netbird/client/iface/bufsize"
|
|
"github.com/netbirdio/netbird/client/iface/wgproxy/rawsocket"
|
|
"github.com/netbirdio/netbird/client/internal/ebpf"
|
|
ebpfMgr "github.com/netbirdio/netbird/client/internal/ebpf/manager"
|
|
nbnet "github.com/netbirdio/netbird/client/net"
|
|
)
|
|
|
|
const (
|
|
loopbackAddr = "127.0.0.1"
|
|
)
|
|
|
|
var (
|
|
localHostNetIPv4 = net.ParseIP("127.0.0.1")
|
|
localHostNetIPv6 = net.ParseIP("::1")
|
|
|
|
serializeOpts = gopacket.SerializeOptions{
|
|
ComputeChecksums: true,
|
|
FixLengths: true,
|
|
}
|
|
)
|
|
|
|
// WGEBPFProxy definition for proxy with EBPF support
|
|
type WGEBPFProxy struct {
|
|
localWGListenPort int
|
|
mtu uint16
|
|
|
|
ebpfManager ebpfMgr.Manager
|
|
turnConnStore map[uint16]net.Conn
|
|
turnConnMutex sync.Mutex
|
|
|
|
lastUsedPort uint16
|
|
rawConnIPv4 net.PacketConn
|
|
rawConnIPv6 net.PacketConn
|
|
conn transport.UDPConn
|
|
|
|
ctx context.Context
|
|
ctxCancel context.CancelFunc
|
|
}
|
|
|
|
// NewWGEBPFProxy create new WGEBPFProxy instance
|
|
func NewWGEBPFProxy(wgPort int, mtu uint16) *WGEBPFProxy {
|
|
log.Debugf("instantiate ebpf proxy")
|
|
wgProxy := &WGEBPFProxy{
|
|
localWGListenPort: wgPort,
|
|
mtu: mtu,
|
|
ebpfManager: ebpf.GetEbpfManagerInstance(),
|
|
turnConnStore: make(map[uint16]net.Conn),
|
|
}
|
|
return wgProxy
|
|
}
|
|
|
|
// Listen load ebpf program and listen the proxy
|
|
func (p *WGEBPFProxy) Listen() error {
|
|
pl := portLookup{}
|
|
wgPorxyPort, err := pl.searchFreePort()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Prepare IPv4 raw socket (required)
|
|
p.rawConnIPv4, err = rawsocket.PrepareSenderRawSocketIPv4()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Prepare IPv6 raw socket (optional)
|
|
p.rawConnIPv6, err = rawsocket.PrepareSenderRawSocketIPv6()
|
|
if err != nil {
|
|
log.Warnf("failed to prepare IPv6 raw socket, continuing with IPv4 only: %v", err)
|
|
}
|
|
|
|
err = p.ebpfManager.LoadWgProxy(wgPorxyPort, p.localWGListenPort)
|
|
if err != nil {
|
|
if closeErr := p.rawConnIPv4.Close(); closeErr != nil {
|
|
log.Warnf("failed to close IPv4 raw socket: %v", closeErr)
|
|
}
|
|
if p.rawConnIPv6 != nil {
|
|
if closeErr := p.rawConnIPv6.Close(); closeErr != nil {
|
|
log.Warnf("failed to close IPv6 raw socket: %v", closeErr)
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
addr := net.UDPAddr{
|
|
Port: wgPorxyPort,
|
|
IP: net.ParseIP(loopbackAddr),
|
|
}
|
|
|
|
p.ctx, p.ctxCancel = context.WithCancel(context.Background())
|
|
|
|
conn, err := nbnet.ListenUDP("udp", &addr)
|
|
if err != nil {
|
|
if cErr := p.Free(); cErr != nil {
|
|
log.Errorf("Failed to close the wgproxy: %s", cErr)
|
|
}
|
|
return err
|
|
}
|
|
p.conn = conn
|
|
|
|
go p.proxyToRemote()
|
|
log.Infof("local wg proxy listening on: %d", wgPorxyPort)
|
|
return nil
|
|
}
|
|
|
|
// AddTurnConn add new turn connection for the proxy
|
|
func (p *WGEBPFProxy) AddTurnConn(turnConn net.Conn) (*net.UDPAddr, error) {
|
|
wgEndpointPort, err := p.storeTurnConn(turnConn)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
log.Infof("turn conn added to wg proxy store: %s, endpoint port: :%d", turnConn.RemoteAddr(), wgEndpointPort)
|
|
|
|
wgEndpoint := &net.UDPAddr{
|
|
IP: net.ParseIP(loopbackAddr),
|
|
Port: int(wgEndpointPort),
|
|
}
|
|
return wgEndpoint, nil
|
|
}
|
|
|
|
// Free resources except the remoteConns will be keep open.
|
|
func (p *WGEBPFProxy) Free() error {
|
|
log.Debugf("free up ebpf wg proxy")
|
|
if p.ctx != nil && p.ctx.Err() != nil {
|
|
//nolint
|
|
return nil
|
|
}
|
|
|
|
p.ctxCancel()
|
|
|
|
var result *multierror.Error
|
|
if p.conn != nil {
|
|
if err := p.conn.Close(); err != nil {
|
|
result = multierror.Append(result, err)
|
|
}
|
|
}
|
|
|
|
if err := p.ebpfManager.FreeWGProxy(); err != nil {
|
|
result = multierror.Append(result, err)
|
|
}
|
|
|
|
if p.rawConnIPv4 != nil {
|
|
if err := p.rawConnIPv4.Close(); err != nil {
|
|
result = multierror.Append(result, err)
|
|
}
|
|
}
|
|
|
|
if p.rawConnIPv6 != nil {
|
|
if err := p.rawConnIPv6.Close(); err != nil {
|
|
result = multierror.Append(result, err)
|
|
}
|
|
}
|
|
return nberrors.FormatErrorOrNil(result)
|
|
}
|
|
|
|
// proxyToRemote read messages from local WireGuard interface and forward it to remote conn
|
|
// From this go routine has only one instance.
|
|
func (p *WGEBPFProxy) proxyToRemote() {
|
|
buf := make([]byte, p.mtu+bufsize.WGBufferOverhead)
|
|
for p.ctx.Err() == nil {
|
|
if err := p.readAndForwardPacket(buf); err != nil {
|
|
if p.ctx.Err() != nil {
|
|
return
|
|
}
|
|
log.Errorf("failed to proxy packet to remote conn: %s", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *WGEBPFProxy) readAndForwardPacket(buf []byte) error {
|
|
n, addr, err := p.conn.ReadFromUDP(buf)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read UDP packet from WG: %w", err)
|
|
}
|
|
|
|
p.turnConnMutex.Lock()
|
|
conn, ok := p.turnConnStore[uint16(addr.Port)]
|
|
p.turnConnMutex.Unlock()
|
|
if !ok {
|
|
if p.ctx.Err() == nil {
|
|
log.Debugf("turn conn not found by port because conn already has been closed: %d", addr.Port)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if _, err := conn.Write(buf[:n]); err != nil {
|
|
return fmt.Errorf("failed to forward local WG packet (%d) to remote turn conn: %w", addr.Port, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *WGEBPFProxy) storeTurnConn(turnConn net.Conn) (uint16, error) {
|
|
p.turnConnMutex.Lock()
|
|
defer p.turnConnMutex.Unlock()
|
|
|
|
np, err := p.nextFreePort()
|
|
if err != nil {
|
|
return np, err
|
|
}
|
|
p.turnConnStore[np] = turnConn
|
|
return np, nil
|
|
}
|
|
|
|
func (p *WGEBPFProxy) removeTurnConn(turnConnID uint16) {
|
|
p.turnConnMutex.Lock()
|
|
defer p.turnConnMutex.Unlock()
|
|
|
|
_, ok := p.turnConnStore[turnConnID]
|
|
if ok {
|
|
log.Debugf("remove turn conn from store by port: %d", turnConnID)
|
|
}
|
|
delete(p.turnConnStore, turnConnID)
|
|
}
|
|
|
|
func (p *WGEBPFProxy) nextFreePort() (uint16, error) {
|
|
if len(p.turnConnStore) == 65535 {
|
|
return 0, fmt.Errorf("reached maximum turn connection numbers")
|
|
}
|
|
generatePort:
|
|
if p.lastUsedPort == 65535 {
|
|
p.lastUsedPort = 1
|
|
} else {
|
|
p.lastUsedPort++
|
|
}
|
|
|
|
if _, ok := p.turnConnStore[p.lastUsedPort]; ok {
|
|
goto generatePort
|
|
}
|
|
return p.lastUsedPort, nil
|
|
}
|
|
|
|
func (p *WGEBPFProxy) sendPkg(data []byte, endpointAddr *net.UDPAddr) error {
|
|
|
|
var ipH gopacket.SerializableLayer
|
|
var networkLayer gopacket.NetworkLayer
|
|
var dstIP net.IP
|
|
var rawConn net.PacketConn
|
|
|
|
if endpointAddr.IP.To4() != nil {
|
|
// IPv4 path
|
|
ipv4 := &layers.IPv4{
|
|
DstIP: localHostNetIPv4,
|
|
SrcIP: endpointAddr.IP,
|
|
Version: 4,
|
|
TTL: 64,
|
|
Protocol: layers.IPProtocolUDP,
|
|
}
|
|
ipH = ipv4
|
|
networkLayer = ipv4
|
|
dstIP = localHostNetIPv4
|
|
rawConn = p.rawConnIPv4
|
|
} else {
|
|
// IPv6 path
|
|
if p.rawConnIPv6 == nil {
|
|
return fmt.Errorf("IPv6 raw socket not available")
|
|
}
|
|
ipv6 := &layers.IPv6{
|
|
DstIP: localHostNetIPv6,
|
|
SrcIP: endpointAddr.IP,
|
|
Version: 6,
|
|
HopLimit: 64,
|
|
NextHeader: layers.IPProtocolUDP,
|
|
}
|
|
ipH = ipv6
|
|
networkLayer = ipv6
|
|
dstIP = localHostNetIPv6
|
|
rawConn = p.rawConnIPv6
|
|
}
|
|
|
|
udpH := &layers.UDP{
|
|
SrcPort: layers.UDPPort(endpointAddr.Port),
|
|
DstPort: layers.UDPPort(p.localWGListenPort),
|
|
}
|
|
|
|
if err := udpH.SetNetworkLayerForChecksum(networkLayer); err != nil {
|
|
return fmt.Errorf("set network layer for checksum: %w", err)
|
|
}
|
|
|
|
layerBuffer := gopacket.NewSerializeBuffer()
|
|
payload := gopacket.Payload(data)
|
|
|
|
if err := gopacket.SerializeLayers(layerBuffer, serializeOpts, ipH, udpH, payload); err != nil {
|
|
return fmt.Errorf("serialize layers: %w", err)
|
|
}
|
|
|
|
if _, err := rawConn.WriteTo(layerBuffer.Bytes(), &net.IPAddr{IP: dstIP}); err != nil {
|
|
return fmt.Errorf("write to raw conn: %w", err)
|
|
}
|
|
return nil
|
|
}
|