Files
netbird/client/iface/wgproxy/loopback/proxy.go

276 lines
7.3 KiB
Go

//go:build linux && !android
package loopback
import (
"context"
"fmt"
"net"
"net/netip"
"sync"
"syscall"
"github.com/hashicorp/go-multierror"
log "github.com/sirupsen/logrus"
"golang.org/x/net/ipv4"
"golang.org/x/sys/unix"
nberrors "github.com/netbirdio/netbird/client/errors"
"github.com/netbirdio/netbird/client/iface/bufsize"
"github.com/netbirdio/netbird/client/iface/wgproxy/rawsocket"
)
const (
loopbackDevice = "lo"
portRangeStart = 3128
portRangeEnd = portRangeStart + 100
)
// Proxy forwards packets between relayed connections and a local kernel
// WireGuard instance. Every relayed peer gets its own loopback address as its
// WireGuard endpoint, so a single socket serves all of them: the destination
// address of an incoming packet identifies the peer.
type Proxy struct {
localWGListenPort int
mtu uint16
proxyPort int
conn *net.UDPConn
packetConn *ipv4.PacketConn
rawConnIPv4 net.PacketConn
rawConnIPv6 net.PacketConn
relayedConnMutex sync.Mutex
relayedConnStore map[netip.Addr]net.Conn
addrs allocator
ctx context.Context
ctxCancel context.CancelFunc
}
// NewProxy creates a proxy for the WireGuard instance listening on wgPort.
func NewProxy(wgPort int, mtu uint16) *Proxy {
log.Debugf("instantiate loopback wg proxy")
return &Proxy{
localWGListenPort: wgPort,
mtu: mtu,
relayedConnStore: make(map[netip.Addr]net.Conn),
}
}
// Listen opens the shared socket and starts forwarding WireGuard packets to the
// relayed connections.
func (p *Proxy) Listen() error {
rawConnIPv4, err := rawsocket.PrepareSenderRawSocketIPv4()
if err != nil {
return fmt.Errorf("prepare IPv4 raw socket: %w", err)
}
p.rawConnIPv4 = rawConnIPv4
p.rawConnIPv6, err = rawsocket.PrepareSenderRawSocketIPv6()
if err != nil {
log.Warnf("failed to prepare IPv6 raw socket, continuing with IPv4 only: %v", err)
}
if err := p.listen(); err != nil {
if freeErr := p.Free(); freeErr != nil {
log.Errorf("failed to free the wgproxy: %s", freeErr)
}
return err
}
p.ctx, p.ctxCancel = context.WithCancel(context.Background())
go p.proxyToRemote()
log.Infof("local wg proxy listening on %s:%d", addrRangePrefix, p.proxyPort)
return nil
}
// listen binds the shared socket on the first free port of the range. The bind
// has to be a wildcard one to receive every peer address in the range, so it is
// restricted to the loopback device: without that the port would be reachable
// on every interface.
func (p *Proxy) listen() error {
var lastErr error
for port := portRangeStart; port <= portRangeEnd; port++ {
err := p.listenOn(port)
if err == nil {
p.proxyPort = port
return nil
}
lastErr = err
}
return fmt.Errorf("bind proxy port in range %d-%d: %w", portRangeStart, portRangeEnd, lastErr)
}
func (p *Proxy) listenOn(proxyPort int) error {
lc := net.ListenConfig{
Control: func(_, _ string, c syscall.RawConn) error {
var sockErr error
if err := c.Control(func(fd uintptr) {
if err := unix.SetsockoptString(int(fd), unix.SOL_SOCKET, unix.SO_BINDTODEVICE, loopbackDevice); err != nil {
sockErr = fmt.Errorf("bind to %s: %w", loopbackDevice, err)
return
}
}); err != nil {
return fmt.Errorf("control socket: %w", err)
}
return sockErr
},
}
conn, err := lc.ListenPacket(context.Background(), "udp4", fmt.Sprintf(":%d", proxyPort))
if err != nil {
return fmt.Errorf("listen on :%d: %w", proxyPort, err)
}
udpConn, ok := conn.(*net.UDPConn)
if !ok {
if closeErr := conn.Close(); closeErr != nil {
log.Errorf("failed to close proxy conn: %s", closeErr)
}
return fmt.Errorf("unexpected conn type %T", conn)
}
packetConn := ipv4.NewPacketConn(udpConn)
// the destination address carries the peer identity, the interface index is
// checked on receive as a second line of defense behind SO_BINDTODEVICE
if err := packetConn.SetControlMessage(ipv4.FlagDst|ipv4.FlagInterface, true); err != nil {
if closeErr := udpConn.Close(); closeErr != nil {
log.Errorf("failed to close proxy conn: %s", closeErr)
}
return fmt.Errorf("request destination address: %w", err)
}
p.conn = udpConn
p.packetConn = packetConn
return nil
}
// AddRelayedConn assigns an endpoint address to the relayed connection and
// returns the address WireGuard should send to.
func (p *Proxy) AddRelayedConn(relayedConn net.Conn) (*net.UDPAddr, error) {
addr, err := p.storeRelayedConn(relayedConn)
if err != nil {
return nil, err
}
log.Infof("relayed conn added to wg proxy store: %s, endpoint address: %s", relayedConn.RemoteAddr(), addr)
return &net.UDPAddr{
IP: addr.AsSlice(),
Port: p.proxyPort,
}, nil
}
// Free releases the proxy resources. The relayed connections are left open.
func (p *Proxy) Free() error {
log.Debugf("free up loopback wg proxy")
if p.ctx != nil && p.ctx.Err() != nil {
//nolint
return nil
}
if p.ctxCancel != nil {
p.ctxCancel()
}
var result *multierror.Error
if p.conn != nil {
if err := p.conn.Close(); 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)
}
// GetProxyPort returns the port every peer endpoint address is reached on.
func (p *Proxy) GetProxyPort() uint16 {
return uint16(p.proxyPort)
}
// proxyToRemote reads packets from the local WireGuard instance and forwards
// them to the relayed connection the destination address belongs to.
func (p *Proxy) 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 *Proxy) readAndForwardPacket(buf []byte) error {
n, cm, _, err := p.packetConn.ReadFrom(buf)
if err != nil {
return fmt.Errorf("read UDP packet from WG: %w", err)
}
if cm == nil {
return fmt.Errorf("no control message on packet")
}
dst, ok := netip.AddrFromSlice(cm.Dst.To4())
if !ok || !inRange(dst) {
log.Tracef("dropping packet for unexpected destination %s", cm.Dst)
return nil
}
p.relayedConnMutex.Lock()
conn, ok := p.relayedConnStore[dst]
p.relayedConnMutex.Unlock()
if !ok {
if p.ctx.Err() == nil {
log.Debugf("relayed conn not found by address because conn already has been closed: %s", dst)
}
return nil
}
if _, err := conn.Write(buf[:n]); err != nil {
return fmt.Errorf("forward local WG packet (%s) to remote relayed conn: %w", dst, err)
}
return nil
}
func (p *Proxy) storeRelayedConn(relayedConn net.Conn) (netip.Addr, error) {
p.relayedConnMutex.Lock()
defer p.relayedConnMutex.Unlock()
addr, err := p.addrs.next(func(a netip.Addr) bool {
_, ok := p.relayedConnStore[a]
return ok
})
if err != nil {
return netip.Addr{}, err
}
p.relayedConnStore[addr] = relayedConn
return addr, nil
}
func (p *Proxy) removeRelayedConn(addr netip.Addr) {
p.relayedConnMutex.Lock()
defer p.relayedConnMutex.Unlock()
if _, ok := p.relayedConnStore[addr]; ok {
log.Debugf("remove relayed conn from store by address: %s", addr)
}
delete(p.relayedConnStore, addr)
}