Skip to content

Commit 81f45da

Browse files
authored
[client] Support embed.Client on Android with netstack mode (#5623)
* [client] Support embed.Client on Android with netstack mode embed.Client.Start() calls ConnectClient.Run() which passes an empty MobileDependency{}. On Android, the engine dereferences nil fields (IFaceDiscover, NetworkChangeListener, DnsReadyListener) causing panics. Provide complete no-op stubs so the engine's existing Android code paths work unchanged — zero modifications to engine.go: - Add androidRunOverride hook in Run() for Android-specific dispatch - Add runOnAndroidEmbed() with complete MobileDependency (all stubs) - Wire default stubs via init() in connect_android_default.go: noopIFaceDiscover, noopNetworkChangeListener, noopDnsReadyListener - Forward logPath to c.run() Tested: embed.Client starts on Android arm64, joins mesh via relay, discovers peers, localhost proxy works for TCP+UDP forwarding. * [client] Fix TestServiceParamsPath for Windows path separators Use filepath.Join in test assertions instead of hardcoded POSIX paths so the test passes on Windows where filepath.Join uses backslashes.
1 parent d670e73 commit 81f45da

3 files changed

Lines changed: 112 additions & 0 deletions

File tree

client/internal/connect.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ import (
4444
"github.com/netbirdio/netbird/version"
4545
)
4646

47+
// androidRunOverride is set on Android to inject mobile dependencies
48+
// when using embed.Client (which calls Run() with empty MobileDependency).
49+
var androidRunOverride func(c *ConnectClient, runningChan chan struct{}, logPath string) error
50+
4751
type ConnectClient struct {
4852
ctx context.Context
4953
config *profilemanager.Config
@@ -76,6 +80,9 @@ func (c *ConnectClient) SetUpdateManager(um *updater.Manager) {
7680

7781
// Run with main logic.
7882
func (c *ConnectClient) Run(runningChan chan struct{}, logPath string) error {
83+
if androidRunOverride != nil {
84+
return androidRunOverride(c, runningChan, logPath)
85+
}
7986
return c.run(MobileDependency{}, runningChan, logPath)
8087
}
8188

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
//go:build android
2+
3+
package internal
4+
5+
import (
6+
"net/netip"
7+
8+
"github.com/netbirdio/netbird/client/internal/dns"
9+
"github.com/netbirdio/netbird/client/internal/listener"
10+
"github.com/netbirdio/netbird/client/internal/stdnet"
11+
)
12+
13+
// noopIFaceDiscover is a stub ExternalIFaceDiscover for embed.Client on Android.
14+
// It returns an empty interface list, which means ICE P2P candidates won't be
15+
// discovered — connections will fall back to relay. Applications that need P2P
16+
// should provide a real implementation via runOnAndroidEmbed that uses
17+
// Android's ConnectivityManager to enumerate network interfaces.
18+
type noopIFaceDiscover struct{}
19+
20+
func (noopIFaceDiscover) IFaces() (string, error) {
21+
// Return empty JSON array — no local interfaces advertised for ICE.
22+
// This is intentional: without Android's ConnectivityManager, we cannot
23+
// reliably enumerate interfaces (netlink is restricted on Android 11+).
24+
// Relay connections still work; only P2P hole-punching is disabled.
25+
return "[]", nil
26+
}
27+
28+
// noopNetworkChangeListener is a stub for embed.Client on Android.
29+
// Network change events are ignored since the embed client manages its own
30+
// reconnection logic via the engine's built-in retry mechanism.
31+
type noopNetworkChangeListener struct{}
32+
33+
func (noopNetworkChangeListener) OnNetworkChanged(string) {
34+
// No-op: embed.Client relies on the engine's internal reconnection
35+
// logic rather than OS-level network change notifications.
36+
}
37+
38+
func (noopNetworkChangeListener) SetInterfaceIP(string) {
39+
// No-op: in netstack mode, the overlay IP is managed by the userspace
40+
// network stack, not by OS-level interface configuration.
41+
}
42+
43+
// noopDnsReadyListener is a stub for embed.Client on Android.
44+
// DNS readiness notifications are not needed in netstack/embed mode
45+
// since system DNS is disabled and DNS resolution happens externally.
46+
type noopDnsReadyListener struct{}
47+
48+
func (noopDnsReadyListener) OnReady() {
49+
// No-op: embed.Client does not need DNS readiness notifications.
50+
// System DNS is disabled in netstack mode.
51+
}
52+
53+
var _ stdnet.ExternalIFaceDiscover = noopIFaceDiscover{}
54+
var _ listener.NetworkChangeListener = noopNetworkChangeListener{}
55+
var _ dns.ReadyListener = noopDnsReadyListener{}
56+
57+
func init() {
58+
// Wire up the default override so embed.Client.Start() works on Android
59+
// with netstack mode. Provides complete no-op stubs for all mobile
60+
// dependencies so the engine's existing Android code paths work unchanged.
61+
// Applications that need P2P ICE or real DNS should replace this by
62+
// setting androidRunOverride before calling Start().
63+
androidRunOverride = func(c *ConnectClient, runningChan chan struct{}, logPath string) error {
64+
return c.runOnAndroidEmbed(
65+
noopIFaceDiscover{},
66+
noopNetworkChangeListener{},
67+
[]netip.AddrPort{},
68+
noopDnsReadyListener{},
69+
runningChan,
70+
logPath,
71+
)
72+
}
73+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
//go:build android
2+
3+
package internal
4+
5+
import (
6+
"net/netip"
7+
8+
"github.com/netbirdio/netbird/client/internal/dns"
9+
"github.com/netbirdio/netbird/client/internal/listener"
10+
"github.com/netbirdio/netbird/client/internal/stdnet"
11+
)
12+
13+
// runOnAndroidEmbed is like RunOnAndroid but accepts a runningChan
14+
// so embed.Client.Start() can detect when the engine is ready.
15+
// It provides complete MobileDependency so the engine's existing
16+
// Android code paths work unchanged.
17+
func (c *ConnectClient) runOnAndroidEmbed(
18+
iFaceDiscover stdnet.ExternalIFaceDiscover,
19+
networkChangeListener listener.NetworkChangeListener,
20+
dnsAddresses []netip.AddrPort,
21+
dnsReadyListener dns.ReadyListener,
22+
runningChan chan struct{},
23+
logPath string,
24+
) error {
25+
mobileDependency := MobileDependency{
26+
IFaceDiscover: iFaceDiscover,
27+
NetworkChangeListener: networkChangeListener,
28+
HostDNSAddresses: dnsAddresses,
29+
DnsReadyListener: dnsReadyListener,
30+
}
31+
return c.run(mobileDependency, runningChan, logPath)
32+
}

0 commit comments

Comments
 (0)