Merge branch 'main' into fix/pkce-flow-session-extend

management.pb.go conflicted because both sides added to it: main gave
ResourceCompact an id and GroupCompact a resources list, this branch added
PKCEAuthorizationFlowRequest.SessionExtend. The .proto merged cleanly, so
the generated file was regenerated from it rather than resolved by hand.
This commit is contained in:
Zoltán Papp
2026-08-27 11:57:56 +02:00
353 changed files with 25426 additions and 3863 deletions
+1 -1
View File
@@ -124,7 +124,7 @@ func startManagement(t *testing.T, config *config.Config, testFile string) (*grp
updateManager := update_channel.NewPeersUpdateManager(metrics)
requestBuffer := mgmt.NewAccountRequestBuffer(ctx, store)
networkMapController := controller.NewController(ctx, store, metrics, updateManager, requestBuffer, mgmt.MockIntegratedValidator{}, settingsMockManager, "netbird.cloud", port_forwarding.NewControllerMock(), manager.NewEphemeralManager(store, peersmanager), config)
networkMapController := controller.NewController(ctx, store, metrics, updateManager, requestBuffer, mgmt.MockIntegratedValidator{}, settingsMockManager, "netbird.cloud", port_forwarding.NewControllerMock(), manager.NewEphemeralManager(store, peersmanager), config, nil)
accountManager, err := mgmt.BuildManager(ctx, config, store, networkMapController, jobManager, nil, "", eventStore, nil, false, iv, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManagerMock, false, cacheStore)
if err != nil {
+1 -1
View File
@@ -146,7 +146,7 @@ func startManagement(t *testing.T, signalAddr string) string {
updateManager := update_channel.NewPeersUpdateManager(metrics)
requestBuffer := mgmt.NewAccountRequestBuffer(context.Background(), testStore)
networkMapController := controller.NewController(context.Background(), testStore, metrics, updateManager, requestBuffer, mgmt.MockIntegratedValidator{}, settingsMockManager, "netbird.selfhosted", port_forwarding.NewControllerMock(), manager.NewEphemeralManager(testStore, peersManager), cfg)
networkMapController := controller.NewController(context.Background(), testStore, metrics, updateManager, requestBuffer, mgmt.MockIntegratedValidator{}, settingsMockManager, "netbird.selfhosted", port_forwarding.NewControllerMock(), manager.NewEphemeralManager(testStore, peersManager), cfg, nil)
accountManager, err := mgmt.BuildManager(context.Background(), cfg, testStore, networkMapController, jobManager, nil, "", eventStore, nil, false, iv, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false, cacheStore)
require.NoError(t, err)
+1 -1
View File
@@ -763,7 +763,7 @@ func (r *router) addNatRule(pair firewall.RouterPair) error {
exprs = append(exprs, sourceExp...)
exprs = append(exprs, destExp...)
var markValue uint32 = nbnet.PreroutingFwmarkMasquerade
markValue := nbnet.PreroutingFwmarkMasquerade
if pair.Inverse {
markValue = nbnet.PreroutingFwmarkMasqueradeReturn
}
+1 -1
View File
@@ -502,7 +502,7 @@ func toBytes(s string) (int64, error) {
func getFwmark() int {
if nbnet.AdvancedRouting() && runtime.GOOS == "linux" {
return nbnet.ControlPlaneMark
return int(nbnet.ControlPlaneMark)
}
return 0
}
+6 -10
View File
@@ -10,8 +10,6 @@ import (
log "github.com/sirupsen/logrus"
"golang.org/x/sys/unix"
nbnet "github.com/netbirdio/netbird/client/net"
)
// PrepareSenderRawSocketIPv4 creates and configures a raw socket for sending IPv4 packets
@@ -60,14 +58,12 @@ func prepareSenderRawSocket(family int, isIPv4 bool) (net.PacketConn, error) {
return nil, fmt.Errorf("binding to lo interface failed: %w", err)
}
// Set the fwmark on the socket.
err = nbnet.SetSocketOpt(fd)
if err != nil {
if closeErr := syscall.Close(fd); closeErr != nil {
log.Warnf("failed to close raw socket fd: %v", closeErr)
}
return nil, fmt.Errorf("setting fwmark failed: %w", err)
}
// The socket is bound to lo and only ever sends to the local WireGuard
// instance, a destination the local routing table resolves without help, so
// it carries no fwmark. Staying unmarked also keeps these packets out of
// third-party NAT rules that match on marks: such a rule rewriting the
// source would make WireGuard adopt the rewritten address as the peer
// endpoint.
// Convert the file descriptor to a PacketConn.
file := os.NewFile(uintptr(fd), fmt.Sprintf("fd %d", fd))
@@ -0,0 +1,77 @@
//go:build linux && !android && privileged
package rawsocket
import (
"net"
"syscall"
"testing"
"golang.org/x/sys/unix"
nbnet "github.com/netbirdio/netbird/client/net"
)
// The sender sockets must stay unmarked: a NAT rule matching on fwmark that
// rewrites the source of an injected packet makes WireGuard adopt the rewritten
// address as the peer endpoint.
func TestSenderRawSocketsCarryNoFwmark(t *testing.T) {
// the mark is only ever applied when advanced routing is available, so
// without it the assertion below would hold for the wrong reason
nbnet.Init()
if !nbnet.AdvancedRouting() {
t.Skip("advanced routing unsupported, the sockets carry no mark either way")
}
tests := []struct {
name string
prepare func() (net.PacketConn, error)
// the proxy treats the IPv6 socket as optional, so a host without IPv6
// is a reason to skip rather than to fail
optional bool
}{
{name: "IPv4", prepare: PrepareSenderRawSocketIPv4},
{name: "IPv6", prepare: PrepareSenderRawSocketIPv6, optional: true},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
conn, err := tc.prepare()
if err != nil {
if tc.optional {
t.Skipf("prepare raw socket: %v", err)
}
t.Fatalf("prepare raw socket: %v", err)
}
defer func() {
if err := conn.Close(); err != nil {
t.Logf("close raw socket: %v", err)
}
}()
syscallConn, ok := conn.(syscall.Conn)
if !ok {
t.Fatalf("raw socket %T does not expose a syscall conn", conn)
}
raw, err := syscallConn.SyscallConn()
if err != nil {
t.Fatalf("syscall conn: %v", err)
}
var mark int
var markErr error
if err := raw.Control(func(fd uintptr) {
mark, markErr = unix.GetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_MARK)
}); err != nil {
t.Fatalf("control: %v", err)
}
if markErr != nil {
t.Fatalf("get SO_MARK: %v", markErr)
}
if mark != 0 {
t.Errorf("SO_MARK = %#x, want 0", mark)
}
})
}
}
+87
View File
@@ -8,7 +8,9 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/netbirdio/netbird/client/iface/wgaddr"
nbdns "github.com/netbirdio/netbird/dns"
mgmProto "github.com/netbirdio/netbird/shared/management/proto"
)
func TestCreatePTRRecord_IPv4(t *testing.T) {
@@ -136,3 +138,88 @@ func TestAddReverseZone_IPv6(t *testing.T) {
assert.Len(t, reverseZone.Records, 1)
assert.Equal(t, int(dns.TypePTR), reverseZone.Records[0].Type)
}
// TestToDNSConfig_ZoneFlagsPreserved pins the per-zone NonAuthoritative flag
// through the legacy DNSConfig path. A non-authoritative zone is match-only:
// the local resolver falls through to the upstream for an in-zone name it does
// not define. The built-in peer zone is the authoritative one and must stay
// that way, so the flag has to travel per zone rather than be derived.
func TestToDNSConfig_ZoneFlagsPreserved(t *testing.T) {
config := toDNSConfig(&mgmProto.DNSConfig{
ServiceEnable: true,
CustomZones: []*mgmProto.CustomZone{
{
Domain: "netbird.cloud.",
Records: []*mgmProto.SimpleRecord{
{Name: "peer1.netbird.cloud.", Type: int64(dns.TypeA), Class: nbdns.DefaultClass, TTL: 300, RData: "100.64.0.1"},
},
},
{
Domain: "corp.internal.",
NonAuthoritative: true,
SearchDomainDisabled: true,
Records: []*mgmProto.SimpleRecord{
{Name: "db.corp.internal.", Type: int64(dns.TypeA), Class: nbdns.DefaultClass, TTL: 300, RData: "10.10.0.5"},
},
},
},
}, wgaddr.Address{
IP: netip.MustParseAddr("100.64.0.1"),
Network: netip.MustParsePrefix("100.64.0.0/16"),
})
zones := make(map[string]nbdns.CustomZone, len(config.CustomZones))
for _, zone := range config.CustomZones {
zones[zone.Domain] = zone
}
peerZone, ok := zones["netbird.cloud."]
require.True(t, ok, "peer zone must survive")
assert.False(t, peerZone.NonAuthoritative, "the built-in peer zone owns the account domain and stays authoritative")
accountZone, ok := zones["corp.internal."]
require.True(t, ok, "account zone must survive")
assert.True(t, accountZone.NonAuthoritative, "an account zone stays match-only, else undefined in-zone names get black-holed")
assert.True(t, accountZone.SearchDomainDisabled)
}
// TestToDNSConfig_SingleZoneForcedAuthoritative pins the compatibility clause
// in toDNSConfig: a config carrying exactly one zone is treated as
// authoritative no matter what the server said, because servers that predate
// the NonAuthoritative field send only the peer FQDN zone.
//
// The clause can only ever downgrade an explicit true to false, so a server
// that legitimately sends a single non-authoritative zone — an account whose
// only zone is a custom one, with no peer records to build the built-in zone
// from — gets that zone's whole apex black-holed on the client. Real accounts
// always carry the peer zone alongside, which is why this is latent. Narrowing
// it needs a way to tell "unset" from "false" on the wire, or the account
// domain passed down here; until then this test states the contract so a
// change to it is deliberate.
func TestToDNSConfig_SingleZoneForcedAuthoritative(t *testing.T) {
config := toDNSConfig(&mgmProto.DNSConfig{
ServiceEnable: true,
CustomZones: []*mgmProto.CustomZone{
{
Domain: "corp.internal.",
NonAuthoritative: true,
Records: []*mgmProto.SimpleRecord{
{Name: "db.corp.internal.", Type: int64(dns.TypeA), Class: nbdns.DefaultClass, TTL: 300, RData: "10.10.0.5"},
},
},
},
}, wgaddr.Address{
IP: netip.MustParseAddr("100.64.0.1"),
Network: netip.MustParsePrefix("100.64.0.0/16"),
})
require.NotEmpty(t, config.CustomZones)
assert.Equal(t, "corp.internal.", config.CustomZones[0].Domain)
assert.False(t, config.CustomZones[0].NonAuthoritative,
"a lone zone is forced authoritative for pre-NonAuthoritative servers")
// The reverse zone the config gains afterwards must not feed back into the
// decision: the compat gate counts the zones the server sent.
require.Len(t, config.CustomZones, 2, "a reverse zone is appended for the overlay prefix")
assert.Equal(t, "64.100.in-addr.arpa.", config.CustomZones[1].Domain)
}
+1 -1
View File
@@ -519,7 +519,7 @@ func startManagement(t *testing.T, dataDir, testFile string) (*grpc.Server, stri
updateManager := update_channel.NewPeersUpdateManager(metrics)
requestBuffer := server.NewAccountRequestBuffer(context.Background(), store)
networkMapController := controller.NewController(context.Background(), store, metrics, updateManager, requestBuffer, server.MockIntegratedValidator{}, settingsMockManager, "netbird.selfhosted", port_forwarding.NewControllerMock(), manager.NewEphemeralManager(store, peersManager), config)
networkMapController := controller.NewController(context.Background(), store, metrics, updateManager, requestBuffer, server.MockIntegratedValidator{}, settingsMockManager, "netbird.selfhosted", port_forwarding.NewControllerMock(), manager.NewEphemeralManager(store, peersManager), config, nil)
accountManager, err := server.BuildManager(context.Background(), config, store, networkMapController, jobManager, nil, "", eventStore, nil, false, ia, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false, cacheStore)
if err != nil {
return nil, "", err
+4
View File
@@ -109,6 +109,10 @@
// - Does NOT remove result.json (cleaned by ResultHandler after read)
// - Does NOT remove msi.log (kept for debugging)
//
// On Windows the updater copy is often still locked when the daemon it restarted
// runs cleanup, so removing it is retried briefly and otherwise left in place for
// the next update to overwrite rather than reported as a failure.
//
// # Dry-Run Mode
//
// Dry-run mode allows testing the update process without actually installing:
@@ -0,0 +1,67 @@
package installer
import (
"os"
"path/filepath"
"testing"
"time"
"golang.org/x/sys/windows"
)
// lockFile opens path without FILE_SHARE_DELETE, so os.Remove fails the way it does
// while the updater process still holds its own image.
func lockFile(t *testing.T, path string) windows.Handle {
t.Helper()
p, err := windows.UTF16PtrFromString(path)
if err != nil {
t.Fatalf("convert path: %v", err)
}
handle, err := windows.CreateFile(p, windows.GENERIC_READ, windows.FILE_SHARE_READ, nil, windows.OPEN_EXISTING, windows.FILE_ATTRIBUTE_NORMAL, 0)
if err != nil {
t.Fatalf("lock %s: %v", path, err)
}
return handle
}
// releaseAfter closes the handle once the delay has passed, standing in for the
// updater process finally exiting.
func releaseAfter(t *testing.T, handle windows.Handle, delay time.Duration) {
t.Helper()
released := make(chan struct{})
t.Cleanup(func() { <-released })
go func() {
defer close(released)
time.Sleep(delay)
if err := windows.CloseHandle(handle); err != nil {
t.Errorf("close handle: %v", err)
}
}()
}
// TestCleanUpInstallerFilesLockedUpdater covers the post-update cleanup race: the
// daemon cleans up at startup while the updater that restarted it is still exiting,
// so the updater image is locked and Windows refuses the delete. Cleanup must wait
// the lock out instead of reporting a failure and leaving the binary behind.
func TestCleanUpInstallerFilesLockedUpdater(t *testing.T) {
tempDir := t.TempDir()
path := filepath.Join(tempDir, updaterBinary)
if err := os.WriteFile(path, []byte("x"), 0o600); err != nil {
t.Fatalf("write updater: %v", err)
}
releaseAfter(t, lockFile(t, path), 300*time.Millisecond)
u := NewWithDir(tempDir)
if err := u.CleanUpInstallerFiles(); err != nil {
t.Fatalf("cleanup must tolerate a still-locked updater: %v", err)
}
if _, err := os.Stat(path); !os.IsNotExist(err) {
t.Errorf("updater binary still present (stat err: %v)", err)
}
}
@@ -152,8 +152,8 @@ func (u *Installer) CleanUpInstallerFiles() error {
var merr *multierror.Error
if err := os.Remove(filepath.Join(u.tempDir, updaterBinary)); err != nil && !os.IsNotExist(err) {
merr = multierror.Append(merr, fmt.Errorf("failed to remove updater binary: %w", err))
if err := removeUpdaterBinary(filepath.Join(u.tempDir, updaterBinary)); err != nil {
merr = multierror.Append(merr, fmt.Errorf("remove updater binary: %w", err))
}
entries, err := os.ReadDir(u.tempDir)
@@ -167,10 +167,16 @@ func (u *Installer) CleanUpInstallerFiles() error {
}
name := entry.Name()
// The updater copy is handled above; on Windows its name also matches the
// extension sweep, which would report the same file twice.
if strings.EqualFold(name, updaterBinary) {
continue
}
for _, ext := range binaryExtensions {
if strings.HasSuffix(strings.ToLower(name), strings.ToLower(ext)) {
if err := os.Remove(filepath.Join(u.tempDir, name)); err != nil {
merr = multierror.Append(merr, fmt.Errorf("failed to remove %s: %w", name, err))
merr = multierror.Append(merr, fmt.Errorf("remove %s: %w", name, err))
}
break
}
@@ -0,0 +1,52 @@
//go:build windows || darwin
package installer
import (
"os"
"path/filepath"
"testing"
)
// TestCleanUpInstallerFiles checks that cleanup removes the updater copy and the
// downloaded installer while leaving the logs and the result file for the daemon.
func TestCleanUpInstallerFiles(t *testing.T) {
tempDir := t.TempDir()
installers := make([]string, 0, len(binaryExtensions))
for _, ext := range binaryExtensions {
installers = append(installers, "netbird_installer."+ext)
}
kept := []string{"installer.log", "result.json"}
for _, name := range append(append([]string{updaterBinary}, installers...), kept...) {
if err := os.WriteFile(filepath.Join(tempDir, name), []byte("x"), 0o600); err != nil {
t.Fatalf("write %s: %v", name, err)
}
}
u := NewWithDir(tempDir)
if err := u.CleanUpInstallerFiles(); err != nil {
t.Fatalf("CleanUpInstallerFiles: %v", err)
}
for _, name := range append([]string{updaterBinary}, installers...) {
if _, err := os.Stat(filepath.Join(tempDir, name)); !os.IsNotExist(err) {
t.Errorf("%s was not removed (stat err: %v)", name, err)
}
}
for _, name := range kept {
if _, err := os.Stat(filepath.Join(tempDir, name)); err != nil {
t.Errorf("%s should have been kept: %v", name, err)
}
}
}
func TestCleanUpInstallerFilesMissingTempDir(t *testing.T) {
u := NewWithDir(filepath.Join(t.TempDir(), "does-not-exist"))
if err := u.CleanUpInstallerFiles(); err != nil {
t.Errorf("a missing temp dir is not a cleanup failure, got: %v", err)
}
}
@@ -0,0 +1,12 @@
package installer
import "os"
// removeUpdaterBinary deletes the updater copy left in the temp dir. On darwin a
// running binary can be unlinked, so no retry is needed.
func removeUpdaterBinary(path string) error {
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
return err
}
return nil
}
@@ -0,0 +1,45 @@
package installer
import (
"errors"
"os"
"time"
log "github.com/sirupsen/logrus"
"golang.org/x/sys/windows"
)
const (
// The updater is the process that restarted the daemon, so when the daemon
// cleans up at startup the updater is often still exiting and Windows refuses
// to delete its locked image. These bound how long cleanup waits for it.
updaterRemoveAttempts = 5
updaterRemoveDelay = 200 * time.Millisecond
)
// removeUpdaterBinary deletes the updater copy left in the temp dir, retrying
// while the still-exiting updater process holds its image. A binary that stays
// locked for the whole window is left in place and reported at info level: the
// next update overwrites it, so it is not worth failing cleanup over.
func removeUpdaterBinary(path string) error {
for attempt := 0; attempt < updaterRemoveAttempts; attempt++ {
if attempt > 0 {
time.Sleep(updaterRemoveDelay)
}
err := os.Remove(path)
if err == nil || os.IsNotExist(err) {
return nil
}
if !isFileLocked(err) {
return err
}
}
log.Infof("updater binary %s is still locked, leaving it for the next update to overwrite", path)
return nil
}
func isFileLocked(err error) bool {
return errors.Is(err, windows.ERROR_ACCESS_DENIED) || errors.Is(err, windows.ERROR_SHARING_VIOLATION)
}
@@ -0,0 +1,59 @@
package installer
import (
"os"
"path/filepath"
"testing"
"time"
"golang.org/x/sys/windows"
)
func TestRemoveUpdaterBinaryRetriesWhileLocked(t *testing.T) {
path := filepath.Join(t.TempDir(), updaterBinary)
if err := os.WriteFile(path, []byte("x"), 0o600); err != nil {
t.Fatalf("write updater: %v", err)
}
releaseAfter(t, lockFile(t, path), updaterRemoveDelay+50*time.Millisecond)
if err := removeUpdaterBinary(path); err != nil {
t.Fatalf("removeUpdaterBinary: %v", err)
}
if _, err := os.Stat(path); !os.IsNotExist(err) {
t.Errorf("updater binary still present (stat err: %v)", err)
}
}
// TestRemoveUpdaterBinaryStaysLocked covers an updater that never releases its
// image within the retry window. Cleanup gives up quietly and leaves the file
// behind rather than reporting a failure.
func TestRemoveUpdaterBinaryStaysLocked(t *testing.T) {
path := filepath.Join(t.TempDir(), updaterBinary)
if err := os.WriteFile(path, []byte("x"), 0o600); err != nil {
t.Fatalf("write updater: %v", err)
}
handle := lockFile(t, path)
t.Cleanup(func() {
if err := windows.CloseHandle(handle); err != nil {
t.Errorf("close handle: %v", err)
}
})
if err := removeUpdaterBinary(path); err != nil {
t.Fatalf("a permanently locked updater is not a cleanup failure, got: %v", err)
}
if _, err := os.Stat(path); err != nil {
t.Errorf("locked updater binary should be left in place, stat: %v", err)
}
}
func TestRemoveUpdaterBinaryMissingFile(t *testing.T) {
path := filepath.Join(t.TempDir(), updaterBinary)
if err := removeUpdaterBinary(path); err != nil {
t.Errorf("a missing updater binary is not a failure, got: %v", err)
}
}
+110
View File
@@ -0,0 +1,110 @@
package net
import (
"fmt"
"os"
"strconv"
"strings"
log "github.com/sirupsen/logrus"
)
const (
// envFwmarkBase overrides the base of the fwmark range. Container network
// plugins, CNIs and other VPNs claim bits of the mark space for themselves,
// and a rule of theirs matching one of our bits acts on our traffic, so
// hosts running such software may need to move the range out of the way.
envFwmarkBase = "NB_FWMARK_BASE"
// defaultFwmarkBase is the base of the fwmark range used when the
// environment does not override it.
defaultFwmarkBase uint32 = 0x1BD00
// fwmarkOffsetMask is the part of a mark that identifies the individual mark
// within the range, so the base occupies everything above it.
fwmarkOffsetMask uint32 = 0xFF
)
// Offsets of the individual marks within the range.
const (
offsetControlPlane uint32 = 0x00
offsetDataPlaneIn uint32 = 0x10
offsetDataPlaneOut uint32 = 0x11
offsetRedirected uint32 = 0x20
offsetMasquerade uint32 = 0x21
offsetMasqueradeReturn uint32 = 0x22
offsetDataPlaneLower uint32 = 0x10
offsetDataPlaneUpper uint32 = fwmarkOffsetMask
)
var (
fwmarkBase = loadFwmarkBase()
// ControlPlaneMark is the fwmark value used to mark packets that should not be routed through the NetBird interface to
// avoid routing loops.
// This includes all control plane traffic (mgmt, signal, flows), relay, ICE/stun/turn and everything that is emitted by the wireguard socket.
// It doesn't collide with the other marks, as the others are used for data plane traffic only.
ControlPlaneMark = fwmarkBase | offsetControlPlane
// DataPlaneMarkLower is the lowest value for the data plane range
DataPlaneMarkLower = fwmarkBase | offsetDataPlaneLower
// DataPlaneMarkUpper is the highest value for the data plane range
DataPlaneMarkUpper = fwmarkBase | offsetDataPlaneUpper
// DataPlaneMarkIn is the mark for inbound data plane traffic.
DataPlaneMarkIn = fwmarkBase | offsetDataPlaneIn
// DataPlaneMarkOut is the mark for outbound data plane traffic.
DataPlaneMarkOut = fwmarkBase | offsetDataPlaneOut
// PreroutingFwmarkRedirected is applied to packets that were redirected (input -> forward, e.g. by Docker or Podman) for special handling.
PreroutingFwmarkRedirected = fwmarkBase | offsetRedirected
// PreroutingFwmarkMasquerade is applied to packets that arrive from the NetBird interface and should be masqueraded.
PreroutingFwmarkMasquerade = fwmarkBase | offsetMasquerade
// PreroutingFwmarkMasqueradeReturn is applied to packets that will leave through the NetBird interface and should be masqueraded.
PreroutingFwmarkMasqueradeReturn = fwmarkBase | offsetMasqueradeReturn
)
// IsDataPlaneMark determines if a fwmark is in the data plane range.
func IsDataPlaneMark(fwmark uint32) bool {
return fwmark >= DataPlaneMarkLower && fwmark <= DataPlaneMarkUpper
}
func loadFwmarkBase() uint32 {
val := os.Getenv(envFwmarkBase)
if val == "" {
return defaultFwmarkBase
}
base, err := parseFwmarkBase(val)
if err != nil {
log.Warnf("failed to parse %s=%q, using the default range: %v", envFwmarkBase, val, err)
return defaultFwmarkBase
}
log.Infof("using fwmark range %#x-%#x from %s", base, base|fwmarkOffsetMask, envFwmarkBase)
return base
}
// parseFwmarkBase reads a mark range base. The low byte of a mark identifies the
// individual mark within the range, so a base has to leave it free.
func parseFwmarkBase(val string) (uint32, error) {
val = strings.TrimSpace(val)
base, err := strconv.ParseUint(val, 0, 32)
if err != nil {
return 0, fmt.Errorf("not a 32 bit number: %w", err)
}
if base == 0 {
return 0, fmt.Errorf("base must not be zero")
}
if uint32(base)&fwmarkOffsetMask != 0 {
return 0, fmt.Errorf("base %#x must leave the low byte free", base)
}
return uint32(base), nil
}
+111
View File
@@ -0,0 +1,111 @@
package net
import (
"testing"
)
func TestParseFwmarkBase(t *testing.T) {
tests := []struct {
name string
val string
want uint32
wantErr bool
}{
{name: "hex", val: "0x5A000", want: 0x5A000},
{name: "hex upper case", val: "0X5A000", want: 0x5A000},
{name: "decimal", val: "65536", want: 65536},
{name: "octal", val: "0o400", want: 0o400},
{name: "surrounding space", val: " 0x5A000 ", want: 0x5A000},
{name: "highest usable base", val: "0xFFFFFF00", want: 0xFFFFFF00},
{name: "low byte in use", val: "0x1BD01", wantErr: true},
{name: "zero", val: "0", wantErr: true},
{name: "not a number", val: "wireguard", wantErr: true},
{name: "wider than 32 bit", val: "0x1FFFFFFFF", wantErr: true},
{name: "negative", val: "-0x100", wantErr: true},
{name: "empty", val: "", wantErr: true},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got, err := parseFwmarkBase(tc.val)
if tc.wantErr {
if err == nil {
t.Fatalf("parseFwmarkBase(%q) = %#x, want an error", tc.val, got)
}
return
}
if err != nil {
t.Fatalf("parseFwmarkBase(%q): %v", tc.val, err)
}
if got != tc.want {
t.Errorf("parseFwmarkBase(%q) = %#x, want %#x", tc.val, got, tc.want)
}
})
}
}
// The marks have to stay inside the range the base defines, otherwise a host
// that moved the range to dodge a collision would still emit the old values.
func TestMarksStayWithinTheRange(t *testing.T) {
lower, upper := fwmarkBase, fwmarkBase|fwmarkOffsetMask
marks := map[string]uint32{
"ControlPlaneMark": ControlPlaneMark,
"DataPlaneMarkLower": DataPlaneMarkLower,
"DataPlaneMarkUpper": DataPlaneMarkUpper,
"DataPlaneMarkIn": DataPlaneMarkIn,
"DataPlaneMarkOut": DataPlaneMarkOut,
"PreroutingFwmarkRedirected": PreroutingFwmarkRedirected,
"PreroutingFwmarkMasquerade": PreroutingFwmarkMasquerade,
"PreroutingFwmarkMasqueradeReturn": PreroutingFwmarkMasqueradeReturn,
}
for name, mark := range marks {
if mark < lower || mark > upper {
t.Errorf("%s = %#x, outside the range %#x-%#x", name, mark, lower, upper)
}
}
// the control plane mark must stay out of the data plane range, the netflow
// conntrack path tells them apart by it
if IsDataPlaneMark(ControlPlaneMark) {
t.Errorf("ControlPlaneMark %#x is inside the data plane range", ControlPlaneMark)
}
for name, mark := range map[string]uint32{
"DataPlaneMarkIn": DataPlaneMarkIn,
"DataPlaneMarkOut": DataPlaneMarkOut,
"PreroutingFwmarkRedirected": PreroutingFwmarkRedirected,
"PreroutingFwmarkMasquerade": PreroutingFwmarkMasquerade,
"PreroutingFwmarkMasqueradeReturn": PreroutingFwmarkMasqueradeReturn,
} {
if !IsDataPlaneMark(mark) {
t.Errorf("%s = %#x is outside the data plane range %#x-%#x", name, mark, DataPlaneMarkLower, DataPlaneMarkUpper)
}
}
}
func TestDefaultMarksAreUnchanged(t *testing.T) {
tests := map[string]struct {
got uint32
want uint32
}{
"ControlPlaneMark": {ControlPlaneMark, 0x1BD00},
"DataPlaneMarkLower": {DataPlaneMarkLower, 0x1BD10},
"DataPlaneMarkUpper": {DataPlaneMarkUpper, 0x1BDFF},
"DataPlaneMarkIn": {DataPlaneMarkIn, 0x1BD10},
"DataPlaneMarkOut": {DataPlaneMarkOut, 0x1BD11},
"PreroutingFwmarkRedirected": {PreroutingFwmarkRedirected, 0x1BD20},
"PreroutingFwmarkMasquerade": {PreroutingFwmarkMasquerade, 0x1BD21},
"PreroutingFwmarkMasqueradeReturn": {PreroutingFwmarkMasqueradeReturn, 0x1BD22},
}
if fwmarkBase != defaultFwmarkBase {
t.Skipf("%s is set, the defaults do not apply", envFwmarkBase)
}
for name, tc := range tests {
if tc.got != tc.want {
t.Errorf("%s = %#x, want %#x", name, tc.got, tc.want)
}
}
}
-35
View File
@@ -7,41 +7,6 @@ import (
"net/netip"
)
const (
// ControlPlaneMark is the fwmark value used to mark packets that should not be routed through the NetBird interface to
// avoid routing loops.
// This includes all control plane traffic (mgmt, signal, flows), relay, ICE/stun/turn and everything that is emitted by the wireguard socket.
// It doesn't collide with the other marks, as the others are used for data plane traffic only.
ControlPlaneMark = 0x1BD00
// Data plane marks (0x1BD10 - 0x1BDFF)
// DataPlaneMarkLower is the lowest value for the data plane range
DataPlaneMarkLower = 0x1BD10
// DataPlaneMarkUpper is the highest value for the data plane range
DataPlaneMarkUpper = 0x1BDFF
// DataPlaneMarkIn is the mark for inbound data plane traffic.
DataPlaneMarkIn = 0x1BD10
// DataPlaneMarkOut is the mark for outbound data plane traffic.
DataPlaneMarkOut = 0x1BD11
// PreroutingFwmarkRedirected is applied to packets that are were redirected (input -> forward, e.g. by Docker or Podman) for special handling.
PreroutingFwmarkRedirected = 0x1BD20
// PreroutingFwmarkMasquerade is applied to packets that arrive from the NetBird interface and should be masqueraded.
PreroutingFwmarkMasquerade = 0x1BD21
// PreroutingFwmarkMasqueradeReturn is applied to packets that will leave through the NetBird interface and should be masqueraded.
PreroutingFwmarkMasqueradeReturn = 0x1BD22
)
// IsDataPlaneMark determines if a fwmark is in the data plane range (0x1BD10-0x1BDFF)
func IsDataPlaneMark(fwmark uint32) bool {
return fwmark >= DataPlaneMarkLower && fwmark <= DataPlaneMarkUpper
}
func GetLastIPFromNetwork(network netip.Prefix, fromEnd int) (netip.Addr, error) {
var endIP net.IP
addr := network.Addr().AsSlice()
+1 -10
View File
@@ -21,15 +21,6 @@ func SetSocketMark(conn syscall.Conn) error {
return setRawSocketMark(sysconn)
}
// SetSocketOpt sets the SO_MARK option on the given file descriptor
func SetSocketOpt(fd int) error {
if !AdvancedRouting() {
return nil
}
return setSocketOptInt(fd)
}
func setRawSocketMark(conn syscall.RawConn) error {
var setErr error
@@ -51,5 +42,5 @@ func setRawSocketMark(conn syscall.RawConn) error {
}
func setSocketOptInt(fd int) error {
return syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_MARK, ControlPlaneMark)
return syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_MARK, int(ControlPlaneMark))
}
-1
View File
@@ -232,4 +232,3 @@ func toNetIDs(routes []string) []route.NetID {
}
return netIDs
}
+1 -1
View File
@@ -200,7 +200,7 @@ func startManagement(t *testing.T, signalAddr string, counter *int) (*grpc.Serve
requestBuffer := server.NewAccountRequestBuffer(context.Background(), store)
peersUpdateManager := update_channel.NewPeersUpdateManager(metrics)
networkMapController := controller.NewController(context.Background(), store, metrics, peersUpdateManager, requestBuffer, server.MockIntegratedValidator{}, settingsMockManager, "netbird.selfhosted", port_forwarding.NewControllerMock(), manager.NewEphemeralManager(store, peersManager), config)
networkMapController := controller.NewController(context.Background(), store, metrics, peersUpdateManager, requestBuffer, server.MockIntegratedValidator{}, settingsMockManager, "netbird.selfhosted", port_forwarding.NewControllerMock(), manager.NewEphemeralManager(store, peersManager), config, nil)
accountManager, err := server.BuildManager(context.Background(), config, store, networkMapController, jobManager, nil, "", eventStore, nil, false, ia, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManagerMock, false, cacheStore)
if err != nil {
return nil, "", err