Support per-peer lazy connection state and default proxy peers to lazy

This commit is contained in:
Viktor Liu
2026-08-01 04:17:18 +09:00
committed by Viktor Liu
parent 0780a806f2
commit 707bc3ea62
15 changed files with 1252 additions and 1063 deletions
+44 -89
View File
@@ -14,6 +14,7 @@ import (
"github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/client/internal/peerstore" "github.com/netbirdio/netbird/client/internal/peerstore"
"github.com/netbirdio/netbird/route" "github.com/netbirdio/netbird/route"
mgmProto "github.com/netbirdio/netbird/shared/management/proto"
) )
// lazyForce is the resolved local decision for lazy connections, layered above the // lazyForce is the resolved local decision for lazy connections, layered above the
@@ -42,6 +43,9 @@ type ConnMgr struct {
iface lazyconn.WGIface iface lazyconn.WGIface
force lazyForce force lazyForce
rosenpassEnabled bool rosenpassEnabled bool
// remoteLazyEnabled caches the account-wide lazy feature flag from management.
// It is the default for peers that do not carry a per-peer lazy hint.
remoteLazyEnabled bool
lazyConnMgr *manager.Manager lazyConnMgr *manager.Manager
// lazyConnMgrMu guards the lazyConnMgr pointer for readers outside the // lazyConnMgrMu guards the lazyConnMgr pointer for readers outside the
@@ -75,69 +79,56 @@ func NewConnMgr(engineConfig *EngineConfig, statusRecorder *peer.Status, peerSto
return e return e
} }
// Start initializes the connection manager. It starts the lazy connection manager when a // Start initializes the connection manager. The lazy connection manager always runs so that
// local override forces it on; with no local override it waits for the management feature flag. // per-peer lazy defaults (e.g. proxy peers) work even when the account flag is off; the
// account flag and the local override decide the default lazy state per peer (see
// PeerLazyDefault). Rosenpass is the only condition that disables it.
func (e *ConnMgr) Start(ctx context.Context) { func (e *ConnMgr) Start(ctx context.Context) {
if e.lazyConnMgr != nil { if e.lazyConnMgr != nil {
log.Errorf("lazy connection manager is already started") log.Errorf("lazy connection manager is already started")
return return
} }
switch e.force {
case lazyForceOff:
log.Infof("lazy connection manager is disabled by local override (%s or MDM policy)", lazyconn.EnvLazyConn)
e.statusRecorder.UpdateLazyConnection(false)
return
case lazyForceNone:
log.Infof("lazy connection manager is managed by the management feature flag")
e.statusRecorder.UpdateLazyConnection(false)
return
}
if e.rosenpassEnabled { if e.rosenpassEnabled {
log.Warnf("rosenpass connection manager is enabled, lazy connection manager will not be started") log.Warnf("rosenpass is enabled, lazy connection manager will not be started")
e.statusRecorder.UpdateLazyConnection(false) e.statusRecorder.UpdateLazyConnection(false)
return return
} }
e.initLazyManager(ctx) e.initLazyManager(ctx)
e.statusRecorder.UpdateLazyConnection(true) e.statusRecorder.UpdateLazyConnection(e.PeerLazyDefault(mgmProto.LazyState_LazyStateDefault))
} }
// UpdatedRemoteFeatureFlag is called when the remote feature flag is updated. // UpdatedRemoteFeatureFlag caches the account-wide lazy feature flag. The manager itself is
// If enabled, it initializes the lazy connection manager and start it. Do not need to call Start() again. // not started or stopped here; the per-sync exclude-list reconciliation moves normal peers
// If disabled, then it closes the lazy connection manager and open the connections to all peers. // between the lazy and always-active sets when the flag flips.
func (e *ConnMgr) UpdatedRemoteFeatureFlag(ctx context.Context, enabled bool) error { func (e *ConnMgr) UpdatedRemoteFeatureFlag(_ context.Context, enabled bool) error {
// a local override (NB_LAZY_CONN or local config) takes precedence over management e.remoteLazyEnabled = enabled
if e.force != lazyForceNone { if e.isStartedWithLazyMgr() {
return nil e.statusRecorder.UpdateLazyConnection(e.PeerLazyDefault(mgmProto.LazyState_LazyStateDefault))
}
return nil
}
// PeerLazyDefault reports whether a peer should be lazy. The local override
// (NB_LAZY_CONN/MDM) wins over everything; without a local override the
// management per-peer state applies (LazyStateLazy/Eager force the decision),
// and LazyStateDefault follows the account-wide flag.
func (e *ConnMgr) PeerLazyDefault(state mgmProto.LazyState) bool {
switch e.force {
case lazyForceOn:
return true
case lazyForceOff:
return false
} }
if enabled { switch state {
// if the lazy connection manager is already started, do not start it again case mgmProto.LazyState_LazyStateLazy:
if e.lazyConnMgr != nil { return true
return nil case mgmProto.LazyState_LazyStateEager:
} return false
default:
if e.rosenpassEnabled { return e.remoteLazyEnabled
log.Infof("rosenpass connection manager is enabled, lazy connection manager will not be started")
e.statusRecorder.UpdateLazyConnection(false)
return nil
}
log.Infof("lazy connection manager is enabled by the management feature flag")
e.initLazyManager(ctx)
e.statusRecorder.UpdateLazyConnection(true)
return e.addPeersToLazyConnManager()
} else {
if e.lazyConnMgr == nil {
e.statusRecorder.UpdateLazyConnection(false)
return nil
}
log.Infof("lazy connection manager is disabled by management feature flag")
e.closeManager(ctx)
e.statusRecorder.UpdateLazyConnection(false)
return nil
} }
} }
@@ -192,12 +183,16 @@ func (e *ConnMgr) SetExcludeList(ctx context.Context, peerIDs map[string]bool) {
} }
} }
func (e *ConnMgr) AddPeerConn(ctx context.Context, peerKey string, conn *peer.Conn) (exists bool) { // AddPeerConn registers a peer connection. permanent requests an always-active connection
// (the peer belongs to the exclude set: a forwarder, or a peer that is not lazy by policy).
// Non-permanent peers are handed to the lazy manager. The subsequent SetExcludeList call
// reconciles membership for existing peers across flag flips.
func (e *ConnMgr) AddPeerConn(ctx context.Context, peerKey string, conn *peer.Conn, permanent bool) (exists bool) {
if success := e.peerStore.AddPeerConn(peerKey, conn); !success { if success := e.peerStore.AddPeerConn(peerKey, conn); !success {
return true return true
} }
if !e.isStartedWithLazyMgr() { if !e.isStartedWithLazyMgr() || permanent {
if err := conn.Open(ctx); err != nil { if err := conn.Open(ctx); err != nil {
conn.Log.Errorf("failed to open connection: %v", err) conn.Log.Errorf("failed to open connection: %v", err)
} }
@@ -316,46 +311,6 @@ func (e *ConnMgr) initLazyManager(engineCtx context.Context) {
}() }()
} }
func (e *ConnMgr) addPeersToLazyConnManager() error {
peers := e.peerStore.PeersPubKey()
lazyPeerCfgs := make([]lazyconn.PeerConfig, 0, len(peers))
for _, peerID := range peers {
var peerConn *peer.Conn
var exists bool
if peerConn, exists = e.peerStore.PeerConn(peerID); !exists {
log.Warnf("failed to find peer conn for peerID: %s", peerID)
continue
}
lazyPeerCfg := lazyconn.PeerConfig{
PublicKey: peerID,
AllowedIPs: peerConn.WgConfig().AllowedIps,
PeerConnID: peerConn.ConnID(),
Log: peerConn.Log,
}
lazyPeerCfgs = append(lazyPeerCfgs, lazyPeerCfg)
}
return e.lazyConnMgr.AddActivePeers(lazyPeerCfgs)
}
func (e *ConnMgr) closeManager(ctx context.Context) {
if e.lazyConnMgr == nil {
return
}
e.lazyCtxCancel()
e.wg.Wait()
e.lazyConnMgrMu.Lock()
e.lazyConnMgr = nil
e.lazyConnMgrMu.Unlock()
for _, peerID := range e.peerStore.PeersPubKey() {
e.peerStore.PeerConnOpen(ctx, peerID)
}
}
func (e *ConnMgr) isStartedWithLazyMgr() bool { func (e *ConnMgr) isStartedWithLazyMgr() bool {
return e.lazyConnMgr != nil && e.lazyCtxCancel != nil return e.lazyConnMgr != nil && e.lazyCtxCancel != nil
} }
+88
View File
@@ -16,6 +16,7 @@ import (
"github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/client/internal/peerstore" "github.com/netbirdio/netbird/client/internal/peerstore"
"github.com/netbirdio/netbird/monotime" "github.com/netbirdio/netbird/monotime"
mgmProto "github.com/netbirdio/netbird/shared/management/proto"
) )
func TestResolveLazyForce(t *testing.T) { func TestResolveLazyForce(t *testing.T) {
@@ -138,4 +139,91 @@ func TestInactivityThresholdEnv(t *testing.T) {
} }
} }
func TestPeerLazyDefault(t *testing.T) {
tests := []struct {
name string
force lazyForce
remoteEnabled bool
state mgmProto.LazyState
want bool
}{
{name: "force on wins over eager state", force: lazyForceOn, state: mgmProto.LazyState_LazyStateEager, want: true},
{name: "force off wins over lazy state", force: lazyForceOff, remoteEnabled: true, state: mgmProto.LazyState_LazyStateLazy, want: false},
{name: "none, default, account off -> active", force: lazyForceNone, state: mgmProto.LazyState_LazyStateDefault, want: false},
{name: "none, default, account on -> lazy", force: lazyForceNone, remoteEnabled: true, state: mgmProto.LazyState_LazyStateDefault, want: true},
{name: "none, lazy state, account off -> lazy", force: lazyForceNone, state: mgmProto.LazyState_LazyStateLazy, want: true},
{name: "none, eager state, account on -> active", force: lazyForceNone, remoteEnabled: true, state: mgmProto.LazyState_LazyStateEager, want: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
e := &ConnMgr{force: tt.force, remoteLazyEnabled: tt.remoteEnabled}
if got := e.PeerLazyDefault(tt.state); got != tt.want {
t.Fatalf("PeerLazyDefault(%v) = %v, want %v", tt.state, got, tt.want)
}
})
}
}
func durPtr(d time.Duration) *time.Duration { return &d } func durPtr(d time.Duration) *time.Duration { return &d }
// TestToExcludedLazyPeers covers the per-peer lazy classification (proxy vs
// normal, across the force/account-flag matrix). Forwarder-target exclusion is
// covered by TestToExcludedLazyPeers_ForwardTarget.
func TestToExcludedLazyPeers(t *testing.T) {
const (
normalKey = "normal"
lazyKey = "lazy-state"
eagerKey = "eager-state"
)
peers := []*mgmProto.RemotePeerConfig{
{WgPubKey: normalKey, AllowedIps: []string{"100.64.0.1/32"}},
{WgPubKey: lazyKey, AllowedIps: []string{"100.64.0.2/32"}, LazyState: mgmProto.LazyState_LazyStateLazy},
{WgPubKey: eagerKey, AllowedIps: []string{"100.64.0.3/32"}, LazyState: mgmProto.LazyState_LazyStateEager},
}
tests := []struct {
name string
force lazyForce
remoteEnabled bool
want map[string]bool
}{
{
name: "account off: lazy-state peer lazy, normal + eager active",
force: lazyForceNone, remoteEnabled: false,
want: map[string]bool{normalKey: true, eagerKey: true},
},
{
name: "account on: only eager-state peer active",
force: lazyForceNone, remoteEnabled: true,
want: map[string]bool{eagerKey: true},
},
{
name: "force off: everything active",
force: lazyForceOff, remoteEnabled: true,
want: map[string]bool{normalKey: true, lazyKey: true, eagerKey: true},
},
{
name: "force on: nothing active",
force: lazyForceOn, remoteEnabled: false,
want: map[string]bool{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
e := &Engine{connMgr: &ConnMgr{force: tt.force, remoteLazyEnabled: tt.remoteEnabled}}
got := e.toExcludedLazyPeers(nil, peers)
if len(got) != len(tt.want) {
t.Fatalf("toExcludedLazyPeers() = %v, want %v", got, tt.want)
}
for k := range tt.want {
if !got[k] {
t.Fatalf("expected peer %s excluded, got %v", k, got)
}
}
})
}
}
+40 -26
View File
@@ -830,7 +830,7 @@ func (e *Engine) blockLanAccess() {
// modifyPeers updates peers that have been modified (e.g. IP address has been changed). // modifyPeers updates peers that have been modified (e.g. IP address has been changed).
// It closes the existing connection, removes it from the peerConns map, and creates a new one. // It closes the existing connection, removes it from the peerConns map, and creates a new one.
func (e *Engine) modifyPeers(peersUpdate []*mgmProto.RemotePeerConfig) error { func (e *Engine) modifyPeers(peersUpdate []*mgmProto.RemotePeerConfig, forwardingRules []firewallManager.ForwardRule) error {
// first, check if peers have been modified // first, check if peers have been modified
var modified []*mgmProto.RemotePeerConfig var modified []*mgmProto.RemotePeerConfig
@@ -869,7 +869,7 @@ func (e *Engine) modifyPeers(peersUpdate []*mgmProto.RemotePeerConfig) error {
} }
// third, add the peer connections again // third, add the peer connections again
for _, p := range modified { for _, p := range modified {
err := e.addNewPeer(p) err := e.addNewPeer(p, forwardingRules)
if err != nil { if err != nil {
return err return err
} }
@@ -1570,15 +1570,14 @@ func (e *Engine) updateNetworkMap(networkMap *mgmProto.NetworkMap) error {
e.updateOfflinePeers(networkMap.GetOfflinePeers()) e.updateOfflinePeers(networkMap.GetOfflinePeers())
done() done()
remotePeers, err := e.reconcilePeers(networkMap) remotePeers, err := e.reconcilePeers(networkMap, forwardingRules)
if err != nil { if err != nil {
return err return err
} }
// must set the exclude list after the peers are added. Without it the manager can not figure out the peers parameters from the store // must set the exclude list after the peers are added. Without it the manager can not figure out the peers parameters from the store
done = e.phase("lazy_exclude") done = e.phase("lazy_exclude")
excludedLazyPeers := e.toExcludedLazyPeers(forwardingRules, remotePeers) e.connMgr.SetExcludeList(e.ctx, e.toExcludedLazyPeers(forwardingRules, remotePeers))
e.connMgr.SetExcludeList(e.ctx, excludedLazyPeers)
done() done()
e.networkSerial = serial e.networkSerial = serial
@@ -1588,8 +1587,10 @@ func (e *Engine) updateNetworkMap(networkMap *mgmProto.NetworkMap) error {
// reconcilePeers applies the remote peer list from the network map (removing, // reconcilePeers applies the remote peer list from the network map (removing,
// modifying and adding peers, then updating SSH config) and returns the remote // modifying and adding peers, then updating SSH config) and returns the remote
// peers with our own peer filtered out, for use by later sync steps. // peers with our own peer filtered out, for use by later sync steps. The
func (e *Engine) reconcilePeers(networkMap *mgmProto.NetworkMap) ([]*mgmProto.RemotePeerConfig, error) { // forwarding rules are used to decide whether a newly added peer needs an
// always-active connection.
func (e *Engine) reconcilePeers(networkMap *mgmProto.NetworkMap, forwardingRules []firewallManager.ForwardRule) ([]*mgmProto.RemotePeerConfig, error) {
// Filter out own peer from the remote peers list // Filter out own peer from the remote peers list
localPubKey := e.config.WgPrivateKey.PublicKey().String() localPubKey := e.config.WgPrivateKey.PublicKey().String()
remotePeers := make([]*mgmProto.RemotePeerConfig, 0, len(networkMap.GetRemotePeers())) remotePeers := make([]*mgmProto.RemotePeerConfig, 0, len(networkMap.GetRemotePeers()))
@@ -1617,14 +1618,14 @@ func (e *Engine) reconcilePeers(networkMap *mgmProto.NetworkMap) ([]*mgmProto.Re
} }
done = e.phase("modified_peers") done = e.phase("modified_peers")
err = e.modifyPeers(remotePeers) err = e.modifyPeers(remotePeers, forwardingRules)
done() done()
if err != nil { if err != nil {
return nil, err return nil, err
} }
done = e.phase("added_peers") done = e.phase("added_peers")
err = e.addNewPeers(remotePeers) err = e.addNewPeers(remotePeers, forwardingRules)
done() done()
if err != nil { if err != nil {
return nil, err return nil, err
@@ -1820,9 +1821,9 @@ func addrToString(addr netip.Addr) string {
} }
// addNewPeers adds peers that were not know before but arrived from the Management service with the update // addNewPeers adds peers that were not know before but arrived from the Management service with the update
func (e *Engine) addNewPeers(peersUpdate []*mgmProto.RemotePeerConfig) error { func (e *Engine) addNewPeers(peersUpdate []*mgmProto.RemotePeerConfig, forwardingRules []firewallManager.ForwardRule) error {
for _, p := range peersUpdate { for _, p := range peersUpdate {
err := e.addNewPeer(p) err := e.addNewPeer(p, forwardingRules)
if err != nil { if err != nil {
return err return err
} }
@@ -1830,8 +1831,9 @@ func (e *Engine) addNewPeers(peersUpdate []*mgmProto.RemotePeerConfig) error {
return nil return nil
} }
// addNewPeer add peer if connection doesn't exist // addNewPeer add peer if connection doesn't exist. A peer that is not lazy by
func (e *Engine) addNewPeer(peerConfig *mgmProto.RemotePeerConfig) error { // policy (or is a forwarder) gets an always-active connection instead.
func (e *Engine) addNewPeer(peerConfig *mgmProto.RemotePeerConfig, forwardingRules []firewallManager.ForwardRule) error {
peerKey := peerConfig.GetWgPubKey() peerKey := peerConfig.GetWgPubKey()
peerIPs := make([]netip.Prefix, 0, len(peerConfig.GetAllowedIps())) peerIPs := make([]netip.Prefix, 0, len(peerConfig.GetAllowedIps()))
if _, ok := e.peerStore.PeerConn(peerKey); ok { if _, ok := e.peerStore.PeerConn(peerKey); ok {
@@ -1865,7 +1867,7 @@ func (e *Engine) addNewPeer(peerConfig *mgmProto.RemotePeerConfig) error {
log.Warnf("error adding peer %s to status recorder, got error: %v", peerKey, err) log.Warnf("error adding peer %s to status recorder, got error: %v", peerKey, err)
} }
if exists := e.connMgr.AddPeerConn(e.ctx, peerKey, conn); exists { if exists := e.connMgr.AddPeerConn(e.ctx, peerKey, conn, e.isPermanentPeer(peerConfig, forwardingRules)); exists {
conn.Close(false) conn.Close(false)
return fmt.Errorf("peer already exists: %s", peerKey) return fmt.Errorf("peer already exists: %s", peerKey)
} }
@@ -2692,25 +2694,37 @@ func (e *Engine) updateForwardRules(rules []*mgmProto.ForwardingRule) ([]firewal
return forwardingRules, nberrors.FormatErrorOrNil(merr) return forwardingRules, nberrors.FormatErrorOrNil(merr)
} }
// toExcludedLazyPeers returns the peers that must have an always-active
// connection, so the caller can reconcile the lazy manager's exclude list.
func (e *Engine) toExcludedLazyPeers(rules []firewallManager.ForwardRule, peers []*mgmProto.RemotePeerConfig) map[string]bool { func (e *Engine) toExcludedLazyPeers(rules []firewallManager.ForwardRule, peers []*mgmProto.RemotePeerConfig) map[string]bool {
excludedPeers := make(map[string]bool) excludedPeers := make(map[string]bool)
for _, p := range peers {
// Ingress forward targets: inbound forwarded traffic is initiated remotely and if e.isPermanentPeer(p, rules) {
// cannot wake a lazy connection, so the peer routing the target must stay excludedPeers[p.GetWgPubKey()] = true
// permanently connected. AllowedIPs are already parsed on the peer conn, so
// reuse those typed prefixes instead of re-parsing the network map strings.
for _, r := range rules {
for _, p := range peers {
if e.peerRoutesAddr(p, r.TranslatedAddress) {
log.Infof("exclude forwarder peer from lazy connection: %s", p.GetWgPubKey())
excludedPeers[p.GetWgPubKey()] = true
}
} }
} }
return excludedPeers return excludedPeers
} }
// isPermanentPeer reports whether a peer needs an always-active connection: it
// is not lazy by policy (the per-peer lazy hint or account flag, subject to the
// local override), or it is an ingress forward target. Inbound forwarded traffic
// is initiated remotely and cannot wake a lazy connection, so the peer routing
// the target must stay permanently connected.
func (e *Engine) isPermanentPeer(p *mgmProto.RemotePeerConfig, rules []firewallManager.ForwardRule) bool {
if !e.connMgr.PeerLazyDefault(p.GetLazyState()) {
return true
}
for _, r := range rules {
if e.peerRoutesAddr(p, r.TranslatedAddress) {
log.Infof("exclude forwarder peer from lazy connection: %s", p.GetWgPubKey())
return true
}
}
return false
}
// peerRoutesAddr reports whether the peer is a router for addr, matched against // peerRoutesAddr reports whether the peer is a router for addr, matched against
// the peer's already-parsed AllowedIPs from the store (the same typed value the // the peer's already-parsed AllowedIPs from the store (the same typed value the
// lazy manager consumes) rather than re-parsing the network map strings. // lazy manager consumes) rather than re-parsing the network map strings.
+4 -2
View File
@@ -49,7 +49,8 @@ func TestToExcludedLazyPeers_ForwardTarget(t *testing.T) {
store.AddPeerConn(targetPeerKey, newTestConn(t, targetPeerKey, "100.110.8.145/32")) store.AddPeerConn(targetPeerKey, newTestConn(t, targetPeerKey, "100.110.8.145/32"))
store.AddPeerConn(otherPeerKey, newTestConn(t, otherPeerKey, "100.110.9.10/32")) store.AddPeerConn(otherPeerKey, newTestConn(t, otherPeerKey, "100.110.9.10/32"))
e := &Engine{peerStore: store} // Lazy on for normal peers, so the only exclusion under test is the forward target.
e := &Engine{peerStore: store, connMgr: &ConnMgr{force: lazyForceOn}}
peers := []*mgmProto.RemotePeerConfig{ peers := []*mgmProto.RemotePeerConfig{
{WgPubKey: targetPeerKey, AllowedIps: []string{"100.110.8.145/32"}}, {WgPubKey: targetPeerKey, AllowedIps: []string{"100.110.8.145/32"}},
@@ -67,7 +68,8 @@ func TestToExcludedLazyPeers_ForwardTarget(t *testing.T) {
} }
func TestToExcludedLazyPeers_NoRules(t *testing.T) { func TestToExcludedLazyPeers_NoRules(t *testing.T) {
e := &Engine{peerStore: peerstore.NewConnStore()} // Lazy on for normal peers and no forward rules, so nothing is excluded.
e := &Engine{peerStore: peerstore.NewConnStore(), connMgr: &ConnMgr{force: lazyForceOn}}
peers := []*mgmProto.RemotePeerConfig{ peers := []*mgmProto.RemotePeerConfig{
{WgPubKey: "peer-a", AllowedIps: []string{"100.110.8.145/32"}}, {WgPubKey: "peer-a", AllowedIps: []string{"100.110.8.145/32"}},
@@ -701,6 +701,7 @@ func toPeerCompact(p *types.ComponentPeer) *proto.PeerCompact {
SupportsIpv6: p.SupportsIPv6, SupportsIpv6: p.SupportsIPv6,
SupportsSourcePrefixes: p.SupportsSourcePrefixes, SupportsSourcePrefixes: p.SupportsSourcePrefixes,
ServerSshAllowed: p.ServerSSHAllowed, ServerSshAllowed: p.ServerSSHAllowed,
ProxyEmbedded: p.ProxyEmbedded,
} }
if !p.LastLogin.IsZero() { if !p.LastLogin.IsZero() {
pc.LastLoginUnixNano = p.LastLogin.UnixNano() pc.LastLoginUnixNano = p.LastLogin.UnixNano()
@@ -684,8 +684,8 @@ func TestEncodeNetworkMapEnvelope_GroupIDToUserIDs(t *testing.T) {
} }
func TestToProxyPatch_EmptyInputReturnsNil(t *testing.T) { func TestToProxyPatch_EmptyInputReturnsNil(t *testing.T) {
assert.Nil(t, toProxyPatch(nil, "netbird.cloud", false, false)) assert.Nil(t, toProxyPatch(nil, "netbird.cloud", false, false, false))
assert.Nil(t, toProxyPatch(&types.NetworkMap{}, "netbird.cloud", false, false), assert.Nil(t, toProxyPatch(&types.NetworkMap{}, "netbird.cloud", false, false, false),
"empty NetworkMap (no peers, rules, routes etc) → nil patch so proto3 omits the field") "empty NetworkMap (no peers, rules, routes etc) → nil patch so proto3 omits the field")
} }
@@ -700,7 +700,7 @@ func TestToProxyPatch_PopulatesAllFields(t *testing.T) {
}}, }},
} }
patch := toProxyPatch(nm, "netbird.cloud", false, false) patch := toProxyPatch(nm, "netbird.cloud", false, false, false)
require.NotNil(t, patch) require.NotNil(t, patch)
assert.Len(t, patch.Peers, 1) assert.Len(t, patch.Peers, 1)
@@ -66,7 +66,7 @@ func ToComponentSyncResponse(
DNSDomain: dnsName, DNSDomain: dnsName,
DNSForwarderPort: dnsFwdPort, DNSForwarderPort: dnsFwdPort,
UserIDClaim: userIDClaim, UserIDClaim: userIDClaim,
ProxyPatch: toProxyPatch(proxyPatch, dnsName, includeIPv6, useSourcePrefixes), ProxyPatch: toProxyPatch(proxyPatch, dnsName, includeIPv6, useSourcePrefixes, peer.ProxyMeta.Embedded),
}) })
resp := &proto.SyncResponse{ resp := &proto.SyncResponse{
@@ -104,7 +104,7 @@ func ToComponentSyncResponse(
// derive them from. Components purity isn't violated: proxy data isn't // derive them from. Components purity isn't violated: proxy data isn't
// policy-graph-derived, it's externally injected post-Calculate, so the // policy-graph-derived, it's externally injected post-Calculate, so the
// client merges it on top of its locally-computed NetworkMap. // client merges it on top of its locally-computed NetworkMap.
func toProxyPatch(nm *types.NetworkMap, dnsName string, includeIPv6, useSourcePrefixes bool) *proto.ProxyPatch { func toProxyPatch(nm *types.NetworkMap, dnsName string, includeIPv6, useSourcePrefixes, localIsProxy bool) *proto.ProxyPatch {
if nm == nil { if nm == nil {
return nil return nil
} }
@@ -114,8 +114,8 @@ func toProxyPatch(nm *types.NetworkMap, dnsName string, includeIPv6, useSourcePr
} }
patch := &proto.ProxyPatch{ patch := &proto.ProxyPatch{
Peers: networkmap.AppendRemotePeerConfig(nil, nm.Peers, dnsName, includeIPv6), Peers: networkmap.AppendRemotePeerConfig(nil, nm.Peers, dnsName, includeIPv6, localIsProxy),
OfflinePeers: networkmap.AppendRemotePeerConfig(nil, nm.OfflinePeers, dnsName, includeIPv6), OfflinePeers: networkmap.AppendRemotePeerConfig(nil, nm.OfflinePeers, dnsName, includeIPv6, localIsProxy),
FirewallRules: networkmap.ToProtocolFirewallRules(nm.FirewallRules, includeIPv6, useSourcePrefixes), FirewallRules: networkmap.ToProtocolFirewallRules(nm.FirewallRules, includeIPv6, useSourcePrefixes),
Routes: networkmap.ToProtocolRoutes(nm.Routes), Routes: networkmap.ToProtocolRoutes(nm.Routes),
RouteFirewallRules: networkmap.ToProtocolRoutesFirewallRules(nm.RoutesFirewallRules), RouteFirewallRules: networkmap.ToProtocolRoutesFirewallRules(nm.RoutesFirewallRules),
@@ -160,6 +160,7 @@ func ToSyncResponse(ctx context.Context, config *nbconfig.Config, httpConfig *nb
// filtered at the source (network map builder). // filtered at the source (network map builder).
includeIPv6 := peer.SupportsIPv6() && peer.IPv6.IsValid() includeIPv6 := peer.SupportsIPv6() && peer.IPv6.IsValid()
useSourcePrefixes := peer.SupportsSourcePrefixes() useSourcePrefixes := peer.SupportsSourcePrefixes()
localIsProxy := peer.ProxyMeta.Embedded
response := &proto.SyncResponse{ response := &proto.SyncResponse{
PeerConfig: toPeerConfig(peer, networkMap.Network, dnsName, settings, httpConfig, deviceFlowConfig, networkMap.EnableSSH, networkMap.ForceRoutingPeerDNSResolution), PeerConfig: toPeerConfig(peer, networkMap.Network, dnsName, settings, httpConfig, deviceFlowConfig, networkMap.EnableSSH, networkMap.ForceRoutingPeerDNSResolution),
@@ -179,7 +180,7 @@ func ToSyncResponse(ctx context.Context, config *nbconfig.Config, httpConfig *nb
response.NetworkMap.PeerConfig = response.PeerConfig response.NetworkMap.PeerConfig = response.PeerConfig
remotePeers := make([]*proto.RemotePeerConfig, 0, len(networkMap.Peers)+len(networkMap.OfflinePeers)) remotePeers := make([]*proto.RemotePeerConfig, 0, len(networkMap.Peers)+len(networkMap.OfflinePeers))
remotePeers = networkmap.AppendRemotePeerConfig(remotePeers, networkMap.Peers, dnsName, includeIPv6) remotePeers = networkmap.AppendRemotePeerConfig(remotePeers, networkMap.Peers, dnsName, includeIPv6, localIsProxy)
if !shouldSkipSendingDeprecatedRemotePeers(peer.Meta.WtVersion) { if !shouldSkipSendingDeprecatedRemotePeers(peer.Meta.WtVersion) {
response.RemotePeers = remotePeers response.RemotePeers = remotePeers
@@ -189,7 +190,7 @@ func ToSyncResponse(ctx context.Context, config *nbconfig.Config, httpConfig *nb
response.RemotePeersIsEmpty = len(remotePeers) == 0 response.RemotePeersIsEmpty = len(remotePeers) == 0
response.NetworkMap.RemotePeersIsEmpty = response.RemotePeersIsEmpty response.NetworkMap.RemotePeersIsEmpty = response.RemotePeersIsEmpty
response.NetworkMap.OfflinePeers = networkmap.AppendRemotePeerConfig(nil, networkMap.OfflinePeers, dnsName, includeIPv6) response.NetworkMap.OfflinePeers = networkmap.AppendRemotePeerConfig(nil, networkMap.OfflinePeers, dnsName, includeIPv6, localIsProxy)
firewallRules := networkmap.ToProtocolFirewallRules(networkMap.FirewallRules, includeIPv6, useSourcePrefixes) firewallRules := networkmap.ToProtocolFirewallRules(networkMap.FirewallRules, includeIPv6, useSourcePrefixes)
response.NetworkMap.FirewallRules = firewallRules response.NetworkMap.FirewallRules = firewallRules
+1
View File
@@ -228,6 +228,7 @@ func (p *Peer) ToComponent() *sharedTypes.ComponentPeer {
SupportsIPv6: p.SupportsIPv6(), SupportsIPv6: p.SupportsIPv6(),
LoginExpirationEnabled: p.LoginExpirationEnabled, LoginExpirationEnabled: p.LoginExpirationEnabled,
AddedWithSSOLogin: p.AddedWithSSOLogin(), AddedWithSSOLogin: p.AddedWithSSOLogin(),
ProxyEmbedded: p.ProxyMeta.Embedded,
} }
if p.LastLogin != nil { if p.LastLogin != nil {
cp.LastLogin = *p.LastLogin cp.LastLogin = *p.LastLogin
+1
View File
@@ -273,6 +273,7 @@ func decodePeerCompact(pc *proto.PeerCompact, peerID string) *types.ComponentPee
SupportsIPv6: pc.SupportsIpv6, SupportsIPv6: pc.SupportsIpv6,
ServerSSHAllowed: pc.ServerSshAllowed, ServerSSHAllowed: pc.ServerSshAllowed,
AddedWithSSOLogin: pc.AddedWithSsoLogin, AddedWithSSOLogin: pc.AddedWithSsoLogin,
ProxyEmbedded: pc.ProxyEmbedded,
} }
if pc.LastLoginUnixNano != 0 { if pc.LastLoginUnixNano != 0 {
peer.LastLogin = time.Unix(0, pc.LastLoginUnixNano) peer.LastLogin = time.Unix(0, pc.LastLoginUnixNano)
+16 -2
View File
@@ -272,8 +272,9 @@ func ToProtocolDNSConfig(update nbdns.Config, cache DNSConfigCache, forwardPort
} }
// AppendRemotePeerConfig appends typed peers as proto.RemotePeerConfig // AppendRemotePeerConfig appends typed peers as proto.RemotePeerConfig
// entries to dst and returns the result. // entries to dst and returns the result. localIsProxy reports whether the peer
func AppendRemotePeerConfig(dst []*proto.RemotePeerConfig, peers []*types.ComponentPeer, dnsName string, includeIPv6 bool) []*proto.RemotePeerConfig { // receiving this config is itself an embedded proxy.
func AppendRemotePeerConfig(dst []*proto.RemotePeerConfig, peers []*types.ComponentPeer, dnsName string, includeIPv6 bool, localIsProxy bool) []*proto.RemotePeerConfig {
for _, rPeer := range peers { for _, rPeer := range peers {
allowedIPs := []string{rPeer.IP.String() + "/32"} allowedIPs := []string{rPeer.IP.String() + "/32"}
if includeIPv6 && rPeer.IPv6.IsValid() { if includeIPv6 && rPeer.IPv6.IsValid() {
@@ -285,11 +286,24 @@ func AppendRemotePeerConfig(dst []*proto.RemotePeerConfig, peers []*types.Compon
SshConfig: &proto.SSHConfig{SshPubKey: []byte(rPeer.SSHKey)}, SshConfig: &proto.SSHConfig{SshPubKey: []byte(rPeer.SSHKey)},
Fqdn: rPeer.FQDN(dnsName), Fqdn: rPeer.FQDN(dnsName),
AgentVersion: rPeer.AgentVersion, AgentVersion: rPeer.AgentVersion,
LazyState: lazyStateFor(localIsProxy, rPeer),
}) })
} }
return dst return dst
} }
// lazyStateFor returns the per-peer lazy override for a remote peer. Connections
// involving an ephemeral proxy peer on either endpoint default to lazy so shared
// proxy infrastructure is not kept permanently connected to every peer. All
// other peers follow the account-wide flag. A future admin-facing per-peer
// setting can return LazyStateEager here to force a peer always-active.
func lazyStateFor(localIsProxy bool, rPeer *types.ComponentPeer) proto.LazyState {
if localIsProxy || rPeer.ProxyEmbedded {
return proto.LazyState_LazyStateLazy
}
return proto.LazyState_LazyStateDefault
}
// BuildAuthorizedUsersProto deduplicates user-IDs into a hashed list and // BuildAuthorizedUsersProto deduplicates user-IDs into a hashed list and
// builds per-machine-user index maps. Returns (hashedUsers, machineUsers). // builds per-machine-user index maps. Returns (hashedUsers, machineUsers).
// Errors from individual hash failures are logged via the provided context; // Errors from individual hash failures are logged via the provided context;
+2 -2
View File
@@ -74,11 +74,11 @@ func EnvelopeToNetworkMap(ctx context.Context, env *proto.NetworkMapEnvelope, lo
protoNM.Routes = ToProtocolRoutes(typedNM.Routes) protoNM.Routes = ToProtocolRoutes(typedNM.Routes)
protoNM.DNSConfig = ToProtocolDNSConfig(typedNM.DNSConfig, nil, dnsFwdPort) protoNM.DNSConfig = ToProtocolDNSConfig(typedNM.DNSConfig, nil, dnsFwdPort)
remotePeers := AppendRemotePeerConfig(nil, typedNM.Peers, dnsName, includeIPv6) remotePeers := AppendRemotePeerConfig(nil, typedNM.Peers, dnsName, includeIPv6, localPeer.ProxyEmbedded)
protoNM.RemotePeers = remotePeers protoNM.RemotePeers = remotePeers
protoNM.RemotePeersIsEmpty = len(remotePeers) == 0 protoNM.RemotePeersIsEmpty = len(remotePeers) == 0
protoNM.OfflinePeers = AppendRemotePeerConfig(nil, typedNM.OfflinePeers, dnsName, includeIPv6) protoNM.OfflinePeers = AppendRemotePeerConfig(nil, typedNM.OfflinePeers, dnsName, includeIPv6, localPeer.ProxyEmbedded)
firewallRules := ToProtocolFirewallRules(typedNM.FirewallRules, includeIPv6, useSourcePrefixes) firewallRules := ToProtocolFirewallRules(typedNM.FirewallRules, includeIPv6, useSourcePrefixes)
protoNM.FirewallRules = firewallRules protoNM.FirewallRules = firewallRules
File diff suppressed because it is too large Load Diff
+21
View File
@@ -497,6 +497,22 @@ message RemotePeerConfig {
string fqdn = 4; string fqdn = 4;
string agentVersion = 5; string agentVersion = 5;
// lazyState is the management per-peer override for lazy (on-demand)
// connections to this remote peer. LazyStateDefault follows the account-wide
// flag; LazyStateLazy forces lazy; LazyStateEager forces an always-active
// connection. A local NB_LAZY_CONN/MDM override still wins over this.
LazyState lazyState = 6;
}
// LazyState is the management per-peer override for lazy connections.
enum LazyState {
// Follow the account-wide lazy connection flag.
LazyStateDefault = 0;
// Force a lazy (on-demand) connection regardless of the account flag.
LazyStateLazy = 1;
// Force an always-active connection regardless of the account flag.
LazyStateEager = 2;
} }
// SSHConfig represents SSH configurations of a peer. // SSHConfig represents SSH configurations of a peer.
@@ -1012,6 +1028,11 @@ message PeerCompact {
// (port 22022) is only added when this flag is set and the peer agent // (port 22022) is only added when this flag is set and the peer agent
// version supports it. // version supports it.
bool server_ssh_allowed = 13; bool server_ssh_allowed = 13;
// Mirror of types.Peer.ProxyMeta.Embedded. Connections involving an
// ephemeral proxy peer on either endpoint default to lazy, so this bit
// feeds the per-peer lazyState emitted in RemotePeerConfig.
bool proxy_embedded = 14;
} }
// PolicyCompact is the compact form of a policy rule. Group references use // PolicyCompact is the compact form of a policy rule. Group references use
@@ -25,6 +25,9 @@ type ComponentPeer struct {
LoginExpirationEnabled bool LoginExpirationEnabled bool
AddedWithSSOLogin bool AddedWithSSOLogin bool
LastLogin time.Time LastLogin time.Time
// ProxyEmbedded marks an ephemeral embedded proxy peer. Connections
// involving such a peer on either endpoint default to lazy.
ProxyEmbedded bool
} }
// FQDN returns the peer's FQDN combined of the peer's DNS label and the system's DNS domain. // FQDN returns the peer's FQDN combined of the peer's DNS label and the system's DNS domain.