Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
d5d416e
proto: add ConnectionMode enum and p2p/relay timeout fields to PeerCo…
MichaelUray May 1, 2026
b341bc0
client: add connectionmode package with Mode type and proto bridge
MichaelUray May 1, 2026
9cad8d6
client/peer: ResolveModeFromEnv with NB_CONNECTION_MODE and deprecati…
MichaelUray May 1, 2026
a4b0d04
client: add --connection-mode, --relay-timeout, --p2p-timeout CLI flags
MichaelUray May 1, 2026
62f29c5
client/conn_mgr: replace asymmetric Lazy/ForceRelay precedence with Mode
MichaelUray May 1, 2026
0d48124
client/peer: connection mode drives skip-ICE branch in Open()
MichaelUray May 1, 2026
6d40418
client/engine: forward resolved Mode to per-peer ConnConfig
MichaelUray May 1, 2026
be96444
mgmt/types: add ConnectionMode + p2p/relay timeout to Settings
MichaelUray May 1, 2026
92885d4
openapi: add connection_mode + p2p/relay timeout fields to AccountSet…
MichaelUray May 1, 2026
b00fee3
mgmt/handlers/accounts: accept connection_mode + timeout settings on PUT
MichaelUray May 1, 2026
12ec653
mgmt/activity: add three new account-scoped event codes
MichaelUray May 1, 2026
ddd53a3
mgmt/account: emit audit events for connection_mode + timeout changes
MichaelUray May 1, 2026
68bf8e3
move connectionmode package + extend toPeerConfig to fill new wire fi…
MichaelUray May 1, 2026
c272786
mgmt/grpc: tests for toPeerConfig connection-mode resolution
MichaelUray May 1, 2026
09c77eb
mgmt/account: trigger updateAccountPeers on ConnectionMode + timeout …
MichaelUray May 2, 2026
ed3e23a
mgmt/handlers/accounts: validate timeout bounds + clarify ConnectionM…
MichaelUray May 2, 2026
885f278
client/peer/env: bound NB_LAZY_CONN_INACTIVITY_THRESHOLD parse + code…
MichaelUray May 2, 2026
85f19fb
client/profile: persist RelayTimeoutSeconds + P2pTimeoutSeconds as *u…
MichaelUray May 2, 2026
5ef9b09
shared/connectionmode: accept "relay" as alias for "relay-forced"
MichaelUray May 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions client/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ const (
extraIFaceBlackListFlag = "extra-iface-blacklist"
dnsRouteIntervalFlag = "dns-router-interval"
enableLazyConnectionFlag = "enable-lazy-connection"
connectionModeFlag = "connection-mode"
relayTimeoutFlag = "relay-timeout"
p2pTimeoutFlag = "p2p-timeout"
mtuFlag = "mtu"
)

Expand Down Expand Up @@ -72,6 +75,9 @@ var (
anonymizeFlag bool
dnsRouteInterval time.Duration
lazyConnEnabled bool
connectionMode string
relayTimeoutSecs uint32
p2pTimeoutSecs uint32
mtu uint16
profilesDisabled bool
updateSettingsDisabled bool
Expand Down Expand Up @@ -191,6 +197,13 @@ func init() {
upCmd.PersistentFlags().BoolVar(&rosenpassPermissive, rosenpassPermissiveFlag, false, "[Experimental] Enable Rosenpass in permissive mode to allow this peer to accept WireGuard connections without requiring Rosenpass functionality from peers that do not have Rosenpass enabled.")
upCmd.PersistentFlags().BoolVar(&autoConnectDisabled, disableAutoConnectFlag, false, "Disables auto-connect feature. If enabled, then the client won't connect automatically when the service starts.")
upCmd.PersistentFlags().BoolVar(&lazyConnEnabled, enableLazyConnectionFlag, false, "[Experimental] Enable the lazy connection feature. If enabled, the client will establish connections on-demand. Note: this setting may be overridden by management configuration.")
upCmd.PersistentFlags().StringVar(&connectionMode, connectionModeFlag, "",
"[Experimental] Peer connection mode: relay-forced, p2p, p2p-lazy, p2p-dynamic, or follow-server. "+
"Overrides the server-pushed value when set. Use follow-server to clear a previously-set local override.")
upCmd.PersistentFlags().Uint32Var(&relayTimeoutSecs, relayTimeoutFlag, 0,
"[Experimental] Relay-worker idle timeout in seconds. 0 = use server-pushed value (or built-in default).")
upCmd.PersistentFlags().Uint32Var(&p2pTimeoutSecs, p2pTimeoutFlag, 0,
"[Experimental] ICE-worker idle timeout in seconds. 0 = use server-pushed value (or built-in default). Only effective in p2p-dynamic mode (Phase 2).")

}

Expand Down
30 changes: 30 additions & 0 deletions client/cmd/up.go
Original file line number Diff line number Diff line change
Expand Up @@ -434,10 +434,20 @@
req.LazyConnectionEnabled = &lazyConnEnabled
}

