Skip to content
Open
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
f088b4a
Export RenewFD function for Android
pappz Sep 23, 2025
b2fbdfa
[client] add initial implementation of RenewTun for android device
doromaraujo Sep 24, 2025
c58ccc5
[android] add usage of routeSelector when retrieving networks
doromaraujo Sep 26, 2025
6e57e55
[android] add route commands to toggle routes
doromaraujo Sep 26, 2025
51b664c
[android] add state file path to android client constructor and struct
doromaraujo Sep 26, 2025
880ec01
[client] add android check to use mobile dependency's StateFilePath
doromaraujo Sep 26, 2025
9016f5d
[client] pass state file to be set as mobile dependency to RunOnAndroid
doromaraujo Sep 26, 2025
53d706a
Revert changed todo message
doromaraujo Sep 27, 2025
41fc5aa
[android] Add peer routes to peers and network domains to networks
doromaraujo Sep 30, 2025
970e3de
[android] Add resolved IP addresses to network domains
doromaraujo Oct 1, 2025
0190059
Merge branch 'main' into feature/android-allow-selecting-routes
doromaraujo Oct 2, 2025
eed622d
[android] Add rudimentary implementation of RenewableTUN
doromaraujo Oct 7, 2025
369a560
[android] Add some nil checks to tun.Device implementations on Renewa…
doromaraujo Oct 7, 2025
e67cd45
[android] Add usage of RenewableTUN when renewing tun fd on android
doromaraujo Oct 7, 2025
040ed99
[android] Use mutex when peeking, dequeueing and adding to devices list
doromaraujo Oct 8, 2025
4e094f8
Merge branch 'netbirdio:main' into main
doromaraujo Oct 8, 2025
fb864e5
[test] Add missing RenewFd mock to MockWGIface used in engine_test
doromaraujo Oct 8, 2025
971eea3
[client] Add missing RenewTun implementation
doromaraujo Oct 8, 2025
546d022
[test] remove unused definition from MockWGIface struct
doromaraujo Oct 8, 2025
039c940
[client] add go:build android to new android files
doromaraujo Oct 8, 2025
90a7846
[client] sort imports, fix typo
doromaraujo Oct 9, 2025
4ff93da
[android] add PlatformFiles interface to group config and state file …
doromaraujo Oct 9, 2025
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
112 changes: 103 additions & 9 deletions client/android/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ package android

