mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-16 07:16:38 +00:00
Use a 2-minute context timeout instead of context.Background() to prevent the upload from blocking indefinitely.
450 lines
12 KiB
Go
450 lines
12 KiB
Go
//go:build android
|
|
|
|
package android
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"slices"
|
|
"sync"
|
|
"time"
|
|
|
|
"golang.org/x/exp/maps"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
"github.com/netbirdio/netbird/client/iface/device"
|
|
"github.com/netbirdio/netbird/client/internal"
|
|
"github.com/netbirdio/netbird/client/internal/debug"
|
|
"github.com/netbirdio/netbird/client/internal/dns"
|
|
"github.com/netbirdio/netbird/client/internal/listener"
|
|
"github.com/netbirdio/netbird/client/internal/peer"
|
|
"github.com/netbirdio/netbird/client/internal/profilemanager"
|
|
"github.com/netbirdio/netbird/client/internal/routemanager"
|
|
"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/route"
|
|
"github.com/netbirdio/netbird/shared/management/domain"
|
|
types "github.com/netbirdio/netbird/upload-server/types"
|
|
)
|
|
|
|
// ConnectionListener export internal Listener for mobile
|
|
type ConnectionListener interface {
|
|
peer.Listener
|
|
}
|
|
|
|
// TunAdapter export internal TunAdapter for mobile
|
|
type TunAdapter interface {
|
|
device.TunAdapter
|
|
}
|
|
|
|
// IFaceDiscover export internal IFaceDiscover for mobile
|
|
type IFaceDiscover interface {
|
|
stdnet.ExternalIFaceDiscover
|
|
}
|
|
|
|
// NetworkChangeListener export internal NetworkChangeListener for mobile
|
|
type NetworkChangeListener interface {
|
|
listener.NetworkChangeListener
|
|
}
|
|
|
|
// DnsReadyListener export internal dns ReadyListener for mobile
|
|
type DnsReadyListener interface {
|
|
dns.ReadyListener
|
|
}
|
|
|
|
func init() {
|
|
formatter.SetLogcatFormatter(log.StandardLogger())
|
|
}
|
|
|
|
// Client struct manage the life circle of background service
|
|
type Client struct {
|
|
tunAdapter device.TunAdapter
|
|
iFaceDiscover IFaceDiscover
|
|
recorder *peer.Status
|
|
ctxCancel context.CancelFunc
|
|
ctxCancelLock *sync.Mutex
|
|
deviceName string
|
|
uiVersion string
|
|
networkChangeListener listener.NetworkChangeListener
|
|
|
|
connectClient *internal.ConnectClient
|
|
config *profilemanager.Config
|
|
cacheDir string
|
|
}
|
|
|
|
// NewClient instantiate a new Client
|
|
func NewClient(androidSDKVersion int, deviceName string, uiVersion string, tunAdapter TunAdapter, iFaceDiscover IFaceDiscover, networkChangeListener NetworkChangeListener) *Client {
|
|
execWorkaround(androidSDKVersion)
|
|
|
|
net.SetAndroidProtectSocketFn(tunAdapter.ProtectSocket)
|
|
return &Client{
|
|
deviceName: deviceName,
|
|
uiVersion: uiVersion,
|
|
tunAdapter: tunAdapter,
|
|
iFaceDiscover: iFaceDiscover,
|
|
recorder: peer.NewRecorder(""),
|
|
ctxCancelLock: &sync.Mutex{},
|
|
networkChangeListener: networkChangeListener,
|
|
}
|
|
}
|
|
|
|
// Run start the internal client. It is a blocker function
|
|
func (c *Client) Run(platformFiles PlatformFiles, urlOpener URLOpener, isAndroidTV bool, dns *DNSList, dnsReadyListener DnsReadyListener, envList *EnvList) error {
|
|
exportEnvList(envList)
|
|
|
|
cfgFile := platformFiles.ConfigurationFilePath()
|
|
stateFile := platformFiles.StateFilePath()
|
|
cacheDir := platformFiles.CacheDir()
|
|
|
|
log.Infof("Starting client with config: %s, state: %s", cfgFile, stateFile)
|
|
|
|
cfg, err := profilemanager.UpdateOrCreateConfig(profilemanager.ConfigInput{
|
|
ConfigPath: cfgFile,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.recorder.UpdateManagementAddress(cfg.ManagementURL.String())
|
|
c.recorder.UpdateRosenpass(cfg.RosenpassEnabled, cfg.RosenpassPermissive)
|
|
|
|
var ctx context.Context
|
|
//nolint
|
|
ctxWithValues := context.WithValue(context.Background(), system.DeviceNameCtxKey, c.deviceName)
|
|
//nolint
|
|
ctxWithValues = context.WithValue(ctxWithValues, system.UiVersionCtxKey, c.uiVersion)
|
|
|
|
c.ctxCancelLock.Lock()
|
|
ctx, c.ctxCancel = context.WithCancel(ctxWithValues)
|
|
defer c.ctxCancel()
|
|
c.ctxCancelLock.Unlock()
|
|
|
|
auth := NewAuthWithConfig(ctx, cfg)
|
|
err = auth.login(urlOpener, isAndroidTV)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// todo do not throw error in case of cancelled context
|
|
ctx = internal.CtxInitState(ctx)
|
|
c.config = cfg
|
|
c.cacheDir = cacheDir
|
|
c.connectClient = internal.NewConnectClient(ctx, cfg, c.recorder)
|
|
return c.connectClient.RunOnAndroid(c.tunAdapter, c.iFaceDiscover, c.networkChangeListener, slices.Clone(dns.items), dnsReadyListener, stateFile, cacheDir)
|
|
}
|
|
|
|
// RunWithoutLogin we apply this type of run function when the backed has been started without UI (i.e. after reboot).
|
|
// In this case make no sense handle registration steps.
|
|
func (c *Client) RunWithoutLogin(platformFiles PlatformFiles, dns *DNSList, dnsReadyListener DnsReadyListener, envList *EnvList) error {
|
|
exportEnvList(envList)
|
|
|
|
cfgFile := platformFiles.ConfigurationFilePath()
|
|
stateFile := platformFiles.StateFilePath()
|
|
cacheDir := platformFiles.CacheDir()
|
|
|
|
log.Infof("Starting client without login with config: %s, state: %s", cfgFile, stateFile)
|
|
|
|
cfg, err := profilemanager.UpdateOrCreateConfig(profilemanager.ConfigInput{
|
|
ConfigPath: cfgFile,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.recorder.UpdateManagementAddress(cfg.ManagementURL.String())
|
|
c.recorder.UpdateRosenpass(cfg.RosenpassEnabled, cfg.RosenpassPermissive)
|
|
|
|
var ctx context.Context
|
|
//nolint
|
|
ctxWithValues := context.WithValue(context.Background(), system.DeviceNameCtxKey, c.deviceName)
|
|
c.ctxCancelLock.Lock()
|
|
ctx, c.ctxCancel = context.WithCancel(ctxWithValues)
|
|
defer c.ctxCancel()
|
|
c.ctxCancelLock.Unlock()
|
|
|
|
// todo do not throw error in case of cancelled context
|
|
ctx = internal.CtxInitState(ctx)
|
|
c.config = cfg
|
|
c.cacheDir = cacheDir
|
|
c.connectClient = internal.NewConnectClient(ctx, cfg, c.recorder)
|
|
return c.connectClient.RunOnAndroid(c.tunAdapter, c.iFaceDiscover, c.networkChangeListener, slices.Clone(dns.items), dnsReadyListener, stateFile, cacheDir)
|
|
}
|
|
|
|
// Stop the internal client and free the resources
|
|
func (c *Client) Stop() {
|
|
c.ctxCancelLock.Lock()
|
|
defer c.ctxCancelLock.Unlock()
|
|
if c.ctxCancel == nil {
|
|
return
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
// DebugBundle generates a debug bundle, uploads it, and returns the upload key.
|
|
// It works both with and without a running engine.
|
|
func (c *Client) DebugBundle(platformFiles PlatformFiles, anonymize bool) (string, error) {
|
|
cfg := c.config
|
|
cacheDir := c.cacheDir
|
|
|
|
// If the engine hasn't been started, load config from disk
|
|
if cfg == nil {
|
|
var err error
|
|
cfg, err = profilemanager.UpdateOrCreateConfig(profilemanager.ConfigInput{
|
|
ConfigPath: platformFiles.ConfigurationFilePath(),
|
|
})
|
|
if err != nil {
|
|
return "", fmt.Errorf("load config: %w", err)
|
|
}
|
|
cacheDir = platformFiles.CacheDir()
|
|
}
|
|
|
|
deps := debug.GeneratorDependencies{
|
|
InternalConfig: cfg,
|
|
StatusRecorder: c.recorder,
|
|
TempDir: cacheDir,
|
|
}
|
|
|
|
if c.connectClient != nil {
|
|
resp, err := c.connectClient.GetLatestSyncResponse()
|
|
if err != nil {
|
|
log.Warnf("get latest sync response: %v", err)
|
|
}
|
|
deps.SyncResponse = resp
|
|
|
|
if e := c.connectClient.Engine(); e != nil {
|
|
if cm := e.GetClientMetrics(); cm != nil {
|
|
deps.ClientMetrics = cm
|
|
}
|
|
}
|
|
}
|
|
|
|
bundleGenerator := debug.NewBundleGenerator(
|
|
deps,
|
|
debug.BundleConfig{
|
|
Anonymize: anonymize,
|
|
IncludeSystemInfo: true,
|
|
},
|
|
)
|
|
|
|
path, err := bundleGenerator.Generate()
|
|
if err != nil {
|
|
return "", fmt.Errorf("generate debug bundle: %w", err)
|
|
}
|
|
defer func() {
|
|
if err := os.Remove(path); err != nil {
|
|
log.Errorf("failed to remove debug bundle file: %v", err)
|
|
}
|
|
}()
|
|
|
|
uploadCtx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
|
defer cancel()
|
|
|
|
key, err := debug.UploadDebugBundle(uploadCtx, types.DefaultBundleURL, cfg.ManagementURL.String(), path)
|
|
if err != nil {
|
|
return "", fmt.Errorf("upload debug bundle: %w", err)
|
|
}
|
|
|
|
log.Infof("debug bundle uploaded with key %s", key)
|
|
return key, nil
|
|
}
|
|
|
|
// SetTraceLogLevel configure the logger to trace level
|
|
func (c *Client) SetTraceLogLevel() {
|
|
log.SetLevel(log.TraceLevel)
|
|
}
|
|
|
|
// SetInfoLogLevel configure the logger to info level
|
|
func (c *Client) SetInfoLogLevel() {
|
|
log.SetLevel(log.InfoLevel)
|
|
}
|
|
|
|
// PeersList return with the list of the PeerInfos
|
|
func (c *Client) PeersList() *PeerInfoArray {
|
|
|
|
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())},
|
|
}
|
|
peerInfos[n] = pi
|
|
}
|
|
return &PeerInfoArray{items: peerInfos}
|
|
}
|
|
|
|
func (c *Client) Networks() *NetworkArray {
|
|
if c.connectClient == nil {
|
|
log.Error("not connected")
|
|
return nil
|
|
}
|
|
|
|
engine := c.connectClient.Engine()
|
|
if engine == nil {
|
|
log.Error("could not get engine")
|
|
return nil
|
|
}
|
|
|
|
routeManager := engine.GetRouteManager()
|
|
if routeManager == nil {
|
|
log.Error("could not get route manager")
|
|
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()
|
|
}
|
|
|
|
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: routePeer.FQDN,
|
|
Status: routePeer.ConnStatus.String(),
|
|
IsSelected: routeSelector.IsSelected(id),
|
|
Domains: domains,
|
|
}
|
|
networkArray.Add(network)
|
|
}
|
|
return networkArray
|
|
}
|
|
|
|
// OnUpdatedHostDNS update the DNS servers addresses for root zones
|
|
func (c *Client) OnUpdatedHostDNS(list *DNSList) error {
|
|
dnsServer, err := dns.GetServerDns()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
dnsServer.OnUpdatedHostDNSServer(slices.Clone(list.items))
|
|
return nil
|
|
}
|
|
|
|
// SetConnectionListener set the network connection listener
|
|
func (c *Client) SetConnectionListener(listener ConnectionListener) {
|
|
c.recorder.SetConnectionListener(listener)
|
|
}
|
|
|
|
// RemoveConnectionListener remove connection listener
|
|
func (c *Client) RemoveConnectionListener() {
|
|
c.recorder.RemoveConnectionListener()
|
|
}
|
|
|
|
func (c *Client) toggleRoute(command routeCommand) error {
|
|
return command.toggleRoute()
|
|
}
|
|
|
|
func (c *Client) getRouteManager() (routemanager.Manager, error) {
|
|
client := c.connectClient
|
|
if client == nil {
|
|
return nil, fmt.Errorf("not connected")
|
|
}
|
|
|
|
engine := client.Engine()
|
|
if engine == nil {
|
|
return nil, fmt.Errorf("engine is not running")
|
|
}
|
|
|
|
manager := engine.GetRouteManager()
|
|
if manager == nil {
|
|
return nil, fmt.Errorf("could not get route manager")
|
|
}
|
|
|
|
return manager, nil
|
|
}
|
|
|
|
func (c *Client) SelectRoute(route string) error {
|
|
manager, err := c.getRouteManager()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return c.toggleRoute(selectRouteCommand{route: route, manager: manager})
|
|
}
|
|
|
|
func (c *Client) DeselectRoute(route string) error {
|
|
manager, err := c.getRouteManager()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return c.toggleRoute(deselectRouteCommand{route: route, manager: manager})
|
|
}
|
|
|
|
// getNetworkDomainsFromRoute extracts domains from a route and enriches each domain
|
|
// with its resolved IP addresses from the provided resolvedDomains map.
|
|
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
|
|
}
|
|
for k, v := range list.AllItems() {
|
|
if err := os.Setenv(k, v); err != nil {
|
|
log.Errorf("could not set env variable %s: %v", k, err)
|
|
}
|
|
}
|
|
}
|