if cmd.Flag(connectionModeFlag).Changed {
req.ConnectionMode = &connectionMode
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if cmd.Flag(relayTimeoutFlag).Changed {
req.RelayTimeoutSeconds = &relayTimeoutSecs
}
if cmd.Flag(p2pTimeoutFlag).Changed {
req.P2PTimeoutSeconds = &p2pTimeoutSecs
}

return &req
}

func setupConfig(customDNSAddressConverted []byte, cmd *cobra.Command, configFilePath string) (*profilemanager.ConfigInput, error) {

Check warning on line 450 in client/cmd/up.go

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

This function has 102 lines of code, which is greater than the 100 authorized. Split it into smaller functions.

See more on https://sonarcloud.io/project/issues?id=netbirdio_netbird&issues=AZ3j2yoBB7jd3e8SJBYm&open=AZ3j2yoBB7jd3e8SJBYm&pullRequest=6047
ic := profilemanager.ConfigInput{
ManagementURL: managementURL,
ConfigPath: configFilePath,
Expand Down Expand Up @@ -550,10 +560,20 @@
if cmd.Flag(enableLazyConnectionFlag).Changed {
ic.LazyConnectionEnabled = &lazyConnEnabled
}

if cmd.Flag(connectionModeFlag).Changed {
ic.ConnectionMode = &connectionMode
}
if cmd.Flag(relayTimeoutFlag).Changed {
ic.RelayTimeoutSeconds = &relayTimeoutSecs
}
if cmd.Flag(p2pTimeoutFlag).Changed {
ic.P2pTimeoutSeconds = &p2pTimeoutSecs
}
return &ic, nil
}

func setupLoginRequest(providedSetupKey string, customDNSAddressConverted []byte, cmd *cobra.Command) (*proto.LoginRequest, error) {

Check warning on line 576 in client/cmd/up.go

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

This function has 102 lines of code, which is greater than the 100 authorized. Split it into smaller functions.

See more on https://sonarcloud.io/project/issues?id=netbirdio_netbird&issues=AZ3j2yoBB7jd3e8SJBYn&open=AZ3j2yoBB7jd3e8SJBYn&pullRequest=6047
loginRequest := proto.LoginRequest{
SetupKey: providedSetupKey,
ManagementUrl: managementURL,
Expand Down Expand Up @@ -664,6 +684,16 @@
if cmd.Flag(enableLazyConnectionFlag).Changed {
loginRequest.LazyConnectionEnabled = &lazyConnEnabled
}

if cmd.Flag(connectionModeFlag).Changed {
loginRequest.ConnectionMode = &connectionMode
}
if cmd.Flag(relayTimeoutFlag).Changed {
loginRequest.RelayTimeoutSeconds = &relayTimeoutSecs
}
if cmd.Flag(p2pTimeoutFlag).Changed {
loginRequest.P2PTimeoutSeconds = &p2pTimeoutSecs
}
return &loginRequest, nil
}

Expand Down
162 changes: 127 additions & 35 deletions client/internal/conn_mgr.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@
"github.com/netbirdio/netbird/client/internal/lazyconn"
"github.com/netbirdio/netbird/client/internal/lazyconn/manager"
"github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/shared/connectionmode"
"github.com/netbirdio/netbird/client/internal/peerstore"
"github.com/netbirdio/netbird/route"
mgmProto "github.com/netbirdio/netbird/shared/management/proto"
)

// ConnMgr coordinates both lazy connections (established on-demand) and permanent peer connections.
Expand All @@ -28,9 +30,19 @@
peerStore *peerstore.Store
statusRecorder *peer.Status
iface lazyconn.WGIface
enabledLocally bool
rosenpassEnabled bool

// Resolved values used to drive lifecycle decisions. Updated when
// the management server pushes a new PeerConfig.
mode connectionmode.Mode
relayTimeoutSecs uint32

// Raw inputs kept so we can re-resolve when server-pushed value changes.
envMode connectionmode.Mode
envRelayTimeout uint32
cfgMode connectionmode.Mode
cfgRelayTimeout uint32

lazyConnMgr *manager.Manager

wg sync.WaitGroup
Expand All @@ -39,72 +51,140 @@
}

