mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-21 12:59:08 +02:00
[client] pqkem: bind the data-path transport on the netstack in netstack mode
In netstack mode (js/wasm and embedded clients) the overlay IP lives only inside the userspace stack, so binding it on the host failed with "cannot assign requested address". The exchange was then disabled, and in strict mode the engine refused to start at all. Reproduced in the lab with NB_USE_NETSTACK_MODE=true. Open the socket through the interface's netstack when there is one. Its datagrams then ride the tunnel like any other overlay traffic (over the relay's WebSocket in the browser). Host-TUN clients keep binding on the host.
This commit is contained in:
@@ -598,7 +598,7 @@ func (e *Engine) startPQKEMManager(publicKey wgtypes.Key) error {
|
|||||||
if !pqkem.Enabled() {
|
if !pqkem.Enabled() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
tr, err := newPQTransport(e.config.WgAddr.IP)
|
tr, err := newPQTransport(e.config.WgAddr.IP, e.wgInterface.GetNet())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if pqkem.Strict() {
|
if pqkem.Strict() {
|
||||||
return fmt.Errorf("pqkem: strict mode enabled but transport bind failed: %w", err)
|
return fmt.Errorf("pqkem: strict mode enabled but transport bind failed: %w", err)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
"golang.zx2c4.com/wireguard/tun/netstack"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DefaultPort is the preferred UDP port for the ML-KEM data-path service, bound on
|
// DefaultPort is the preferred UDP port for the ML-KEM data-path service, bound on
|
||||||
@@ -25,10 +26,15 @@ type pqTransport struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// newPQTransport binds a UDP socket on the WG overlay IP, preferring DefaultPort and
|
// newPQTransport binds a UDP socket on the WG overlay IP, preferring DefaultPort and
|
||||||
// falling back to an OS-assigned ephemeral port if it is in use. Call it after the WG
|
// falling back to an ephemeral port if it is in use. Call it after the WG interface is
|
||||||
// interface is up so the overlay IP is assigned; when the bound port is not
|
// up so the overlay IP is assigned; when the bound port is not DefaultPort it must be
|
||||||
// DefaultPort it must be announced to peers via Body.mlkemPort.
|
// announced to peers via Body.mlkemPort.
|
||||||
func newPQTransport(overlayIP netip.Addr) (*pqTransport, error) {
|
//
|
||||||
|
// ns is the interface's netstack when it runs in netstack mode (js/wasm, embedded
|
||||||
|
// clients): the overlay IP then exists only inside that userspace stack, so the socket
|
||||||
|
// is opened there and its datagrams ride the tunnel like any other overlay traffic. A
|
||||||
|
// nil ns binds on the host.
|
||||||
|
func newPQTransport(overlayIP netip.Addr, ns *netstack.Net) (*pqTransport, error) {
|
||||||
if !overlayIP.IsValid() {
|
if !overlayIP.IsValid() {
|
||||||
return nil, fmt.Errorf("invalid overlay IP for pqkem transport")
|
return nil, fmt.Errorf("invalid overlay IP for pqkem transport")
|
||||||
}
|
}
|
||||||
@@ -37,6 +43,13 @@ func newPQTransport(overlayIP netip.Addr) (*pqTransport, error) {
|
|||||||
// IPv4-mapped IPv6 address (a hardcoded "udp4" would otherwise fail on its 16 bytes).
|
// IPv4-mapped IPv6 address (a hardcoded "udp4" would otherwise fail on its 16 bytes).
|
||||||
overlayIP = overlayIP.Unmap()
|
overlayIP = overlayIP.Unmap()
|
||||||
listen := func(port uint16) (net.PacketConn, error) {
|
listen := func(port uint16) (net.PacketConn, error) {
|
||||||
|
if ns != nil {
|
||||||
|
conn, err := ns.ListenUDPAddrPort(netip.AddrPortFrom(overlayIP, port))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
conn, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IP(overlayIP.AsSlice()), Port: int(port)})
|
conn, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IP(overlayIP.AsSlice()), Port: int(port)})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"golang.zx2c4.com/wireguard/tun/netstack"
|
||||||
)
|
)
|
||||||
|
|
||||||
type pqInbound struct {
|
type pqInbound struct {
|
||||||
@@ -45,9 +46,9 @@ func receivePQ(t *testing.T, got <-chan pqInbound) pqInbound {
|
|||||||
func TestPQTransport_HostRoundTrip(t *testing.T) {
|
func TestPQTransport_HostRoundTrip(t *testing.T) {
|
||||||
loopback := netip.MustParseAddr("127.0.0.1")
|
loopback := netip.MustParseAddr("127.0.0.1")
|
||||||
|
|
||||||
a, err := newPQTransport(loopback)
|
a, err := newPQTransport(loopback, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
b, err := newPQTransport(loopback)
|
b, err := newPQTransport(loopback, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotEqual(t, a.LocalPort(), b.LocalPort(), "two transports on one IP must get distinct ports")
|
require.NotEqual(t, a.LocalPort(), b.LocalPort(), "two transports on one IP must get distinct ports")
|
||||||
|
|
||||||
@@ -60,3 +61,32 @@ func TestPQTransport_HostRoundTrip(t *testing.T) {
|
|||||||
assert.Equal(t, []byte("offer"), in.msg, "payload must arrive intact")
|
assert.Equal(t, []byte("offer"), in.msg, "payload must arrive intact")
|
||||||
assert.Equal(t, netip.AddrPortFrom(loopback, uint16(a.LocalPort())), in.src, "source must be the sender's unmapped IPv4 endpoint")
|
assert.Equal(t, netip.AddrPortFrom(loopback, uint16(a.LocalPort())), in.src, "source must be the sender's unmapped IPv4 endpoint")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestPQTransport_NetstackRoundTrip covers netstack mode (js/wasm and embedded clients),
|
||||||
|
// where the overlay IP exists only inside the userspace stack. Binding it on the host
|
||||||
|
// fails, which disabled the exchange (or, in strict mode, stopped the engine from
|
||||||
|
// starting); the transport must open its socket on the netstack instead.
|
||||||
|
func TestPQTransport_NetstackRoundTrip(t *testing.T) {
|
||||||
|
overlay := netip.MustParseAddr("192.0.2.1")
|
||||||
|
dev, ns, err := netstack.CreateNetTUN([]netip.Addr{overlay}, nil, 1280)
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Cleanup(func() { _ = dev.Close() })
|
||||||
|
|
||||||
|
_, err = newPQTransport(overlay, nil)
|
||||||
|
require.Error(t, err, "the netstack-only overlay IP must not be bindable on the host")
|
||||||
|
|
||||||
|
a, err := newPQTransport(overlay, ns)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, DefaultPort, a.LocalPort(), "the default port must be free on a fresh netstack")
|
||||||
|
b, err := newPQTransport(overlay, ns)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
runPQTransport(t, a)
|
||||||
|
gotB := runPQTransport(t, b)
|
||||||
|
|
||||||
|
require.NoError(t, a.Send(netip.AddrPortFrom(overlay, uint16(b.LocalPort())), []byte("offer")))
|
||||||
|
|
||||||
|
in := receivePQ(t, gotB)
|
||||||
|
assert.Equal(t, []byte("offer"), in.msg, "payload must arrive intact")
|
||||||
|
assert.Equal(t, netip.AddrPortFrom(overlay, uint16(a.LocalPort())), in.src, "source must be the sender's overlay endpoint")
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user