Skip to content

Commit 70129c0

Browse files
committed
fix(client): fix NetworkAddresses discovery on Android and re-sync on network change
Android 10+ restricts MAC addresses and Go's net.Interfaces() is broken on Android 11+ due to SELinux restrictions on netlink sockets. Use wlynxg/anet as drop-in replacement on Android. Remove MAC-based interface filter. Add 10s network address watcher with 30s debounce to re-sync with management server on WiFi/cellular transitions. Fixes #3614 #2962
1 parent decb5dd commit 70129c0

9 files changed

Lines changed: 290 additions & 6 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,5 @@ infrastructure_files/setup-*.env
3333
vendor/
3434
/netbird
3535
client/netbird-electron/
36+
build/
37+
docs/superpowers/

client/android/client.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,22 @@ func (c *Client) RenewTun(fd int) error {
185185
return e.RenewTun(fd)
186186
}
187187

188+
// OnUnderlyingNetworkChanged should be called by the Android layer when the
189+
// underlying network changes (e.g., WiFi ↔ cellular). It triggers a re-sync
190+
// of NetworkAddresses with the management server so posture checks evaluate
191+
// the current network state immediately, without waiting for the next
192+
// periodic sync cycle.
193+
func (c *Client) OnUnderlyingNetworkChanged() {
194+
if c.connectClient == nil {
195+
return
196+
}
197+
e := c.connectClient.Engine()
198+
if e == nil {
199+
return
200+
}
201+
e.ResyncNetworkAddresses()
202+
}
203+
188204
// SetTraceLogLevel configure the logger to trace level
189205
func (c *Client) SetTraceLogLevel() {
190206
log.SetLevel(log.TraceLevel)

client/internal/engine.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,10 @@ type Engine struct {
210210
// checks are the client-applied posture checks that need to be evaluated on the client
211211
checks []*mgmProto.Checks
212212

213+
// lastNetworkAddresses tracks reported addresses for change detection
214+
lastNetworkAddresses []system.NetworkAddress
215+
lastNetworkAddressSync time.Time
216+
213217
relayManager *relayClient.Manager
214218
stateManager *statemanager.Manager
215219
srWatcher *guard.SRWatcher
@@ -560,6 +564,10 @@ func (e *Engine) Start(netbirdConfig *mgmProto.NetbirdConfig, mgmtURL *url.URL)
560564
e.receiveManagementEvents()
561565
e.receiveJobEvents()
562566

567+
// watch for network address changes (WiFi ↔ mobile) for posture checks
568+
e.shutdownWg.Add(1)
569+
go e.startNetworkAddressWatcher()
570+
563571
// starting network monitor at the very last to avoid disruptions
564572
e.startNetworkMonitor()
565573

@@ -884,6 +892,10 @@ func (e *Engine) handleSync(update *mgmProto.SyncResponse) error {
884892
return err
885893
}
886894

895+
// Fallback: detect network address changes during periodic sync in case
896+
// platform-specific callbacks (e.g., Android NetworkCallback) were missed.
897+
e.resyncMetaIfNetworkChanged()
898+
887899
nm := update.GetNetworkMap()
888900
if nm == nil {
889901
return nil
@@ -1003,6 +1015,99 @@ func (e *Engine) updateChecksIfNew(checks []*mgmProto.Checks) error {
10031015
return nil
10041016
}
10051017

1018+
// ResyncNetworkAddresses can be called externally (e.g., from Android
1019+
// network change callbacks) to immediately re-sync NetworkAddresses.
1020+
func (e *Engine) ResyncNetworkAddresses() {
1021+
e.syncMsgMux.Lock()
1022+
defer e.syncMsgMux.Unlock()
1023+
e.resyncMetaIfNetworkChanged()
1024+
}
1025+
1026+
// startNetworkAddressWatcher polls for network address changes every 10s.
1027+
// This catches cases where platform callbacks are missed or delayed,
1028+
// ensuring posture checks always evaluate the current network state.
1029+
func (e *Engine) startNetworkAddressWatcher() {
1030+
defer e.shutdownWg.Done()
1031+
ticker := time.NewTicker(10 * time.Second)
1032+
defer ticker.Stop()
1033+
1034+
for {
1035+
select {
1036+
case <-e.ctx.Done():
1037+
return
1038+
case <-ticker.C:
1039+
e.syncMsgMux.Lock()
1040+
e.resyncMetaIfNetworkChanged()
1041+
e.syncMsgMux.Unlock()
1042+
}
1043+
}
1044+
}
1045+
1046+
// resyncMetaIfNetworkChanged detects changes in local network addresses
1047+
// (e.g., WiFi reconnect on mobile) and re-syncs meta with the management
1048+
// server so that posture checks evaluate the current network state.
1049+
func (e *Engine) resyncMetaIfNetworkChanged() {
1050+
// Debounce: don't re-sync more than once per 30 seconds to avoid
1051+
// flapping during VPN tunnel setup when interfaces are in flux.
1052+
if time.Since(e.lastNetworkAddressSync) < 30*time.Second {
1053+
return
1054+
}
1055+
1056+
info := system.GetInfo(e.ctx)
1057+
if info == nil {
1058+
return
1059+
}
1060+
1061+
current := info.NetworkAddresses
1062+
if networkAddressesEqual(e.lastNetworkAddresses, current) {
1063+
return
1064+
}
1065+
1066+
log.Infof("network addresses changed (%d -> %d addrs), re-syncing meta with management server",
1067+
len(e.lastNetworkAddresses), len(current))
1068+
e.lastNetworkAddresses = current
1069+
e.lastNetworkAddressSync = time.Now()
1070+
1071+
info.SetFlags(
1072+
e.config.RosenpassEnabled,
1073+
e.config.RosenpassPermissive,
1074+
&e.config.ServerSSHAllowed,
1075+
e.config.DisableClientRoutes,
1076+
e.config.DisableServerRoutes,
1077+
e.config.DisableDNS,
1078+
e.config.DisableFirewall,
1079+
e.config.BlockLANAccess,
1080+
e.config.BlockInbound,
1081+
e.config.LazyConnectionEnabled,
1082+
e.config.EnableSSHRoot,
1083+
e.config.EnableSSHSFTP,
1084+
e.config.EnableSSHLocalPortForwarding,
1085+
e.config.EnableSSHRemotePortForwarding,
1086+
e.config.DisableSSHAuth,
1087+
)
1088+
1089+
if err := e.mgmClient.SyncMeta(info); err != nil {
1090+
log.Warnf("failed to re-sync meta after network change: %v", err)
1091+
}
1092+
}
1093+
1094+
func networkAddressesEqual(a, b []system.NetworkAddress) bool {
1095+
if len(a) != len(b) {
1096+
return false
1097+
}
1098+
// Sort-unabhängiger Vergleich: prüfe ob alle IPs aus a in b vorkommen
1099+
bSet := make(map[string]struct{}, len(b))
1100+
for _, addr := range b {
1101+
bSet[addr.NetIP.String()] = struct{}{}
1102+
}
1103+
for _, addr := range a {
1104+
if _, ok := bSet[addr.NetIP.String()]; !ok {
1105+
return false
1106+
}
1107+
}
1108+
return true
1109+
}
1110+
10061111
func (e *Engine) updateConfig(conf *mgmProto.PeerConfig) error {
10071112
if e.wgInterface == nil {
10081113
return errors.New("wireguard interface is not initialized")
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package internal
2+
3+
import (
4+
"net/netip"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
9+
"github.com/netbirdio/netbird/client/system"
10+
)
11+
12+
func TestNetworkAddressesEqual(t *testing.T) {
13+
tests := []struct {
14+
name string
15+
a []system.NetworkAddress
16+
b []system.NetworkAddress
17+
want bool
18+
}{
19+
{
20+
name: "both nil",
21+
a: nil,
22+
b: nil,
23+
want: true,
24+
},
25+
{
26+
name: "both empty",
27+
a: []system.NetworkAddress{},
28+
b: []system.NetworkAddress{},
29+
want: true,
30+
},
31+
{
32+
name: "nil vs empty",
33+
a: nil,
34+
b: []system.NetworkAddress{},
35+
want: true,
36+
},
37+
{
38+
name: "same addresses same order",
39+
a: []system.NetworkAddress{
40+
{NetIP: netip.MustParsePrefix("192.168.1.10/24")},
41+
{NetIP: netip.MustParsePrefix("10.0.0.1/8")},
42+
},
43+
b: []system.NetworkAddress{
44+
{NetIP: netip.MustParsePrefix("192.168.1.10/24")},
45+
{NetIP: netip.MustParsePrefix("10.0.0.1/8")},
46+
},
47+
want: true,
48+
},
49+
{
50+
name: "same addresses different order",
51+
a: []system.NetworkAddress{
52+
{NetIP: netip.MustParsePrefix("10.0.0.1/8")},
53+
{NetIP: netip.MustParsePrefix("192.168.1.10/24")},
54+
},
55+
b: []system.NetworkAddress{
56+
{NetIP: netip.MustParsePrefix("192.168.1.10/24")},
57+
{NetIP: netip.MustParsePrefix("10.0.0.1/8")},
58+
},
59+
want: true,
60+
},
61+
{
62+
name: "different lengths",
63+
a: []system.NetworkAddress{
64+
{NetIP: netip.MustParsePrefix("192.168.1.10/24")},
65+
},
66+
b: []system.NetworkAddress{
67+
{NetIP: netip.MustParsePrefix("192.168.1.10/24")},
68+
{NetIP: netip.MustParsePrefix("10.0.0.1/8")},
69+
},
70+
want: false,
71+
},
72+
{
73+
name: "different addresses",
74+
a: []system.NetworkAddress{
75+
{NetIP: netip.MustParsePrefix("192.168.1.10/24")},
76+
},
77+
b: []system.NetworkAddress{
78+
{NetIP: netip.MustParsePrefix("172.16.0.1/12")},
79+
},
80+
want: false,
81+
},
82+
{
83+
name: "wifi to mobile switch",
84+
a: []system.NetworkAddress{
85+
{NetIP: netip.MustParsePrefix("192.168.91.167/24")},
86+
{NetIP: netip.MustParsePrefix("100.87.143.60/16")},
87+
},
88+
b: []system.NetworkAddress{
89+
{NetIP: netip.MustParsePrefix("93.111.154.63/24")},
90+
{NetIP: netip.MustParsePrefix("100.87.143.60/16")},
91+
},
92+
want: false,
93+
},
94+
}
95+
96+
for _, tt := range tests {
97+
t.Run(tt.name, func(t *testing.T) {
98+
got := networkAddressesEqual(tt.a, tt.b)
99+
assert.Equal(t, tt.want, got)
100+
})
101+
}
102+
}

client/system/info.go

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -146,21 +146,20 @@ func extractDeviceName(ctx context.Context, defaultName string) string {
146146
}
147147

148148
func networkAddresses() ([]NetworkAddress, error) {
149-
interfaces, err := net.Interfaces()
149+
interfaces, err := getNetInterfaces()
150150
if err != nil {
151151
return nil, err
152152
}
153153

154154
var netAddresses []NetworkAddress
155155
for _, iface := range interfaces {
156-
if iface.HardwareAddr.String() == "" {
157-
continue
158-
}
159-
addrs, err := iface.Addrs()
156+
addrs, err := getInterfaceAddrs(&iface)
160157
if err != nil {
161158
continue
162159
}
163160

161+
mac := iface.HardwareAddr.String()
162+
164163
for _, address := range addrs {
165164
ipNet, ok := address.(*net.IPNet)
166165
if !ok {
@@ -173,7 +172,7 @@ func networkAddresses() ([]NetworkAddress, error) {
173172

174173
netAddr := NetworkAddress{
175174
NetIP: netip.MustParsePrefix(ipNet.String()),
176-
Mac: iface.HardwareAddr.String(),
175+
Mac: mac,
177176
}
178177

179178
if isDuplicated(netAddresses, netAddr) {

client/system/info_android.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ func GetInfo(ctx context.Context) *Info {
3030
kernelVersion = osInfo[2]
3131
}
3232

33+
addrs, err := networkAddresses()
34+
if err != nil {
35+
log.Warnf("failed to discover network addresses: %s", err)
36+
}
37+
3338
gio := &Info{
3439
GoOS: runtime.GOOS,
3540
Kernel: kernel,
@@ -41,6 +46,7 @@ func GetInfo(ctx context.Context) *Info {
4146
NetbirdVersion: version.NetbirdVersion(),
4247
UIVersion: extractUIVersion(ctx),
4348
KernelVersion: kernelVersion,
49+
NetworkAddresses: addrs,
4450
SystemSerialNumber: serial(),
4551
SystemProductName: productModel(),
4652
SystemManufacturer: productManufacturer(),

client/system/info_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,27 @@ func Test_NetAddresses(t *testing.T) {
4343
t.Errorf("no network addresses found")
4444
}
4545
}
46+
47+
func Test_networkAddresses(t *testing.T) {
48+
addrs, err := networkAddresses()
49+
assert.NoError(t, err)
50+
assert.NotEmpty(t, addrs, "should discover at least one network address")
51+
52+
for _, addr := range addrs {
53+
assert.True(t, addr.NetIP.IsValid(), "address should be valid: %s", addr.NetIP)
54+
assert.False(t, addr.NetIP.Addr().IsLoopback(), "should not include loopback addresses")
55+
}
56+
}
57+
58+
func Test_networkAddresses_noDuplicates(t *testing.T) {
59+
addrs, err := networkAddresses()
60+
assert.NoError(t, err)
61+
62+
seen := make(map[string]struct{})
63+
for _, addr := range addrs {
64+
key := addr.NetIP.String()
65+
_, exists := seen[key]
66+
assert.False(t, exists, "duplicate address found: %s", key)
67+
seen[key] = struct{}{}
68+
}
69+
}

client/system/network_addresses.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
//go:build !android
2+
3+
package system
4+
5+
import "net"
6+
7+
func getNetInterfaces() ([]net.Interface, error) {
8+
return net.Interfaces()
9+
}
10+
11+
func getInterfaceAddrs(iface *net.Interface) ([]net.Addr, error) {
12+
return iface.Addrs()
13+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
//go:build android
2+
3+
package system
4+
5+
import (
6+
"net"
7+
8+
"github.com/wlynxg/anet"
9+
)
10+
11+
func getNetInterfaces() ([]net.Interface, error) {
12+
return anet.Interfaces()
13+
}
14+
15+
func getInterfaceAddrs(iface *net.Interface) ([]net.Addr, error) {
16+
return anet.InterfaceAddrsByInterface(iface)
17+
}

0 commit comments

Comments
 (0)