import (
"context"
"fmt"
"github.com/netbirdio/netbird/route"
"github.com/netbirdio/netbird/shared/management/domain"
"golang.org/x/exp/maps"
"os"
"slices"
"sync"
Expand All @@ -17,9 +21,9 @@ import (
"github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/client/internal/profilemanager"
"github.com/netbirdio/netbird/client/internal/stdnet"
"github.com/netbirdio/netbird/client/net"
"github.com/netbirdio/netbird/client/system"
"github.com/netbirdio/netbird/formatter"
"github.com/netbirdio/netbird/client/net"
)

// ConnectionListener export internal Listener for mobile
Expand Down Expand Up @@ -62,12 +66,13 @@ type Client struct {
deviceName string
uiVersion string
networkChangeListener listener.NetworkChangeListener
stateFile string

connectClient *internal.ConnectClient
}

// NewClient instantiate a new Client
func NewClient(cfgFile string, androidSDKVersion int, deviceName string, uiVersion string, tunAdapter TunAdapter, iFaceDiscover IFaceDiscover, networkChangeListener NetworkChangeListener) *Client {
func NewClient(cfgFile string, androidSDKVersion int, deviceName string, uiVersion string, tunAdapter TunAdapter, iFaceDiscover IFaceDiscover, networkChangeListener NetworkChangeListener, stateFile string) *Client {
execWorkaround(androidSDKVersion)

net.SetAndroidProtectSocketFn(tunAdapter.ProtectSocket)
Expand All @@ -80,6 +85,7 @@ func NewClient(cfgFile string, androidSDKVersion int, deviceName string, uiVersi
recorder: peer.NewRecorder(""),
ctxCancelLock: &sync.Mutex{},
networkChangeListener: networkChangeListener,
stateFile: stateFile,
}
}

Expand Down Expand Up @@ -115,7 +121,7 @@ func (c *Client) Run(urlOpener URLOpener, dns *DNSList, dnsReadyListener DnsRead
// todo do not throw error in case of cancelled context
ctx = internal.CtxInitState(ctx)
c.connectClient = internal.NewConnectClient(ctx, cfg, c.recorder)
return c.connectClient.RunOnAndroid(c.tunAdapter, c.iFaceDiscover, c.networkChangeListener, slices.Clone(dns.items), dnsReadyListener)
return c.connectClient.RunOnAndroid(c.tunAdapter, c.iFaceDiscover, c.networkChangeListener, slices.Clone(dns.items), dnsReadyListener, c.stateFile)
}

// RunWithoutLogin we apply this type of run function when the backed has been started without UI (i.e. after reboot).
Expand All @@ -142,7 +148,7 @@ func (c *Client) RunWithoutLogin(dns *DNSList, dnsReadyListener DnsReadyListener
// todo do not throw error in case of cancelled context
ctx = internal.CtxInitState(ctx)
c.connectClient = internal.NewConnectClient(ctx, cfg, c.recorder)
return c.connectClient.RunOnAndroid(c.tunAdapter, c.iFaceDiscover, c.networkChangeListener, slices.Clone(dns.items), dnsReadyListener)
return c.connectClient.RunOnAndroid(c.tunAdapter, c.iFaceDiscover, c.networkChangeListener, slices.Clone(dns.items), dnsReadyListener, c.stateFile)
}

// Stop the internal client and free the resources
Expand All @@ -156,6 +162,19 @@ func (c *Client) Stop() {
c.ctxCancel()
}

func (c *Client) RenewTun(fd int) error {
if c.connectClient == nil {
return fmt.Errorf("engine not running")
}

e := c.connectClient.Engine()
if e == nil {
return fmt.Errorf("engine not initialized")
}

return e.RenewTun(fd)
}

// SetTraceLogLevel configure the logger to trace level
func (c *Client) SetTraceLogLevel() {
log.SetLevel(log.TraceLevel)
Expand All @@ -177,6 +196,7 @@ func (c *Client) PeersList() *PeerInfoArray {
p.IP,
p.FQDN,
p.ConnStatus.String(),
PeerRoutes{routes: maps.Keys(p.GetRoutes())},
}
peerInfos[n] = pi
}
Expand All @@ -201,31 +221,43 @@ func (c *Client) Networks() *NetworkArray {
return nil
}

routeSelector := routeManager.GetRouteSelector()
if routeSelector == nil {
log.Error("could not get route selector")
return nil
}

networkArray := &NetworkArray{
items: make([]Network, 0),
}

resolvedDomains := c.recorder.GetResolvedDomainsStates()

for id, routes := range routeManager.GetClientRoutesWithNetID() {
if len(routes) == 0 {
continue
}

r := routes[0]
domains := c.getNetworkDomainsFromRoute(r, resolvedDomains)
netStr := r.Network.String()

if r.IsDynamic() {
netStr = r.Domains.SafeString()
}

peer, err := c.recorder.GetPeer(routes[0].Peer)
routePeer, err := c.recorder.GetPeer(routes[0].Peer)
if err != nil {
log.Errorf("could not get peer info for %s: %v", routes[0].Peer, err)
continue
}
network := Network{
Name: string(id),
Network: netStr,
Peer: peer.FQDN,
Status: peer.ConnStatus.String(),
Name: string(id),
Network: netStr,
Peer: routePeer.FQDN,
Status: routePeer.ConnStatus.String(),
IsSelected: routeSelector.IsSelected(id),
Domains: domains,
}
networkArray.Add(network)
}
Expand Down Expand Up @@ -253,6 +285,68 @@ func (c *Client) RemoveConnectionListener() {
c.recorder.RemoveConnectionListener()
}

func (c *Client) toggleRoute(command routeCommand) error {
return command.toggleRoute()
}

func (c *Client) SelectRoute(route string) error {
client := c.connectClient
if client == nil {
return fmt.Errorf("not connected")
}

engine := client.Engine()
if engine == nil {
return fmt.Errorf("engine is not running")
}

manager := engine.GetRouteManager()
if manager == nil {
return fmt.Errorf("could not get route manager")
}

return c.toggleRoute(selectRouteCommand{route: route, manager: manager})
}

func (c *Client) DeselectRoute(route string) error {
client := c.connectClient
if client == nil {
return fmt.Errorf("not connected")
}

engine := client.Engine()
if engine == nil {
return fmt.Errorf("engine is not running")
}

manager := engine.GetRouteManager()
if manager == nil {
return fmt.Errorf("could not get route manager")
}

return c.toggleRoute(deselectRouteCommand{route: route, manager: manager})
}

func (c *Client) getNetworkDomainsFromRoute(route *route.Route, resolvedDomains map[domain.Domain]peer.ResolvedDomainInfo) NetworkDomains {
domains := NetworkDomains{}

for _, d := range route.Domains {
networkDomain := NetworkDomain{
Address: d.SafeString(),
}

if info, exists := resolvedDomains[d]; exists {
for _, prefix := range info.Prefixes {
networkDomain.addResolvedIP(prefix.Addr().String())
}
}

domains.Add(networkDomain)
}

return domains
}

func exportEnvList(list *EnvList) {
if list == nil {
return
Expand Down
56 changes: 56 additions & 0 deletions client/android/network_domains.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//go:build android

package android

import "fmt"

type ResolvedIPs struct {
resolvedIPs []string
}

func (r *ResolvedIPs) Add(ipAddress string) {
r.resolvedIPs = append(r.resolvedIPs, ipAddress)
}

func (r *ResolvedIPs) Get(i int) (string, error) {
if i < 0 || i >= len(r.resolvedIPs) {
return "", fmt.Errorf("%d is out of range", i)
}
return r.resolvedIPs[i], nil
}

func (r *ResolvedIPs) Size() int {
return len(r.resolvedIPs)
}

type NetworkDomain struct {
Address string
resolvedIPs ResolvedIPs
}

func (d *NetworkDomain) addResolvedIP(resolvedIP string) {
d.resolvedIPs.Add(resolvedIP)
}

func (d *NetworkDomain) GetResolvedIPs() *ResolvedIPs {
return &d.resolvedIPs
}

type NetworkDomains struct {
domains []NetworkDomain
}

func (n *NetworkDomains) Add(domain NetworkDomain) {
n.domains = append(n.domains, domain)
}

func (n *NetworkDomains) Get(i int) (*NetworkDomain, error) {
if i < 0 || i >= len(n.domains) {
return nil, fmt.Errorf("%d is out of range", i)
}
return &n.domains[i], nil
}

func (n *NetworkDomains) Size() int {
return len(n.domains)
}
14 changes: 10 additions & 4 deletions client/android/networks.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,16 @@
package android

type Network struct {
Name string
Network string
Peer string
Status string
Name string
Network string
Peer string
Status string
IsSelected bool
Domains NetworkDomains
}

func (n Network) GetNetworkDomains() *NetworkDomains {
return &n.Domains
}

type NetworkArray struct {
Expand Down
7 changes: 7 additions & 0 deletions client/android/peer_notifier.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
//go:build android

package android

// PeerInfo describe information about the peers. It designed for the UI usage
type PeerInfo struct {
IP string
FQDN string
ConnStatus string // Todo replace to enum
Routes PeerRoutes
}

func (p *PeerInfo) GetPeerRoutes() *PeerRoutes {
return &p.Routes
}

// PeerInfoArray is a wrapper of []PeerInfo
Expand Down
20 changes: 20 additions & 0 deletions client/android/peer_routes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//go:build android

package android

import "fmt"

type PeerRoutes struct {
routes []string
}

func (p *PeerRoutes) Get(i int) (string, error) {
if i < 0 || i >= len(p.routes) {
return "", fmt.Errorf("%d is out of range", i)
}
return p.routes[i], nil
}

func (p *PeerRoutes) Size() int {
return len(p.routes)
}
65 changes: 65 additions & 0 deletions client/android/route_command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//go:build android

package android

import (
"fmt"
"github.com/netbirdio/netbird/client/internal/routemanager"
"github.com/netbirdio/netbird/route"
log "github.com/sirupsen/logrus"
"golang.org/x/exp/maps"
)

func toggleRoute(id string, manager routemanager.Manager,
operationName string,
routeOperation func(routes []route.NetID, allRoutes []route.NetID) error) error {
netID := route.NetID(id)
routes := []route.NetID{netID}

log.Debugf("%s with id: %s", operationName, id)

if err := routeOperation(routes, maps.Keys(manager.GetClientRoutesWithNetID())); err != nil {
log.Debugf("error when %s: %s", operationName, err)
return fmt.Errorf("error %s: %w", operationName, err)
}

manager.TriggerSelection(manager.GetClientRoutes())

return nil
}

type routeCommand interface {
toggleRoute() error
}

type selectRouteCommand struct {
route string
manager routemanager.Manager
}

func (s selectRouteCommand) toggleRoute() error {
routeSelector := s.manager.GetRouteSelector()
if routeSelector == nil {
return fmt.Errorf("no route selector available")
}

routeOperation := func(routes []route.NetID, allRoutes []route.NetID) error {
return routeSelector.SelectRoutes(routes, true, allRoutes)
}

return toggleRoute(s.route, s.manager, "selecting route", routeOperation)
}

type deselectRouteCommand struct {
route string
manager routemanager.Manager
}

func (d deselectRouteCommand) toggleRoute() error {
routeSelector := d.manager.GetRouteSelector()
if routeSelector == nil {
return fmt.Errorf("no route selector available")
}

return toggleRoute(d.route, d.manager, "deselecting route", routeSelector.DeselectRoutes)
}
Loading
Loading