1. TL;DR
I’ve set up a GitHub Workflow that automatically patchs the official code and builds working binaries for Android.
Just grab the latest binary from the latest workflow run here: https://github.com/lzc256/netbird/actions/workflows/sync-and-build.yml
Done! You can now enjoy a working IP sniffing, ICE & P2P peering experience.
The built binary is nothing different from the official one, but just applied a patch that replaces the original net library with anet, which works for Android IP sniffing. It’s built every 6 hours, up to date with the upstream branch.
2. Background
I tried running the netbird-linux-arm64 cli directly in Android Termux. Wrapping it in a script seemed to get it working, except the P2P feature. You always get this error in the log:
2026-03-15T14:29:17.803Z ERRO client/internal/peer/ice/agent.go:44: failed to create pion's stdnet: route ip+net: netlinkrib: permission denied
What’s worse, all STUN servers fail. Since ICE is uninitialized, the android client could never connect to other peers via P2P. Depending on the relay results in high latency and slow speed, which is unacceptable.
3. Workaround
After hard efforts of investigation for about 1 day, I found similar issues in other projects. In a nutshell, in Android 11 (API 30), the Android kernel forbids direct access to /proc/net, which disables the golang library net. A bunch of methods fail, including but not limited to:
net.Interfaces()oif.Addrs()- …
These are the root of the problem.
Luckily, there are relevant discussions. I’m fortunate enough to find the most valuable one: https://github.com/golang/go/issues/40569, in which @wlynxg posted their own workaround repo wlynxg/anet that provides a net library alternative which works on Android, anet.
We could just simply replace every net with anet in the NetBird repo, after which the issue was resolved.
All modifications are based on commit 3e6baea (v0.66.4). If there are conflicts or more netlinkrib: permission denied errors arise after an upstream update, just simply replace all net methods in the new code.
Build script:
go mod edit -replace github.com/wlynxg/anet=github.com/lzc256/anet@latest
go mod tidy
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -v -trimpath -tags "load_wgnt_from_rsrc" -ldflags "-s -w -checklinkname=0" -a -o netbird-anet-linux-arm64 ./client
android-anet.patch
diff --git a/client/firewall/uspfilter/localip.go b/client/firewall/uspfilter/localip.go
index ffc807f4..0167564b 100644
--- a/client/firewall/uspfilter/localip.go
+++ b/client/firewall/uspfilter/localip.go
@@ -7,6 +7,7 @@ import (
"sync"
log "github.com/sirupsen/logrus"
+ "github.com/wlynxg/anet"
"github.com/netbirdio/netbird/client/firewall/uspfilter/common"
)
@@ -87,7 +88,7 @@ func (m *localIPManager) processIP(ip netip.Addr, bitmap *[256]*ipv4LowBitmap, i
}
func (m *localIPManager) processInterface(iface net.Interface, bitmap *[256]*ipv4LowBitmap, ipv4Set map[netip.Addr]struct{}, ipv4Addresses *[]netip.Addr) {
- addrs, err := iface.Addrs()
+ addrs, err := anet.InterfaceAddrsByInterface(&iface)
if err != nil {
log.Debugf("get addresses for interface %s failed: %v", iface.Name, err)
return
@@ -140,7 +141,7 @@ func (m *localIPManager) UpdateLocalIPs(iface common.IFaceMapper) (err error) {
}
}
- interfaces, err := net.Interfaces()
+ interfaces, err := anet.Interfaces()
if err != nil {
log.Warnf("failed to get interfaces: %v", err)
} else {
diff --git a/client/internal/routemanager/systemops/systemops_generic.go b/client/internal/routemanager/systemops/systemops_generic.go
index ec219c7f..bc52562c 100644
--- a/client/internal/routemanager/systemops/systemops_generic.go
+++ b/client/internal/routemanager/systemops/systemops_generic.go
@@ -398,6 +398,7 @@ func ipToAddr(ip net.IP, intf *net.Interface) (netip.Addr, error) {
// IsAddrRouted checks if the candidate address would route to the vpn, in which case it returns true and the matched prefix.
func IsAddrRouted(addr netip.Addr, vpnRoutes []netip.Prefix) (bool, netip.Prefix) {
+ return false, netip.Prefix{}
localRoutes, err := hasSeparateRouting()
if err != nil {
if !errors.Is(err, ErrRoutingIsSeparate) {
diff --git a/client/internal/stdnet/discover_pion.go b/client/internal/stdnet/discover_pion.go
index 83b4a1d1..5e164482 100644
--- a/client/internal/stdnet/discover_pion.go
+++ b/client/internal/stdnet/discover_pion.go
@@ -1,9 +1,8 @@
package stdnet
import (
- "net"
-
"github.com/pion/transport/v3"
+ "github.com/wlynxg/anet"
)
type pionDiscover struct {
@@ -12,7 +11,7 @@ type pionDiscover struct {
func (d pionDiscover) iFaces() ([]*transport.Interface, error) {
ifs := []*transport.Interface{}
- oifs, err := net.Interfaces()
+ oifs, err := anet.Interfaces()
if err != nil {
return nil, err
}
@@ -20,7 +19,7 @@ func (d pionDiscover) iFaces() ([]*transport.Interface, error) {
for _, oif := range oifs {
ifc := transport.NewInterface(oif)
- addrs, err := oif.Addrs()
+ addrs, err := anet.InterfaceAddrsByInterface(&oif)
if err != nil {
return nil, err
}
diff --git a/client/system/info.go b/client/system/info.go
index 01176e76..8cc1a5b2 100644
--- a/client/system/info.go
+++ b/client/system/info.go
@@ -7,6 +7,7 @@ import (
"strings"
log "github.com/sirupsen/logrus"
+ "github.com/wlynxg/anet"
"google.golang.org/grpc/metadata"
"github.com/netbirdio/netbird/shared/management/proto"
@@ -146,7 +147,7 @@ func extractDeviceName(ctx context.Context, defaultName string) string {
}
func networkAddresses() ([]NetworkAddress, error) {
- interfaces, err := net.Interfaces()
+ interfaces, err := anet.Interfaces()
if err != nil {
return nil, err
}
@@ -156,7 +157,7 @@ func networkAddresses() ([]NetworkAddress, error) {
if iface.HardwareAddr.String() == "" {
continue
}
- addrs, err := iface.Addrs()
+ addrs, err := anet.InterfaceAddrsByInterface(&iface)
if err != nil {
continue
}
diff --git a/go.mod b/go.mod
index 4bcdbdc7..0eef0ba3 100644
--- a/go.mod
+++ b/go.mod
@@ -101,6 +101,7 @@ require (
github.com/ti-mo/conntrack v0.5.1
github.com/ti-mo/netfilter v0.5.2
github.com/vmihailenco/msgpack/v5 v5.4.1
+ github.com/wlynxg/anet v0.0.6-0.20250109065809-5501d401a269
github.com/yusufpapurcu/wmi v1.2.4
github.com/zcalusic/sysinfo v1.1.3
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0
@@ -267,7 +268,6 @@ require (
github.com/tklauser/numcpus v0.8.0 // indirect
github.com/vishvananda/netns v0.0.5 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
- github.com/wlynxg/anet v0.0.5 // indirect
github.com/yuin/goldmark v1.7.8 // indirect
github.com/zeebo/blake3 v0.2.3 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
@@ -298,3 +298,5 @@ replace github.com/pion/ice/v4 => github.com/netbirdio/ice/v4 v4.0.0-20250908184
replace github.com/libp2p/go-netroute => github.com/netbirdio/go-netroute v0.0.0-20240611143515-f59b0e1d3944
replace github.com/dexidp/dex => github.com/netbirdio/dex v0.244.0
+
+replace github.com/wlynxg/anet => github.com/lzc256/anet v0.0.0-20260316065608-bca8c2f29883
diff --git a/go.sum b/go.sum
index 1bd9396b..c8167d32 100644
--- a/go.sum
+++ b/go.sum
@@ -353,6 +353,8 @@ github.com/lrh3321/ipset-go v0.0.0-20250619021614-54a0a98ace81/go.mod h1:RD8ML/Y
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae h1:dIZY4ULFcto4tAFlj1FYZl8ztUZ13bdq+PLY+NOfbyI=
github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
+github.com/lzc256/anet v0.0.0-20260316065608-bca8c2f29883 h1:TgQZ0mjWKfGAEGzINtZQBOVX2p7fA86qsJzw32fW9h0=
+github.com/lzc256/anet v0.0.0-20260316065608-bca8c2f29883/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattermost/xml-roundtrip-validator v0.1.0 h1:RXbVD2UAl7A7nOTR4u7E3ILa4IbtvKBHw64LDsmu9hU=
@@ -583,8 +585,6 @@ github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IU
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
-github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
-github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
References:
https://github.com/golang/go/issues/40569#issuecomment-1614781739 https://codeberg.org/bg443/JetBird/issues/42 https://github.com/netbirdio/netbird/issues/1354
Loading Comment Component...
