Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
108 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
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
138 changes: 134 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,50 @@ 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, ",")
}
// 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 +428,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
18 changes: 18 additions & 0 deletions client/android/peer_notifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,24 @@ 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
}

func (p *PeerInfo) GetPeerRoutes() *PeerRoutes {
Expand Down
85 changes: 85 additions & 0 deletions client/android/preferences.go
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,91 @@ 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
}
Comment on lines +325 to +331
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 | 🟡 Minor | ⚡ Quick win

SetConnectionMode("") does not actually clear the override.

The doc says "Pass an empty string to clear the override (revert to following the server-pushed value)", but the implementation stores &"" (non-nil pointer to an empty string). GetConnectionMode checks p.configInput.ConnectionMode != nil first, so after SetConnectionMode("") the getter returns "" and never falls through to read cfg.ConnectionMode — the local override is set-to-empty rather than cleared, which is observably different from "follow server".

🛡️ Proposed fix
 func (p *Preferences) SetConnectionMode(mode string) {
-	m := mode
-	p.configInput.ConnectionMode = &m
+	if mode == "" {
+		p.configInput.ConnectionMode = nil
+		return
+	}
+	m := mode
+	p.configInput.ConnectionMode = &m
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 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
}
// 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) {
if mode == "" {
p.configInput.ConnectionMode = nil
return
}
m := mode
p.configInput.ConnectionMode = &m
}
🤖 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 325 - 331, SetConnectionMode
currently stores a non-nil pointer to an empty string so calling
SetConnectionMode("") doesn’t clear the override; change SetConnectionMode to
set p.configInput.ConnectionMode = nil when mode == "" and otherwise set it to a
pointer to the provided string (e.g., allocate a new string variable or use &m)
so GetConnectionMode’s nil-check will correctly fall through to the
server-pushed value; update the logic in the SetConnectionMode method that
manipulates p.configInput.ConnectionMode accordingly.


// 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.
func (p *Preferences) SetRelayTimeoutSeconds(secs int64) {
v := uint32(secs)
p.configInput.RelayTimeoutSeconds = &v
}

// 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.
func (p *Preferences) SetP2pTimeoutSeconds(secs int64) {
v := uint32(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.
func (p *Preferences) SetP2pRetryMaxSeconds(secs int64) {
v := uint32(secs)
p.configInput.P2pRetryMaxSeconds = &v
Comment on lines +349 to +392
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 | 🟠 Major | ⚡ Quick win

Guard the int64uint32 conversions.

These setters silently wrap invalid inputs; for example, -1 becomes 4294967295. That stores a bogus timeout/backoff override instead of clearing or rejecting it.

🤖 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 349 - 392, The setters
(SetRelayTimeoutSeconds, SetP2pTimeoutSeconds, SetP2pRetryMaxSeconds) currently
cast int64→uint32 and silently wrap negative or too-large values; change each to
guard inputs: treat secs <= 0 as "clear the override" by setting the
corresponding p.configInput.RelayTimeoutSeconds / P2pTimeoutSeconds /
P2pRetryMaxSeconds to nil, if secs > math.MaxUint32 clamp to math.MaxUint32
before converting, otherwise convert safely to uint32 and store the pointer;
implement these checks so negative values do not wrap into large uint32s and
zero clears the override.

}

// Commit writes out the changes to the config file
func (p *Preferences) Commit() error {
_, err := profilemanager.UpdateOrCreateConfig(p.configInput)
Expand Down
17 changes: 17 additions & 0 deletions client/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ const (
extraIFaceBlackListFlag = "extra-iface-blacklist"
dnsRouteIntervalFlag = "dns-router-interval"
enableLazyConnectionFlag = "enable-lazy-connection"
connectionModeFlag = "connection-mode"
relayTimeoutFlag = "relay-timeout"
p2pTimeoutFlag = "p2p-timeout"
p2pRetryMaxFlag = "p2p-retry-max"
mtuFlag = "mtu"
)

Expand Down Expand Up @@ -72,6 +76,10 @@ var (
anonymizeFlag bool
dnsRouteInterval time.Duration
lazyConnEnabled bool
connectionMode string
relayTimeoutSecs uint32
p2pTimeoutSecs uint32
p2pRetryMaxSecs uint32
mtu uint16
profilesDisabled bool
updateSettingsDisabled bool
Expand Down Expand Up @@ -192,6 +200,15 @@ 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).")
upCmd.PersistentFlags().Uint32Var(&p2pRetryMaxSecs, p2pRetryMaxFlag, 0,
"[Experimental] Maximum ICE-failure-backoff interval in seconds. 0 = use server-pushed value (or built-in default 15 min). Effective in p2p-dynamic mode (Phase 3 of #5989).")

}

Expand Down
18 changes: 18 additions & 0 deletions client/cmd/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,24 @@ func init() {
installCmd.Flags().StringSliceVar(&serviceEnvVars, "service-env", nil, serviceEnvDesc)
reconfigureCmd.Flags().StringSliceVar(&serviceEnvVars, "service-env", nil, serviceEnvDesc)

// Profile-level connection-mode + timeout flags. Same semantics as on
// `netbird up` but writeable at install time so server/headless
// installs can pre-seed the active profile before the daemon starts.
// Same package-level vars are shared with upCmd; on `up` they take
// effect through setupConfig(), here we apply them once before
// installing the service so the daemon picks them up on first run.
for _, c := range []*cobra.Command{installCmd, reconfigureCmd} {
c.Flags().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.")
c.Flags().Uint32Var(&relayTimeoutSecs, relayTimeoutFlag, 0,
"[Experimental] Relay-worker idle timeout in seconds. 0 = use server-pushed value (or built-in default).")
c.Flags().Uint32Var(&p2pTimeoutSecs, p2pTimeoutFlag, 0,
"[Experimental] ICE-worker idle timeout in seconds. 0 = use server-pushed value. Only effective in p2p-dynamic mode.")
c.Flags().Uint32Var(&p2pRetryMaxSecs, p2pRetryMaxFlag, 0,
"[Experimental] Maximum ICE-failure-backoff interval in seconds. 0 = use server-pushed value (or built-in default 15 min).")
}

rootCmd.AddCommand(serviceCmd)
}

Expand Down
Loading
Loading