Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
147 commits
Select commit Hold shift + click to select a range
c4844cc
proto: add ConnectionMode enum and p2p/relay timeout fields to PeerCo…
MichaelUray May 1, 2026
e0ed831
client: add connectionmode package with Mode type and proto bridge
MichaelUray May 1, 2026
c71c951
client/peer: ResolveModeFromEnv with NB_CONNECTION_MODE and deprecati…
MichaelUray May 1, 2026
7d90a5b
client: add --connection-mode, --relay-timeout, --p2p-timeout CLI flags
MichaelUray May 1, 2026
cc10c9f
client/conn_mgr: replace asymmetric Lazy/ForceRelay precedence with Mode
MichaelUray May 1, 2026
dfd48e9
client/peer: connection mode drives skip-ICE branch in Open()
MichaelUray May 1, 2026
82877f0
client/engine: forward resolved Mode to per-peer ConnConfig
MichaelUray May 1, 2026
cd0abe8
mgmt/types: add ConnectionMode + p2p/relay timeout to Settings
MichaelUray May 1, 2026
0022145
openapi: add connection_mode + p2p/relay timeout fields to AccountSet…
MichaelUray May 1, 2026
b22128e
mgmt/handlers/accounts: accept connection_mode + timeout settings on PUT
MichaelUray May 1, 2026
3d8cc98
mgmt/activity: add three new account-scoped event codes
MichaelUray May 1, 2026
b53322d
mgmt/account: emit audit events for connection_mode + timeout changes
MichaelUray May 1, 2026
f63e2a7
move connectionmode package + extend toPeerConfig to fill new wire fi…
MichaelUray May 1, 2026
77852a9
mgmt/grpc: tests for toPeerConfig connection-mode resolution
MichaelUray May 1, 2026
0304dc2
client/peer/handshaker: add RemoveICEListener + lock the listener-read
MichaelUray May 1, 2026
ec2b46e
client/lazyconn/inactivity: two-timer per-peer with separate channels
MichaelUray May 1, 2026
3528248
client/lazyconn/manager: Config gains ICEInactivityThreshold + Relay
MichaelUray May 1, 2026
5c18a23
feat(peer): add Conn.AttachICE / DetachICE for p2p-dynamic mode
MichaelUray May 1, 2026
ad789e3
client/peer/conn: Open() defers ICE-listener registration in p2p-dynamic
MichaelUray May 1, 2026
a9710d9
client/conn_mgr: per-mode DeactivatePeer + DetachICEForPeer
MichaelUray May 1, 2026
d662d9b
client/conn_mgr: wire p2p-dynamic two-timer lifecycle
MichaelUray May 1, 2026
76e2d0b
client/conn_mgr: resolve P2pTimeoutSeconds from server-pushed PeerConfig
MichaelUray May 1, 2026
efaa2b1
client/conn_mgr: ActivatePeer attaches ICE listener in p2p-dynamic
MichaelUray May 1, 2026
838702d
proto: add P2pRetryMaxSeconds field to PeerConfig (Phase 3 #5989)
MichaelUray May 1, 2026
90c3d9f
mgmt/types: add P2pRetryMaxSeconds to Settings
MichaelUray May 1, 2026
74265f3
openapi: add p2p_retry_max_seconds to AccountSettings
MichaelUray May 1, 2026
9f60f1a
mgmt/handlers/accounts: accept p2p_retry_max_seconds setting on PUT
MichaelUray May 1, 2026
37276ea
mgmt/account+activity: emit audit event for p2p_retry_max changes
MichaelUray May 1, 2026
f9db3b0
mgmt/grpc: include P2pRetryMaxSeconds in toPeerConfig with sentinel m…
MichaelUray May 1, 2026
f63852d
client/peer: add iceBackoffState with truncated exponential schedule
MichaelUray May 1, 2026
7928f05
client/peer/conn: hook iceBackoffState into Conn lifecycle
MichaelUray May 1, 2026
4bb38b6
client/peer/conn: AttachICE returns nil-no-op during ice backoff
MichaelUray May 1, 2026
a49534f
client/peer: hook pion ICE state changes into iceBackoff
MichaelUray May 1, 2026
68dd578
client/conn_mgr: resolve P2pRetryMaxSeconds from server PeerConfig
MichaelUray May 1, 2026
7349b95
client/conn_mgr: propagate P2pRetryMaxSeconds changes to active Conns
MichaelUray May 1, 2026
75713b9
client: --p2p-retry-max CLI flag + EngineConfig + profile field
MichaelUray May 1, 2026
32c4efb
client/engine: forward P2pRetryMaxSeconds to ConnConfig
MichaelUray May 1, 2026
451872e
client/cmd/status: show ICE-backoff state in --detail output
MichaelUray May 1, 2026
19fb079
client/status: suppress ICE-backoff line when nextRetry has passed
MichaelUray May 1, 2026
b22143d
client/proto+shared/management: regenerate after ConnectionMode + P2p…
MichaelUray May 5, 2026
bfdc73e
client/server: cover Phase 3.7i ConnectionMode fields in SetConfigReq…
MichaelUray May 6, 2026
7c1a7a1
client+shared: pin proto version headers to upstream values
MichaelUray May 6, 2026
701a20f
client/peer: fix codespell typo in env_test.go (unparseable -> unpars…
MichaelUray May 6, 2026
58eb4f8
client+shared: golangci-lint cleanups + proxy_service.pb.go protoc ve…
MichaelUray May 6, 2026
6dd1e44
client/internal/debug: render Phase 1+2+3 connection-mode fields in d…
MichaelUray May 6, 2026
7c80838
client/peer: reset ICE backoff + recreate workerICE on network change
MichaelUray May 2, 2026
b9a967f
client/peer/conn: send offer after workerICE recreate (Phase 3.5 foll…
MichaelUray May 2, 2026
8760fa1
client/peer/worker_ice: buffer remote candidates that race ahead of a…
MichaelUray May 2, 2026
6f86055
Revert "client/peer/worker_ice: buffer remote candidates that race ah…
MichaelUray May 2, 2026
939d946
client/peer/conn: refactor onNetworkChange to use the in-place agent …
MichaelUray May 2, 2026
15c6d90
client/peer/conn: drop SendOffer from onNetworkChange to fix offer-storm
MichaelUray May 2, 2026
78d2fdc
client/peer/worker_ice: skip new offers while ICE agent is connecting…
MichaelUray May 2, 2026
90dba34
client/internal: re-attach ICE on every signal trigger (Phase 3.7d)
MichaelUray May 2, 2026
bcb30b9
client/peer/conn: re-attach ICE listener inside onNetworkChange (Phas…
MichaelUray May 2, 2026
9e444b5
client/peer/ice_backoff: short delay for first failure post-network-c…
MichaelUray May 2, 2026
67e7f36
client/peer: skip workerICE.Close on network change when ICE still Co…
MichaelUray May 2, 2026
ddd1f87
client/peer/ice_backoff: widen post-network-change grace window (Phas…
MichaelUray May 2, 2026
34300d5
client/cmd/service: persist connection-mode + timeouts on install/rec…
MichaelUray May 2, 2026
b12be21
client/ui: Connection Mode + timeouts in Network tab (Phase 3.7h GUI)
MichaelUray May 2, 2026
f4ff7c7
client: surface server-pushed connection-mode/timeouts via daemon-RPC
MichaelUray May 3, 2026
0b85cf5
client/ui: Follow-Server (currently: ...) display + Lazy menu removal
MichaelUray May 3, 2026
672a9bc
client/android: gomobile getters for ConnectionMode + ServerPushed va…
MichaelUray May 3, 2026
abeecc6
client+shared: regenerate proto after Phase 3.7h GUI proto changes
MichaelUray May 5, 2026
3730df0
client+shared: regenerate proto on rebased PR-B + pin protoc version …
MichaelUray May 6, 2026
ef13e66
client: surface server-pushed connection-mode/timeouts via daemon-RPC
MichaelUray May 3, 2026
36c2f14
proto/management: RemotePeerConfig + PeerSystemMeta extensions for pe…
MichaelUray May 3, 2026
7c1ae62
mgmt/peer: store effective_connection_mode in PeerSystemMeta
MichaelUray May 3, 2026
44d1939
mgmt/conversion: appendRemotePeerConfig fills effective+configured+gr…
MichaelUray May 3, 2026
601b25c
mgmt/client+engine: report effective_connection_mode + SyncMeta debounce
MichaelUray May 3, 2026
8b76795
proto/management: SyncPeerConnections unary RPC + PeerConnectionMap msgs
MichaelUray May 3, 2026
3468538
mgmt/peer_connections: Store interface + MemoryStore impl
MichaelUray May 3, 2026
4244e49
mgmt/peer_connections: SnapshotRouter for on-demand refresh dispatch
MichaelUray May 3, 2026
988c219
mgmt: shared peer_connections bootstrap + SyncPeerConnections handler
MichaelUray May 3, 2026
3f4de29
mgmt/client: SyncPeerConnections unary client method + interface + mock
MichaelUray May 3, 2026
82ca06a
mgmt/client/test: update NewServer call for Phase-3.7i bootstrap params
MichaelUray May 3, 2026
5f47408
mgmt/grpc: route SnapshotRequest via Sync server-stream, bypass debou…
MichaelUray May 3, 2026
ed6c44b
client/internal: conn_state_pusher with adaptive heartbeat + snapshot
MichaelUray May 3, 2026
39fe648
client/internal: wire conn_state_pusher into Engine + Sync receive
MichaelUray May 3, 2026
48b2ef5
proto/daemon: FullStatus aggregate counters + PeerState enrichment
MichaelUray May 3, 2026
e02ea87
client/peer/status: counters in FullStatus + UpdatePeerRemoteMeta
MichaelUray May 3, 2026
e96f4ec
client/engine: plumb RemotePeerConfig into peer.UpdatePeerRemoteMeta
MichaelUray May 3, 2026
35eaa67
mgmt/http: GET + POST /api/peers/{id}/connections handlers with RBAC
MichaelUray May 3, 2026
aa77c5a
mgmt/router: register /api/peers/{id}/connections + /refresh routes
MichaelUray May 3, 2026
87f5491
client/ui: new "Peers" tab in Networks window
MichaelUray May 3, 2026
f486956
client/android: gomobile getters for 6 peer-status aggregate counters
MichaelUray May 3, 2026
88d50e6
client/android: enrich PeerInfo with peer.State fields for Android UI
MichaelUray May 3, 2026
5a408f3
client/ui: Peers tab first + tray rename + Stack-wrapped Accordion fill
MichaelUray May 3, 2026
a00edda
peer-status: live_online from peer.Status.Connected for accurate counter
MichaelUray May 3, 2026
0236cc5
client/ui: rename to "Peers and Networks" + force Peers tab to fill
MichaelUray May 3, 2026
accade5
client/ui: per-peer expandable rows + outer-footer Show-Full + tab-aw…
MichaelUray May 3, 2026
045fd16
client/ui: peer rows use dynamic add/remove + scroll has 600px floor
MichaelUray May 3, 2026
d05e3dd
client/ui: persist peer-row expand state across Refresh + size to con…
MichaelUray May 3, 2026
d9cc9f4
client/conn_mgr: lazy/dynamic mode change resets peers to Idle + tole…
MichaelUray May 3, 2026
301d8d1
mgmt/peer_connections: nonce/router/refresh hardening per code review
MichaelUray May 3, 2026
db1af91
client/conn_state_pusher: dirty-on-fail + initial-snapshot gating
MichaelUray May 3, 2026
6197a3c
peer/status: endpoint+relay-server changes trigger immediate state push
MichaelUray May 3, 2026
a116998
proto+mgmt: server_liveness_known marker for authoritative LiveOnline
MichaelUray May 3, 2026
ceb9a4c
client/conn+ui: ICE handover packet-loss fix + nil-Latency guard
MichaelUray May 3, 2026
a81a01b
mgmt/store: getAccountPgx selects Phase-3 connection-mode columns
MichaelUray May 3, 2026
aca8500
client: Android refresh wg-stats on PeersList + bump default relay-ti…
MichaelUray May 3, 2026
0fc4ccc
client/lazyconn: IsSupported also accepts "0.0.0-dev-…" semver-padded…
MichaelUray May 3, 2026
7e737db
client+shared: regenerate proto after Phase 3.7i visibility extensions
MichaelUray May 5, 2026
20fa31b
client/peer/status: add notifyPeerListChanged + notifyPeerStateChange…
MichaelUray May 5, 2026
550c830
client+shared: regenerate proto on rebased PR-C + pin protoc version …
MichaelUray May 6, 2026
17705d8
client/cmd: testutil_test.go nbgrpc.NewServer call to 13 args (peer_c…
MichaelUray May 6, 2026
eb60bf3
mgmt/grpc: nil-default peer_connections store+router + 13-arg test fi…
MichaelUray May 6, 2026
395f771
PR-C linter cleanups for golangci-lint
MichaelUray May 6, 2026
c8fcd96
client/conn_state_pusher: nil-receiver safe On* entry points
MichaelUray May 6, 2026
1f01f70
client: surface server-pushed connection-mode/timeouts via daemon-RPC
MichaelUray May 3, 2026
79c98c7
proto/management: RemotePeerConfig + PeerSystemMeta extensions for pe…
MichaelUray May 3, 2026
4e7d542
proto/daemon: FullStatus aggregate counters + PeerState enrichment
MichaelUray May 3, 2026
9deb8c9
client/engine: nil-guard connStatePusher closures during shutdown
MichaelUray May 3, 2026
c50d6c1
client: reconnect-guard p2p-dynamic-aware + proactive close on remote…
MichaelUray May 3, 2026
14240a5
client: hybrid "Relayed (negotiating P2P)" UI label during wakeup window
MichaelUray May 3, 2026
9dee8f6
client/ui (Win): colored status swatch on each peer row
MichaelUray May 3, 2026
7fb43ba
codex-review fixes: settings push, RelayServer materiality, uint32 va…
MichaelUray May 3, 2026
0a95367
codex review round 2: debounce safety, pre-init drain, dashboard cach…
MichaelUray May 3, 2026
9351794
hardening: explicit cancel hooks + handover-order regression tests
MichaelUray May 3, 2026
d0b9607
hardening Item 1+2: reconnect-guard inactivity-skip + UI ICE-backoff …
MichaelUray May 3, 2026
e8ebe7b
codex review: 4 findings — server build, store epoch, meta notify, IC…
MichaelUray May 3, 2026
046b9af
codex follow-up: session_id epoch field for unary-RPC stale-delta safety
MichaelUray May 3, 2026
adce677
fix: gate guard skip-offer on everConnected (regression from Item 1)
MichaelUray May 3, 2026
5be90b3
fix: WG-handshake-timeout recovery — push peer back to lazy-idle
MichaelUray May 3, 2026
5bd3862
proto+client+mgmt: add SupportedFeatures capability advertisement
MichaelUray May 4, 2026
0a102ad
mgmt/types+store: LegacyLazyFallback{Enabled,TimeoutSeconds}
MichaelUray May 4, 2026
2ed3efd
mgmt/conversion: legacy-client p2p-dynamic -> p2p-lazy fallback
MichaelUray May 4, 2026
8a37151
client/internal: latch conn_state_pusher disabled on Unimplemented
MichaelUray May 4, 2026
025bd4b
mgmt/http+activity: expose LegacyLazyFallback settings via API
MichaelUray May 4, 2026
342296e
mgmt: legacy-fallback defaults consistent across all construction paths
MichaelUray May 4, 2026
1a256f5
mgmt/peer: restore policy-aware peer visibility for user role
MichaelUray May 4, 2026
482b1be
client/ui: silent auto-refresh on Networks window when daemon IPC drops
MichaelUray May 4, 2026
1915bdf
client/ui: make peer-detail + network-range text selectable + copyable
MichaelUray May 4, 2026
2c43374
client/stdnet: case-insensitive ICE interface filter (Windows P2P fix)
MichaelUray May 4, 2026
56ebcf4
mgmt/store: pgx getPeers must SELECT meta_supported_features + meta_e…
MichaelUray May 4, 2026
041975a
fix: keep WG peer entry across lazy-suspend so routed-subnet AllowedI…
MichaelUray May 4, 2026
9023cac
client/lazyconn: AttachICE on activity wake-up (p2p-dynamic re-attach…
MichaelUray May 4, 2026
200a7f2
client/lazyconn+peer: reset iceBackoff on activity-trigger wake
MichaelUray May 5, 2026
12ddd32
client/iface+peer+engine: relay-state ICE re-attach fast-path
MichaelUray May 5, 2026
3cc937c
client/peer: rate-limited backoff override on relay-state activity
MichaelUray May 5, 2026
e07aaba
client/peer: markSuccess must stamp lastResetAt + tests for override
MichaelUray May 5, 2026
f71601c
client/peer: clarify backoff intent + classify failure types + invari…
MichaelUray May 5, 2026
5a2302c
client/peer/guard: activity-driven reset of ICE retry budget
MichaelUray May 5, 2026
613713d
post-rebase: regenerate proto files + restore notify helpers + mock fix
MichaelUray May 5, 2026
7f1c876
post-rebase: Codex review fixes (PR-blockers + status recorder)
MichaelUray May 5, 2026
69c0835
client+shared: regenerate proto on rebased PR-D + pin protoc versions…
MichaelUray May 6, 2026
4364c53
client+mgmt: fix errcheck + math.MaxUint32 386-overflow on PR-D
MichaelUray May 6, 2026
6aac4ee
PR-D linter cleanups for golangci-lint
MichaelUray May 6, 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ infrastructure_files/**/docker-compose.yml.bkp.**
infrastructure_files/**/openid-configuration.json.bkp.**
infrastructure_files/**/turnserver.conf.bkp.**
management/management
management/netbird-mgmt
client/client
client/client.exe
*.syso
Expand Down
144 changes: 140 additions & 4 deletions client/android/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"os"
"slices"
"strings"
"sync"
"time"

Expand Down Expand Up @@ -295,17 +296,56 @@ func (c *Client) SetInfoLogLevel() {

// PeersList return with the list of the PeerInfos
func (c *Client) PeersList() *PeerInfoArray {
// Refresh WireGuard counters (BytesRx/Tx + LastWireguardHandshake)
// from the kernel/uapi interface before snapshotting. Without this
// the Android UI sees the stale values that were last written when
// the peer was opened/closed (typically 0), because the desktop
// CLI's Status RPC is what normally drives RefreshWireGuardStats.
// Phase 3.7i.
if err := c.recorder.RefreshWireGuardStats(); err != nil {
log.Debugf("PeersList: refresh wg stats: %v", err)
}

fullStatus := c.recorder.GetFullStatus()

peerInfos := make([]PeerInfo, len(fullStatus.Peers))
for n, p := range fullStatus.Peers {
pi := PeerInfo{
p.IP,
p.FQDN,
int(p.ConnStatus),
PeerRoutes{routes: maps.Keys(p.GetRoutes())},
IP: p.IP,
FQDN: p.FQDN,
ConnStatus: int(p.ConnStatus),
Routes: PeerRoutes{routes: maps.Keys(p.GetRoutes())},
}

// Phase 3.7i (#5989): enrichment fields.
pi.Relayed = p.Relayed
pi.ServerOnline = p.ServerOnline
pi.LocalIceCandidateEndpoint = p.LocalIceCandidateEndpoint
pi.RemoteIceCandidateEndpoint = p.RemoteIceCandidateEndpoint
pi.RelayServerAddress = p.RelayServerAddress
if !p.LastWireguardHandshake.IsZero() {
pi.LastWireguardHandshake = p.LastWireguardHandshake.Format(time.RFC3339)
}
if !p.RemoteLastSeenAtServer.IsZero() {
pi.LastSeenAtServer = p.RemoteLastSeenAtServer.Format(time.RFC3339)
}
pi.LatencyMs = p.Latency.Milliseconds()
pi.BytesRx = p.BytesRx
pi.BytesTx = p.BytesTx
pi.EffectiveConnectionMode = p.RemoteEffectiveConnectionMode
pi.ConfiguredConnectionMode = p.RemoteConfiguredConnectionMode
if len(p.RemoteGroups) > 0 {
pi.Groups = strings.Join(p.RemoteGroups, ",")
}
pi.ConnectionTypeExtended = peer.DeriveConnectionTypeExtended(p)
pi.IceBackoffFailures = int32(p.IceBackoffFailures)
if !p.IceBackoffNextRetry.IsZero() {
pi.IceBackoffNextRetry = p.IceBackoffNextRetry.Format(time.RFC3339)
}
pi.IceBackoffSuspended = p.IceBackoffSuspended
// AgentVersion / OsVersion: peer.State does not expose these fields;
// left empty until daemon surfaces them (future phase).

peerInfos[n] = pi
}
return &PeerInfoArray{items: peerInfos}
Expand Down Expand Up @@ -394,6 +434,102 @@ func (c *Client) RemoveConnectionListener() {
c.recorder.RemoveConnectionListener()
}

// GetServerPushedConnectionMode returns the canonical name of the
// connection mode the management server most recently pushed via
// PeerConfig (independent of any local profile/env override). Returns
// an empty string when the engine has not connected yet or the server
// has not pushed a value -- the Android UI then knows to display
// just "Follow server" without the (currently: ...) suffix.
func (c *Client) GetServerPushedConnectionMode() string {
cm := c.connMgrSafe()
if cm == nil {
return ""
}
return cm.ServerPushedMode().String()
}

// GetServerPushedRelayTimeoutSecs returns the relay timeout in seconds
// most recently pushed by the management server, or 0 when no value
// has been received. Used by the Android UI as a hint.
func (c *Client) GetServerPushedRelayTimeoutSecs() int64 {
cm := c.connMgrSafe()
if cm == nil {
return 0
}
return int64(cm.ServerPushedRelayTimeoutSecs())
}

// GetServerPushedP2pTimeoutSecs returns the ICE-only timeout (seconds)
// most recently pushed by the management server.
func (c *Client) GetServerPushedP2pTimeoutSecs() int64 {
cm := c.connMgrSafe()
if cm == nil {
return 0
}
return int64(cm.ServerPushedP2pTimeoutSecs())
}

// GetServerPushedP2pRetryMaxSecs returns the ICE-backoff cap (seconds)
// most recently pushed by the management server.
func (c *Client) GetServerPushedP2pRetryMaxSecs() int64 {
cm := c.connMgrSafe()
if cm == nil {
return 0
}
return int64(cm.ServerPushedP2pRetryMaxSecs())
}

// GetConfiguredPeersTotal returns the total number of configured peers
// (server-online + server-offline). Phase 3.7i (#5989).
func (c *Client) GetConfiguredPeersTotal() int64 {
return int64(c.recorder.GetFullStatus().ConfiguredPeersTotal)
}

// GetServerOnlinePeers returns the number of peers that are reachable via
// the server (P2P + Relayed + Idle). Phase 3.7i (#5989).
func (c *Client) GetServerOnlinePeers() int64 {
return int64(c.recorder.GetFullStatus().ServerOnlinePeers)
}

// GetP2PConnectedPeers returns the number of peers connected via direct
// P2P (ICE). Phase 3.7i (#5989).
func (c *Client) GetP2PConnectedPeers() int64 {
return int64(c.recorder.GetFullStatus().P2PConnectedPeers)
}

// GetRelayedConnectedPeers returns the number of peers connected via relay.
// Phase 3.7i (#5989).
func (c *Client) GetRelayedConnectedPeers() int64 {
return int64(c.recorder.GetFullStatus().RelayedConnectedPeers)
}

// GetIdleOnlinePeers returns the number of peers that are online on the
// server but have no active connection yet. Phase 3.7i (#5989).
func (c *Client) GetIdleOnlinePeers() int64 {
return int64(c.recorder.GetFullStatus().IdleOnlinePeers)
}

// GetServerOfflinePeers returns the number of peers that are not reachable
// via the server. Phase 3.7i (#5989).
func (c *Client) GetServerOfflinePeers() int64 {
return int64(c.recorder.GetFullStatus().ServerOfflinePeers)
}

// connMgrSafe is a small helper that walks the Client -> ConnectClient
// -> Engine -> ConnMgr chain and returns nil at the first nil pointer.
// Each accessor that surfaces engine state to the Android UI uses it.
func (c *Client) connMgrSafe() *internal.ConnMgr {
cc := c.getConnectClient()
if cc == nil {
return nil
}
engine := cc.Engine()
if engine == nil {
return nil
}
return engine.ConnMgr()
}

func (c *Client) toggleRoute(command routeCommand) error {
return command.toggleRoute()
}
Expand Down
31 changes: 31 additions & 0 deletions client/android/peer_notifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,37 @@ type PeerInfo struct {
FQDN string
ConnStatus int
Routes PeerRoutes

// Phase 3.7i (#5989): per-peer enrichment fields. Strings for
// gomobile-friendliness (no time.Time / no []string).
Relayed bool
ServerOnline bool
LocalIceCandidateEndpoint string
RemoteIceCandidateEndpoint string
RelayServerAddress string
LastWireguardHandshake string // RFC3339; "" if zero
LastSeenAtServer string // RFC3339; "" if zero
LatencyMs int64
BytesRx int64
BytesTx int64
EffectiveConnectionMode string
ConfiguredConnectionMode string
Groups string // comma-separated
AgentVersion string
OsVersion string
// Phase 3.7i hybrid display: daemon-derived UI label.
// Values: "", "P2P", "Relayed", "Relayed (negotiating P2P)".
// UIs should prefer this over (Relayed bool) when non-empty so the
// transient post-wakeup negotiation window renders identically
// across Android / Windows / Dashboard.
ConnectionTypeExtended string

// Phase 3.7i lifecycle hardening: ICE-backoff snapshot. Lets the UI
// explain why a peer is staying on Relayed when failures pile up
// (gomobile-friendly: no time.Time exported — RFC3339 string).
IceBackoffFailures int32
IceBackoffNextRetry string // RFC3339; "" if zero
IceBackoffSuspended bool
}

func (p *PeerInfo) GetPeerRoutes() *PeerRoutes {
Expand Down
101 changes: 101 additions & 0 deletions client/android/preferences.go
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,107 @@ func (p *Preferences) SetBlockInbound(block bool) {
p.configInput.BlockInbound = &block
}

// GetConnectionMode returns the locally configured connection-mode override
// (canonical lower-kebab-case: "relay-forced", "p2p", "p2p-lazy",
// "p2p-dynamic", "follow-server"), or empty string if no local override
// is configured -- the daemon will then follow the server-pushed value.
func (p *Preferences) GetConnectionMode() (string, error) {
if p.configInput.ConnectionMode != nil {
return *p.configInput.ConnectionMode, nil
}
cfg, err := profilemanager.ReadConfig(p.configInput.ConfigPath)
if err != nil {
return "", err
}
return cfg.ConnectionMode, nil
}

// SetConnectionMode stores a local override for the connection mode.
// Pass an empty string to clear the override (revert to following the
// server-pushed value).
func (p *Preferences) SetConnectionMode(mode string) {
m := mode
p.configInput.ConnectionMode = &m
}

// GetRelayTimeoutSeconds returns the locally configured relay-worker
// inactivity timeout in seconds, or 0 if no override is set (follow
// server-pushed value, or built-in default if the server has none).
func (p *Preferences) GetRelayTimeoutSeconds() (int64, error) {
if p.configInput.RelayTimeoutSeconds != nil {
return int64(*p.configInput.RelayTimeoutSeconds), nil
}
cfg, err := profilemanager.ReadConfig(p.configInput.ConfigPath)
if err != nil {
return 0, err
}
return int64(cfg.RelayTimeoutSeconds), nil
}

// SetRelayTimeoutSeconds stores a local override for the relay timeout.
// Pass 0 to clear the override. Negative values are clamped to 0;
// values larger than MaxUint32 are clamped to MaxUint32. The Android
// AdvancedFragment UI already clamps negatives but a Java caller using
// the bare gomobile API directly would otherwise wrap silently.
func (p *Preferences) SetRelayTimeoutSeconds(secs int64) {
v := clampUint32Seconds(secs)
p.configInput.RelayTimeoutSeconds = &v
}
Comment on lines +347 to +355
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

cfg_file="$(fd -p 'config.go' client/internal/profilemanager | head -n1)"
if [[ -z "${cfg_file}" ]]; then
  echo "Could not find client/internal/profilemanager/config.go" >&2
  exit 1
fi

echo "=== ConfigInput / Config timeout fields ==="
rg -n -C3 'type ConfigInput struct|type Config struct|RelayTimeoutSeconds|P2pTimeoutSeconds|P2pRetryMaxSeconds|ConnectionMode' "$cfg_file"

echo
echo "=== Read / write paths ==="
rg -n -C5 'func ReadConfig|func UpdateOrCreateConfig' "$cfg_file"

echo
echo "=== Any nil-vs-zero normalization before persist ==="
rg -n -C3 'RelayTimeoutSeconds.*0|P2pTimeoutSeconds.*0|P2pRetryMaxSeconds.*0|ConnectionMode.*follow-server|nil' client/internal/profilemanager client/android

Repository: netbirdio/netbird

Length of output: 50374


🏁 Script executed:

cat -n client/android/preferences.go | sed -n '340,400p'

Repository: netbirdio/netbird

Length of output: 2798


🏁 Script executed:

cat -n client/android/preferences.go | sed -n '398,420p'

Repository: netbirdio/netbird

Length of output: 713


Fix setters to write nil when 0 is passed, not &0.

The documented behavior—"Pass 0 to clear the override"—is broken. When you call SetRelayTimeoutSeconds(0), the setter always creates a non-nil pointer to 0 and stores it in configInput.RelayTimeoutSeconds. When UpdateOrCreateConfig is called, it dereferences this pointer and writes literal 0 to the config file.

Per the Config struct documentation, local 0 means "follow server," so this persists an override (follow-server behavior) instead of clearing the local override. The same issue affects SetP2pTimeoutSeconds and SetP2pRetryMaxSeconds.

Each setter should check if the clamped value is 0 and write nil instead of &0:

func (p *Preferences) SetRelayTimeoutSeconds(secs int64) {
	v := clampUint32Seconds(secs)
	if v == 0 {
		p.configInput.RelayTimeoutSeconds = nil
	} else {
		p.configInput.RelayTimeoutSeconds = &v
	}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/android/preferences.go` around lines 347 - 355, The setters
(Preferences.SetRelayTimeoutSeconds, and likewise SetP2pTimeoutSeconds and
SetP2pRetryMaxSeconds) currently always take the clamped uint32 value v and
assign its address to p.configInput.* which creates a non-nil pointer for 0;
instead detect when clampUint32Seconds(secs) returns 0 and set the corresponding
p.configInput.RelayTimeoutSeconds / P2pTimeoutSeconds / P2pRetryMaxSeconds to
nil, otherwise set it to a pointer to v so passing 0 clears the override rather
than persisting a literal 0.


// GetP2pTimeoutSeconds returns the locally configured ICE-worker
// inactivity timeout in seconds (only effective in p2p-dynamic mode),
// or 0 if no override is set.
func (p *Preferences) GetP2pTimeoutSeconds() (int64, error) {
if p.configInput.P2pTimeoutSeconds != nil {
return int64(*p.configInput.P2pTimeoutSeconds), nil
}
cfg, err := profilemanager.ReadConfig(p.configInput.ConfigPath)
if err != nil {
return 0, err
}
return int64(cfg.P2pTimeoutSeconds), nil
}

// SetP2pTimeoutSeconds stores a local override for the p2p timeout.
// Pass 0 to clear the override. See SetRelayTimeoutSeconds for clamping.
func (p *Preferences) SetP2pTimeoutSeconds(secs int64) {
v := clampUint32Seconds(secs)
p.configInput.P2pTimeoutSeconds = &v
}

// GetP2pRetryMaxSeconds returns the locally configured cap on the
// per-peer ICE-failure backoff schedule, or 0 if no override is set.
func (p *Preferences) GetP2pRetryMaxSeconds() (int64, error) {
if p.configInput.P2pRetryMaxSeconds != nil {
return int64(*p.configInput.P2pRetryMaxSeconds), nil
}
cfg, err := profilemanager.ReadConfig(p.configInput.ConfigPath)
if err != nil {
return 0, err
}
return int64(cfg.P2pRetryMaxSeconds), nil
}

// SetP2pRetryMaxSeconds stores a local override for the backoff cap.
// Pass 0 to clear the override. See SetRelayTimeoutSeconds for clamping.
func (p *Preferences) SetP2pRetryMaxSeconds(secs int64) {
v := clampUint32Seconds(secs)
p.configInput.P2pRetryMaxSeconds = &v
}

// clampUint32Seconds maps an int64 seconds value into the uint32 range
// the daemon stores internally. Negative -> 0. >MaxUint32 -> MaxUint32.
// Defensive against Java callers that bypass UI validation.
func clampUint32Seconds(secs int64) uint32 {
if secs <= 0 {
return 0
}
if secs > int64(^uint32(0)) {
return ^uint32(0)
}
return uint32(secs)
}

// Commit writes out the changes to the config file
func (p *Preferences) Commit() error {
_, err := profilemanager.UpdateOrCreateConfig(p.configInput)
Expand Down
37 changes: 37 additions & 0 deletions client/android/preferences_clamp_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package android

import (
"math"
"testing"
)

// Codex review: Preferences.SetXxxSeconds used to cast int64 directly
// to uint32, silently wrapping negatives into huge positives and
// truncating values >MaxUint32. Lock down the new clamp behavior.
func TestClampUint32Seconds(t *testing.T) {
maxU := uint32(math.MaxUint32)
tests := []struct {
name string
input int64
want uint32
}{
{"zero", 0, 0},
{"one", 1, 1},
{"3h_typical", 10800, 10800},
{"24h_typical", 86400, 86400},
{"max_uint32_exact", int64(math.MaxUint32), maxU},
{"max_uint32_plus_one_clamps", int64(math.MaxUint32) + 1, maxU},
{"int64_max_clamps", math.MaxInt64, maxU},
{"negative_one_clamps_to_zero", -1, 0},
{"negative_huge_clamps_to_zero", -86400, 0},
{"int64_min_clamps_to_zero", math.MinInt64, 0},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got := clampUint32Seconds(tc.input)
if got != tc.want {
t.Errorf("clampUint32Seconds(%d) = %d, want %d", tc.input, got, tc.want)
}
})
}
}
Loading
Loading