diff --git a/client/android/client.go b/client/android/client.go index bb15268eb61..f2dd9e3f7a4 100644 --- a/client/android/client.go +++ b/client/android/client.go @@ -8,8 +8,8 @@ import ( "github.com/netbirdio/netbird/client/internal" "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/routemanager" "github.com/netbirdio/netbird/client/internal/stdnet" "github.com/netbirdio/netbird/client/system" "github.com/netbirdio/netbird/formatter" @@ -31,9 +31,9 @@ type IFaceDiscover interface { stdnet.ExternalIFaceDiscover } -// RouteListener export internal RouteListener for mobile -type RouteListener interface { - routemanager.RouteListener +// NetworkChangeListener export internal NetworkChangeListener for mobile +type NetworkChangeListener interface { + listener.NetworkChangeListener } // DnsReadyListener export internal dns ReadyListener for mobile @@ -47,26 +47,26 @@ func init() { // Client struct manage the life circle of background service type Client struct { - cfgFile string - tunAdapter iface.TunAdapter - iFaceDiscover IFaceDiscover - recorder *peer.Status - ctxCancel context.CancelFunc - ctxCancelLock *sync.Mutex - deviceName string - routeListener routemanager.RouteListener + cfgFile string + tunAdapter iface.TunAdapter + iFaceDiscover IFaceDiscover + recorder *peer.Status + ctxCancel context.CancelFunc + ctxCancelLock *sync.Mutex + deviceName string + networkChangeListener listener.NetworkChangeListener } // NewClient instantiate a new Client -func NewClient(cfgFile, deviceName string, tunAdapter TunAdapter, iFaceDiscover IFaceDiscover, routeListener RouteListener) *Client { +func NewClient(cfgFile, deviceName string, tunAdapter TunAdapter, iFaceDiscover IFaceDiscover, networkChangeListener NetworkChangeListener) *Client { return &Client{ - cfgFile: cfgFile, - deviceName: deviceName, - tunAdapter: tunAdapter, - iFaceDiscover: iFaceDiscover, - recorder: peer.NewRecorder(""), - ctxCancelLock: &sync.Mutex{}, - routeListener: routeListener, + cfgFile: cfgFile, + deviceName: deviceName, + tunAdapter: tunAdapter, + iFaceDiscover: iFaceDiscover, + recorder: peer.NewRecorder(""), + ctxCancelLock: &sync.Mutex{}, + networkChangeListener: networkChangeListener, } } @@ -96,7 +96,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) - return internal.RunClientMobile(ctx, cfg, c.recorder, c.tunAdapter, c.iFaceDiscover, c.routeListener, dns.items, dnsReadyListener) + return internal.RunClientMobile(ctx, cfg, c.recorder, c.tunAdapter, c.iFaceDiscover, c.networkChangeListener, dns.items, dnsReadyListener) } // RunWithoutLogin we apply this type of run function when the backed has been started without UI (i.e. after reboot). @@ -120,7 +120,7 @@ func (c *Client) RunWithoutLogin(dns *DNSList, dnsReadyListener DnsReadyListener // todo do not throw error in case of cancelled context ctx = internal.CtxInitState(ctx) - return internal.RunClientMobile(ctx, cfg, c.recorder, c.tunAdapter, c.iFaceDiscover, c.routeListener, dns.items, dnsReadyListener) + return internal.RunClientMobile(ctx, cfg, c.recorder, c.tunAdapter, c.iFaceDiscover, c.networkChangeListener, dns.items, dnsReadyListener) } // Stop the internal client and free the resources diff --git a/client/internal/connect.go b/client/internal/connect.go index 97fe2cc59e9..79f97e87f3f 100644 --- a/client/internal/connect.go +++ b/client/internal/connect.go @@ -13,8 +13,8 @@ import ( gstatus "google.golang.org/grpc/status" "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/routemanager" "github.com/netbirdio/netbird/client/internal/stdnet" "github.com/netbirdio/netbird/client/ssh" "github.com/netbirdio/netbird/client/system" @@ -31,14 +31,14 @@ func RunClient(ctx context.Context, config *Config, statusRecorder *peer.Status) } // RunClientMobile with main logic on mobile system -func RunClientMobile(ctx context.Context, config *Config, statusRecorder *peer.Status, tunAdapter iface.TunAdapter, iFaceDiscover stdnet.ExternalIFaceDiscover, routeListener routemanager.RouteListener, dnsAddresses []string, dnsReadyListener dns.ReadyListener) error { +func RunClientMobile(ctx context.Context, config *Config, statusRecorder *peer.Status, tunAdapter iface.TunAdapter, iFaceDiscover stdnet.ExternalIFaceDiscover, networkChangeListener listener.NetworkChangeListener, dnsAddresses []string, dnsReadyListener dns.ReadyListener) error { // in case of non Android os these variables will be nil mobileDependency := MobileDependency{ - TunAdapter: tunAdapter, - IFaceDiscover: iFaceDiscover, - RouteListener: routeListener, - HostDNSAddresses: dnsAddresses, - DnsReadyListener: dnsReadyListener, + TunAdapter: tunAdapter, + IFaceDiscover: iFaceDiscover, + NetworkChangeListener: networkChangeListener, + HostDNSAddresses: dnsAddresses, + DnsReadyListener: dnsReadyListener, } return runClient(ctx, config, statusRecorder, mobileDependency) } diff --git a/client/internal/dns/mockServer.go b/client/internal/dns/mockServer.go index 8970eec6e81..3534fc0c3fd 100644 --- a/client/internal/dns/mockServer.go +++ b/client/internal/dns/mockServer.go @@ -2,6 +2,7 @@ package dns import ( "fmt" + nbdns "github.com/netbirdio/netbird/dns" ) @@ -43,3 +44,7 @@ func (m *MockServer) UpdateDNSServer(serial uint64, update nbdns.Config) error { } return fmt.Errorf("method UpdateDNSServer is not implemented") } + +func (m *MockServer) SearchDomains() []string { + return make([]string, 0) +} diff --git a/client/internal/dns/notifier.go b/client/internal/dns/notifier.go new file mode 100644 index 00000000000..85c270e5859 --- /dev/null +++ b/client/internal/dns/notifier.go @@ -0,0 +1,57 @@ +package dns + +import ( + "reflect" + "sort" + "sync" + + "github.com/netbirdio/netbird/client/internal/listener" +) + +type notifier struct { + listener listener.NetworkChangeListener + listenerMux sync.Mutex + searchDomains []string +} + +func newNotifier(initialSearchDomains []string) *notifier { + sort.Strings(initialSearchDomains) + return ¬ifier{ + searchDomains: initialSearchDomains, + } +} + +func (n *notifier) setListener(listener listener.NetworkChangeListener) { + n.listenerMux.Lock() + defer n.listenerMux.Unlock() + n.listener = listener +} + +func (n *notifier) onNewSearchDomains(searchDomains []string) { + sort.Strings(searchDomains) + + if len(n.searchDomains) != len(searchDomains) { + n.searchDomains = searchDomains + n.notify() + return + } + + if reflect.DeepEqual(n.searchDomains, searchDomains) { + return + } + + n.searchDomains = searchDomains + n.notify() +} + +func (n *notifier) notify() { + n.listenerMux.Lock() + defer n.listenerMux.Unlock() + if n.listener == nil { + return + } + + go func(l listener.NetworkChangeListener) { + l.OnNetworkChanged() + }(n.listener) +} diff --git a/client/internal/dns/server.go b/client/internal/dns/server.go index 600b16a6e84..6655a6e4e1d 100644 --- a/client/internal/dns/server.go +++ b/client/internal/dns/server.go @@ -10,6 +10,7 @@ import ( "github.com/mitchellh/hashstructure/v2" log "github.com/sirupsen/logrus" + "github.com/netbirdio/netbird/client/internal/listener" nbdns "github.com/netbirdio/netbird/dns" ) @@ -25,6 +26,7 @@ type Server interface { DnsIP() string UpdateDNSServer(serial uint64, update nbdns.Config) error OnUpdatedHostDNSServer(strings []string) + SearchDomains() []string } type registeredHandlerMap map[string]handlerWithStop @@ -47,6 +49,9 @@ type DefaultServer struct { permanent bool hostsDnsList []string hostsDnsListLock sync.Mutex + + // make sense on mobile only + searchDomainNotifier *notifier } type handlerWithStop interface { @@ -81,12 +86,15 @@ func NewDefaultServer(ctx context.Context, wgInterface WGIface, customAddress st } // NewDefaultServerPermanentUpstream returns a new dns server. It optimized for mobile systems -func NewDefaultServerPermanentUpstream(ctx context.Context, wgInterface WGIface, hostsDnsList []string) *DefaultServer { +func NewDefaultServerPermanentUpstream(ctx context.Context, wgInterface WGIface, hostsDnsList []string, config nbdns.Config, listener listener.NetworkChangeListener) *DefaultServer { log.Debugf("host dns address list is: %v", hostsDnsList) ds := newDefaultServer(ctx, wgInterface, newServiceViaMemory(wgInterface)) ds.permanent = true ds.hostsDnsList = hostsDnsList ds.addHostRootZone() + ds.currentConfig = dnsConfigToHostDNSConfig(config, ds.service.RuntimeIP(), ds.service.RuntimePort()) + ds.searchDomainNotifier = newNotifier(ds.SearchDomains()) + ds.searchDomainNotifier.setListener(listener) setServerDns(ds) return ds } @@ -212,6 +220,21 @@ func (s *DefaultServer) UpdateDNSServer(serial uint64, update nbdns.Config) erro } } +func (s *DefaultServer) SearchDomains() []string { + var searchDomains []string + + for _, dConf := range s.currentConfig.domains { + if dConf.disabled { + continue + } + if dConf.matchOnly { + continue + } + searchDomains = append(searchDomains, dConf.domain) + } + return searchDomains +} + func (s *DefaultServer) applyConfiguration(update nbdns.Config) error { // is the service should be disabled, we stop the listener or fake resolver // and proceed with a regular update to clean up the handlers and records @@ -246,6 +269,10 @@ func (s *DefaultServer) applyConfiguration(update nbdns.Config) error { log.Error(err) } + if s.searchDomainNotifier != nil { + s.searchDomainNotifier.onNewSearchDomains(s.SearchDomains()) + } + return nil } diff --git a/client/internal/dns/server_test.go b/client/internal/dns/server_test.go index 119ac684c95..cd7932d07fa 100644 --- a/client/internal/dns/server_test.go +++ b/client/internal/dns/server_test.go @@ -593,7 +593,8 @@ func TestDNSPermanent_updateHostDNS_emptyUpstream(t *testing.T) { defer wgIFace.Close() var dnsList []string - dnsServer := NewDefaultServerPermanentUpstream(context.Background(), wgIFace, dnsList) + dnsConfig := nbdns.Config{} + dnsServer := NewDefaultServerPermanentUpstream(context.Background(), wgIFace, dnsList, dnsConfig, nil) err = dnsServer.Initialize() if err != nil { t.Errorf("failed to initialize DNS server: %v", err) @@ -616,8 +617,8 @@ func TestDNSPermanent_updateUpstream(t *testing.T) { t.Fatal("failed to initialize wg interface") } defer wgIFace.Close() - - dnsServer := NewDefaultServerPermanentUpstream(context.Background(), wgIFace, []string{"8.8.8.8"}) + dnsConfig := nbdns.Config{} + dnsServer := NewDefaultServerPermanentUpstream(context.Background(), wgIFace, []string{"8.8.8.8"}, dnsConfig, nil) err = dnsServer.Initialize() if err != nil { t.Errorf("failed to initialize DNS server: %v", err) @@ -708,8 +709,8 @@ func TestDNSPermanent_matchOnly(t *testing.T) { t.Fatal("failed to initialize wg interface") } defer wgIFace.Close() - - dnsServer := NewDefaultServerPermanentUpstream(context.Background(), wgIFace, []string{"8.8.8.8"}) + dnsConfig := nbdns.Config{} + dnsServer := NewDefaultServerPermanentUpstream(context.Background(), wgIFace, []string{"8.8.8.8"}, dnsConfig, nil) err = dnsServer.Initialize() if err != nil { t.Errorf("failed to initialize DNS server: %v", err) diff --git a/client/internal/engine.go b/client/internal/engine.go index aeeddc37213..a81fabde6eb 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -195,12 +195,13 @@ func (e *Engine) Start() error { var routes []*route.Route if runtime.GOOS == "android" { - routes, err = e.readInitialSettings() + var dnsConfig *nbdns.Config + routes, dnsConfig, err = e.readInitialSettings() if err != nil { return err } if e.dnsServer == nil { - e.dnsServer = dns.NewDefaultServerPermanentUpstream(e.ctx, e.wgInterface, e.mobileDep.HostDNSAddresses) + e.dnsServer = dns.NewDefaultServerPermanentUpstream(e.ctx, e.wgInterface, e.mobileDep.HostDNSAddresses, *dnsConfig, e.mobileDep.NetworkChangeListener) go e.mobileDep.DnsReadyListener.OnReady() } } else { @@ -215,15 +216,16 @@ func (e *Engine) Start() error { } e.routeManager = routemanager.NewManager(e.ctx, e.config.WgPrivateKey.PublicKey().String(), e.wgInterface, e.statusRecorder, routes) - e.routeManager.SetRouteChangeListener(e.mobileDep.RouteListener) + e.routeManager.SetRouteChangeListener(e.mobileDep.NetworkChangeListener) - if runtime.GOOS != "android" { - err = e.wgInterface.Create() - } else { + if runtime.GOOS == "android" { err = e.wgInterface.CreateOnMobile(iface.MobileIFaceArguments{ - Routes: e.routeManager.InitialRouteRange(), - Dns: e.dnsServer.DnsIP(), + Routes: e.routeManager.InitialRouteRange(), + Dns: e.dnsServer.DnsIP(), + SearchDomains: e.dnsServer.SearchDomains(), }) + } else { + err = e.wgInterface.Create() } if err != nil { log.Errorf("failed creating tunnel interface %s: [%s]", wgIFaceName, err.Error()) @@ -1051,13 +1053,14 @@ func (e *Engine) close() { } } -func (e *Engine) readInitialSettings() ([]*route.Route, error) { +func (e *Engine) readInitialSettings() ([]*route.Route, *nbdns.Config, error) { netMap, err := e.mgmClient.GetNetworkMap() if err != nil { - return nil, err + return nil, nil, err } routes := toRoutes(netMap.GetRoutes()) - return routes, nil + dnsCfg := toDNSConfig(netMap.GetDNSConfig()) + return routes, &dnsCfg, nil } func findIPFromInterfaceName(ifaceName string) (net.IP, error) { diff --git a/client/internal/listener/network_change.go b/client/internal/listener/network_change.go new file mode 100644 index 00000000000..ff9cb11f5a3 --- /dev/null +++ b/client/internal/listener/network_change.go @@ -0,0 +1,7 @@ +package listener + +// NetworkChangeListener is a callback interface for mobile system +type NetworkChangeListener interface { + // OnNetworkChanged invoke when network settings has been changed + OnNetworkChanged() +} diff --git a/client/internal/mobile_dependency.go b/client/internal/mobile_dependency.go index fc8fa61ce2a..a2bbf247366 100644 --- a/client/internal/mobile_dependency.go +++ b/client/internal/mobile_dependency.go @@ -2,16 +2,16 @@ package internal import ( "github.com/netbirdio/netbird/client/internal/dns" - "github.com/netbirdio/netbird/client/internal/routemanager" + "github.com/netbirdio/netbird/client/internal/listener" "github.com/netbirdio/netbird/client/internal/stdnet" "github.com/netbirdio/netbird/iface" ) // MobileDependency collect all dependencies for mobile platform type MobileDependency struct { - TunAdapter iface.TunAdapter - IFaceDiscover stdnet.ExternalIFaceDiscover - RouteListener routemanager.RouteListener - HostDNSAddresses []string - DnsReadyListener dns.ReadyListener + TunAdapter iface.TunAdapter + IFaceDiscover stdnet.ExternalIFaceDiscover + NetworkChangeListener listener.NetworkChangeListener + HostDNSAddresses []string + DnsReadyListener dns.ReadyListener } diff --git a/client/internal/routemanager/manager.go b/client/internal/routemanager/manager.go index 05b7250450b..1f812983c4e 100644 --- a/client/internal/routemanager/manager.go +++ b/client/internal/routemanager/manager.go @@ -7,6 +7,7 @@ import ( log "github.com/sirupsen/logrus" + "github.com/netbirdio/netbird/client/internal/listener" "github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/iface" "github.com/netbirdio/netbird/route" @@ -16,7 +17,7 @@ import ( // Manager is a route manager interface type Manager interface { UpdateRoutes(updateSerial uint64, newRoutes []*route.Route) error - SetRouteChangeListener(listener RouteListener) + SetRouteChangeListener(listener listener.NetworkChangeListener) InitialRouteRange() []string Stop() } @@ -96,7 +97,7 @@ func (m *DefaultManager) UpdateRoutes(updateSerial uint64, newRoutes []*route.Ro } // SetRouteChangeListener set RouteListener for route change notifier -func (m *DefaultManager) SetRouteChangeListener(listener RouteListener) { +func (m *DefaultManager) SetRouteChangeListener(listener listener.NetworkChangeListener) { m.notifier.setListener(listener) } diff --git a/client/internal/routemanager/mock.go b/client/internal/routemanager/mock.go index f56dbfb175a..8970841a225 100644 --- a/client/internal/routemanager/mock.go +++ b/client/internal/routemanager/mock.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/netbirdio/netbird/client/internal/listener" "github.com/netbirdio/netbird/iface" "github.com/netbirdio/netbird/route" ) @@ -32,7 +33,7 @@ func (m *MockManager) Start(ctx context.Context, iface *iface.WGIface) { } // SetRouteChangeListener mock implementation of SetRouteChangeListener from Manager interface -func (m *MockManager) SetRouteChangeListener(listener RouteListener) { +func (m *MockManager) SetRouteChangeListener(listener listener.NetworkChangeListener) { } diff --git a/client/internal/routemanager/notifier.go b/client/internal/routemanager/notifier.go index e3781116655..96ca95aa227 100644 --- a/client/internal/routemanager/notifier.go +++ b/client/internal/routemanager/notifier.go @@ -4,31 +4,26 @@ import ( "sort" "sync" + "github.com/netbirdio/netbird/client/internal/listener" "github.com/netbirdio/netbird/route" ) -// RouteListener is a callback interface for mobile system -type RouteListener interface { - // OnNewRouteSetting invoke when new route setting has been arrived - OnNewRouteSetting() -} - type notifier struct { initialRouteRangers []string routeRangers []string - routeListener RouteListener - routeListenerMux sync.Mutex + listener listener.NetworkChangeListener + listenerMux sync.Mutex } func newNotifier() *notifier { return ¬ifier{} } -func (n *notifier) setListener(listener RouteListener) { - n.routeListenerMux.Lock() - defer n.routeListenerMux.Unlock() - n.routeListener = listener +func (n *notifier) setListener(listener listener.NetworkChangeListener) { + n.listenerMux.Lock() + defer n.listenerMux.Unlock() + n.listener = listener } func (n *notifier) setInitialClientRoutes(clientRoutes []*route.Route) { @@ -62,15 +57,15 @@ func (n *notifier) onNewRoutes(idMap map[string][]*route.Route) { } func (n *notifier) notify() { - n.routeListenerMux.Lock() - defer n.routeListenerMux.Unlock() - if n.routeListener == nil { + n.listenerMux.Lock() + defer n.listenerMux.Unlock() + if n.listener == nil { return } - go func(l RouteListener) { - l.OnNewRouteSetting() - }(n.routeListener) + go func(l listener.NetworkChangeListener) { + l.OnNetworkChanged() + }(n.listener) } func (n *notifier) hasDiff(a []string, b []string) bool { diff --git a/iface/tun.go b/iface/tun.go index 51a7783a13e..ec8af4c322e 100644 --- a/iface/tun.go +++ b/iface/tun.go @@ -1,8 +1,9 @@ package iface type MobileIFaceArguments struct { - Routes []string - Dns string + Routes []string + Dns string + SearchDomains []string } // NetInterface represents a generic network tunnel interface diff --git a/iface/tun_adapter.go b/iface/tun_adapter.go index 0ba0bde2295..da0b1695bd7 100644 --- a/iface/tun_adapter.go +++ b/iface/tun_adapter.go @@ -2,6 +2,6 @@ package iface // TunAdapter is an interface for create tun device from externel service type TunAdapter interface { - ConfigureInterface(address string, mtu int, dns string, routes string) (int, error) + ConfigureInterface(address string, mtu int, dns string, searchDomains string, routes string) (int, error) UpdateAddr(address string) error } diff --git a/iface/tun_android.go b/iface/tun_android.go index d45f05282f8..30d86e7e21b 100644 --- a/iface/tun_android.go +++ b/iface/tun_android.go @@ -37,7 +37,8 @@ func (t *tunDevice) Create(mIFaceArgs MobileIFaceArguments) error { log.Info("create tun interface") var err error routesString := t.routesToString(mIFaceArgs.Routes) - t.fd, err = t.tunAdapter.ConfigureInterface(t.address.String(), t.mtu, mIFaceArgs.Dns, routesString) + searchDomainsToString := t.searchDomainsToString(mIFaceArgs.SearchDomains) + t.fd, err = t.tunAdapter.ConfigureInterface(t.address.String(), t.mtu, mIFaceArgs.Dns, searchDomainsToString, routesString) if err != nil { log.Errorf("failed to create Android interface: %s", err) return err @@ -94,3 +95,7 @@ func (t *tunDevice) Close() (err error) { func (t *tunDevice) routesToString(routes []string) string { return strings.Join(routes, ";") } + +func (t *tunDevice) searchDomainsToString(searchDomains []string) string { + return strings.Join(searchDomains, ";") +}