func NewConnMgr(engineConfig *EngineConfig, statusRecorder *peer.Status, peerStore *peerstore.Store, iface lazyconn.WGIface) *ConnMgr {
e := &ConnMgr{
envMode, envRelayTimeout := peer.ResolveModeFromEnv()

// First-pass resolution without server input -- updated later when
// the first NetworkMap arrives via UpdatedRemotePeerConfig.
mode, relayTimeout := resolveConnectionMode(
envMode, envRelayTimeout,
engineConfig.ConnectionMode, engineConfig.RelayTimeoutSeconds,
nil,
)

return &ConnMgr{
peerStore: peerStore,
statusRecorder: statusRecorder,
iface: iface,
rosenpassEnabled: engineConfig.RosenpassEnabled,
mode: mode,
relayTimeoutSecs: relayTimeout,
envMode: envMode,
envRelayTimeout: envRelayTimeout,
cfgMode: engineConfig.ConnectionMode,
cfgRelayTimeout: engineConfig.RelayTimeoutSeconds,
}
}

// resolveConnectionMode applies the spec-section-4.1 precedence chain:
// 1. client env (already resolved by caller via peer.ResolveModeFromEnv)
// 2. client config (from profile, including the FollowServer sentinel)
// 3. server-pushed PeerConfig.ConnectionMode (with UNSPECIFIED ->
// legacy LazyConnectionEnabled fallback)
//
// Returns the resolved Mode and the resolved relay-timeout in seconds
// (0 = use built-in default at the call site).
func resolveConnectionMode(
envMode connectionmode.Mode,
envRelayTimeout uint32,
cfgMode connectionmode.Mode,
cfgRelayTimeout uint32,
serverPC *mgmProto.PeerConfig,
) (connectionmode.Mode, uint32) {
mode := envMode
if mode == connectionmode.ModeUnspecified {
if cfgMode != connectionmode.ModeUnspecified && cfgMode != connectionmode.ModeFollowServer {
mode = cfgMode
}
}
if engineConfig.LazyConnectionEnabled || lazyconn.IsLazyConnEnabledByEnv() {
e.enabledLocally = true
if mode == connectionmode.ModeUnspecified {
if serverPC != nil {
serverMode := connectionmode.FromProto(serverPC.GetConnectionMode())
if serverMode != connectionmode.ModeUnspecified {
mode = serverMode
} else {
mode = connectionmode.ResolveLegacyLazyBool(serverPC.GetLazyConnectionEnabled())
}
} else {
mode = connectionmode.ModeP2P // safe default when nothing at all is known
}
}
return e

// Relay-timeout precedence (analog).
relay := envRelayTimeout
if relay == 0 {
relay = cfgRelayTimeout
}
if relay == 0 && serverPC != nil {
relay = serverPC.GetRelayTimeoutSeconds()
}

return mode, relay
}

// Start initializes the connection manager and starts the lazy connection manager if enabled by env var or cmd line option.
// Start initializes the connection manager. If the resolved Mode at
// daemon startup is ModeP2PLazy, the lazy connection manager is brought
// up immediately; otherwise it stays dormant until UpdatedRemotePeerConfig
// transitions into lazy mode.
func (e *ConnMgr) Start(ctx context.Context) {
if e.lazyConnMgr != nil {
log.Errorf("lazy connection manager is already started")
return
}

if !e.enabledLocally {
log.Infof("lazy connection manager is disabled")
if e.mode != connectionmode.ModeP2PLazy {
log.Infof("lazy connection manager is disabled (mode=%s)", e.mode)
return
}

if e.rosenpassEnabled {
log.Warnf("rosenpass connection manager is enabled, lazy connection manager will not be started")
log.Warnf("rosenpass enabled, lazy connection manager will not be started")
return
}

e.initLazyManager(ctx)
e.statusRecorder.UpdateLazyConnection(true)
}

// UpdatedRemoteFeatureFlag is called when the remote feature flag is updated.
// If enabled, it initializes the lazy connection manager and start it. Do not need to call Start() again.
// If disabled, then it closes the lazy connection manager and open the connections to all peers.
func (e *ConnMgr) UpdatedRemoteFeatureFlag(ctx context.Context, enabled bool) error {
// do not disable lazy connection manager if it was enabled by env var
if e.enabledLocally {
// UpdatedRemotePeerConfig is called when the management server pushes a
// new PeerConfig. Re-resolves the effective mode through the precedence
// chain and starts/stops the lazy manager accordingly.
func (e *ConnMgr) UpdatedRemotePeerConfig(ctx context.Context, pc *mgmProto.PeerConfig) error {
newMode, newRelay := resolveConnectionMode(e.envMode, e.envRelayTimeout, e.cfgMode, e.cfgRelayTimeout, pc)

if newMode == e.mode && newRelay == e.relayTimeoutSecs {
return nil
}

if enabled {
// if the lazy connection manager is already started, do not start it again
if e.lazyConnMgr != nil {
return nil
}

prev := e.mode
e.mode = newMode
e.relayTimeoutSecs = newRelay

wasLazy := prev == connectionmode.ModeP2PLazy
isLazy := newMode == connectionmode.ModeP2PLazy
switch {
case !wasLazy && isLazy:

Check warning on line 161 in client/internal/conn_mgr.go

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Reduce this case clause number of lines from 11 to at most 10, for example by extracting code into methods.

See more on https://sonarcloud.io/project/issues?id=netbirdio_netbird&issues=AZ3j2ynlB7jd3e8SJBYl&open=AZ3j2ynlB7jd3e8SJBYl&pullRequest=6047
if e.rosenpassEnabled {
log.Infof("rosenpass connection manager is enabled, lazy connection manager will not be started")
log.Warnf("rosenpass enabled, ignoring lazy mode push")
return nil
}

log.Warnf("lazy connection manager is enabled by management feature flag")
e.initLazyManager(ctx)
e.statusRecorder.UpdateLazyConnection(true)
return e.addPeersToLazyConnManager()
} else {
if e.lazyConnMgr == nil {
return nil
log.Infof("lazy connection manager enabled by management push (mode=%s)", newMode)
e.initLazyManager(ctx)
}
log.Infof("lazy connection manager is disabled by management feature flag")
e.statusRecorder.UpdateLazyConnection(true)
return e.addPeersToLazyConnManager()
case wasLazy && !isLazy:
log.Infof("lazy connection manager disabled by management push (mode=%s)", newMode)
e.closeManager(ctx)
e.statusRecorder.UpdateLazyConnection(false)
return nil
}
return nil
}

// UpdatedRemoteFeatureFlag is the legacy entry point that only knows the
// boolean LazyConnectionEnabled field. Kept as a thin shim that builds a
// synthetic PeerConfig and delegates to UpdatedRemotePeerConfig.
//
// Deprecated: callers should switch to UpdatedRemotePeerConfig and pass
// the real PeerConfig so the new ConnectionMode + timeouts propagate.
func (e *ConnMgr) UpdatedRemoteFeatureFlag(ctx context.Context, enabled bool) error {
return e.UpdatedRemotePeerConfig(ctx, &mgmProto.PeerConfig{LazyConnectionEnabled: enabled})
}

// UpdateRouteHAMap updates the route HA mappings in the lazy connection manager
Expand Down Expand Up @@ -309,6 +389,18 @@
return e.lazyConnMgr != nil && e.lazyCtxCancel != nil
}

// Mode returns the currently resolved connection mode. Used by the engine
// when constructing per-peer connections (Phase 1 forwards it into
// peer.ConnConfig in a follow-up commit).
func (e *ConnMgr) Mode() connectionmode.Mode {
return e.mode
}

// RelayTimeout returns the resolved relay-worker idle timeout in seconds.
func (e *ConnMgr) RelayTimeout() uint32 {
return e.relayTimeoutSecs
}

func inactivityThresholdEnv() *time.Duration {
envValue := os.Getenv(lazyconn.EnvInactivityThreshold)
if envValue == "" {
Expand Down
Loading
Loading