merge main
@@ -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
|
||||
|
||||
@@ -57,11 +57,11 @@ func TestPreferences_ReadUncommitedValues(t *testing.T) {
|
||||
p.SetManagementURL(exampleString)
|
||||
resp, err = p.GetManagementURL()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read managmenet url: %s", err)
|
||||
t.Fatalf("failed to read management url: %s", err)
|
||||
}
|
||||
|
||||
if resp != exampleString {
|
||||
t.Errorf("unexpected managemenet url: %s", resp)
|
||||
t.Errorf("unexpected management url: %s", resp)
|
||||
}
|
||||
|
||||
p.SetPreSharedKey(exampleString)
|
||||
@@ -102,11 +102,11 @@ func TestPreferences_Commit(t *testing.T) {
|
||||
|
||||
resp, err = p.GetManagementURL()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read managmenet url: %s", err)
|
||||
t.Fatalf("failed to read management url: %s", err)
|
||||
}
|
||||
|
||||
if resp != exampleURL {
|
||||
t.Errorf("unexpected managemenet url: %s", resp)
|
||||
t.Errorf("unexpected management url: %s", resp)
|
||||
}
|
||||
|
||||
resp, err = p.GetPreSharedKey()
|
||||
|
||||
@@ -65,7 +65,7 @@ func startManagement(t *testing.T, config *mgmt.Config) (*grpc.Server, net.Liste
|
||||
t.Fatal(err)
|
||||
}
|
||||
s := grpc.NewServer()
|
||||
store, err := mgmt.NewFileStore(config.Datadir, nil)
|
||||
store, err := mgmt.NewStoreFromJson(config.Datadir, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ func runInDaemonMode(ctx context.Context, cmd *cobra.Command) error {
|
||||
defer func() {
|
||||
err := conn.Close()
|
||||
if err != nil {
|
||||
log.Warnf("failed closing dameon gRPC client connection %v", err)
|
||||
log.Warnf("failed closing daemon gRPC client connection %v", err)
|
||||
return
|
||||
}
|
||||
}()
|
||||
@@ -200,11 +200,11 @@ func validateNATExternalIPs(list []string) error {
|
||||
|
||||
subElements := strings.Split(element, "/")
|
||||
if len(subElements) > 2 {
|
||||
return fmt.Errorf("%s is not a valid input for %s. it should be formated as \"String\" or \"String/String\"", element, externalIPMapFlag)
|
||||
return fmt.Errorf("%s is not a valid input for %s. it should be formatted as \"String\" or \"String/String\"", element, externalIPMapFlag)
|
||||
}
|
||||
|
||||
if len(subElements) == 1 && !isValidIP(subElements[0]) {
|
||||
return fmt.Errorf("%s is not a valid input for %s. it should be formated as \"IP\" or \"IP/IP\", or \"IP/Interface Name\"", element, externalIPMapFlag)
|
||||
return fmt.Errorf("%s is not a valid input for %s. it should be formatted as \"IP\" or \"IP/IP\", or \"IP/Interface Name\"", element, externalIPMapFlag)
|
||||
}
|
||||
|
||||
last := 0
|
||||
@@ -259,7 +259,7 @@ func parseCustomDNSAddress(modified bool) ([]byte, error) {
|
||||
var parsed []byte
|
||||
if modified {
|
||||
if !isValidAddrPort(customDNSAddress) {
|
||||
return nil, fmt.Errorf("%s is invalid, it should be formated as IP:Port string or as an empty string like \"\"", customDNSAddress)
|
||||
return nil, fmt.Errorf("%s is invalid, it should be formatted as IP:Port string or as an empty string like \"\"", customDNSAddress)
|
||||
}
|
||||
if customDNSAddress == "" && logFile != "console" {
|
||||
parsed = []byte("empty")
|
||||
|
||||
@@ -192,7 +192,7 @@ func (m *Manager) AddFiltering(
|
||||
}
|
||||
if ipsetName != "" {
|
||||
// ipset name is defined and it means that this rule was created
|
||||
// for it, need to assosiate it with ruleset
|
||||
// for it, need to associate it with ruleset
|
||||
m.rulesets[ipsetName] = ruleset{
|
||||
rule: rule,
|
||||
ips: map[string]string{rule.ip: ruleID},
|
||||
@@ -236,7 +236,7 @@ func (m *Manager) DeleteRule(rule fw.Rule) error {
|
||||
}
|
||||
|
||||
// we delete last IP from the set, that means we need to delete
|
||||
// set itself and assosiated firewall rule too
|
||||
// set itself and associated firewall rule too
|
||||
delete(m.rulesets, r.ipsetName)
|
||||
|
||||
if err := ipset.Destroy(r.ipsetName); err != nil {
|
||||
|
||||
@@ -754,7 +754,7 @@ func (m *Manager) AllowNetbird() error {
|
||||
}
|
||||
|
||||
if chain == nil {
|
||||
log.Debugf("chain INPUT not found. Skiping add allow netbird rule")
|
||||
log.Debugf("chain INPUT not found. Skipping add allow netbird rule")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -148,7 +148,7 @@ func TestNftablesManager(t *testing.T) {
|
||||
// test expectations:
|
||||
// 1) "accept extra routed traffic rule" for the interface
|
||||
// 2) "drop all rule" for the interface
|
||||
require.Len(t, rules, 2, "expected 2 rules after deleteion")
|
||||
require.Len(t, rules, 2, "expected 2 rules after deletion")
|
||||
|
||||
err = manager.Reset()
|
||||
require.NoError(t, err, "failed to reset")
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
package uspfilter
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type action string
|
||||
|
||||
const (
|
||||
addRule action = "add"
|
||||
deleteRule action = "delete"
|
||||
|
||||
firewallRuleName = "Netbird"
|
||||
noRulesMatchCriteria = "No rules match the specified criteria"
|
||||
addRule action = "add"
|
||||
deleteRule action = "delete"
|
||||
firewallRuleName = "Netbird"
|
||||
)
|
||||
|
||||
// Reset firewall to the default state
|
||||
@@ -26,6 +24,14 @@ func (m *Manager) Reset() error {
|
||||
m.outgoingRules = make(map[string]RuleSet)
|
||||
m.incomingRules = make(map[string]RuleSet)
|
||||
|
||||
if !isWindowsFirewallReachable() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !isFirewallRuleActive(firewallRuleName) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := manageFirewallRule(firewallRuleName, deleteRule); err != nil {
|
||||
return fmt.Errorf("couldn't remove windows firewall: %w", err)
|
||||
}
|
||||
@@ -35,6 +41,13 @@ func (m *Manager) Reset() error {
|
||||
|
||||
// AllowNetbird allows netbird interface traffic
|
||||
func (m *Manager) AllowNetbird() error {
|
||||
if !isWindowsFirewallReachable() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if isFirewallRuleActive(firewallRuleName) {
|
||||
return nil
|
||||
}
|
||||
return manageFirewallRule(firewallRuleName,
|
||||
addRule,
|
||||
"dir=in",
|
||||
@@ -45,47 +58,37 @@ func (m *Manager) AllowNetbird() error {
|
||||
)
|
||||
}
|
||||
|
||||
func manageFirewallRule(ruleName string, action action, args ...string) error {
|
||||
active, err := isFirewallRuleActive(ruleName)
|
||||
if err != nil {
|
||||
return err
|
||||
func manageFirewallRule(ruleName string, action action, extraArgs ...string) error {
|
||||
|
||||
args := []string{"advfirewall", "firewall", string(action), "rule", "name=" + ruleName}
|
||||
if action == addRule {
|
||||
args = append(args, extraArgs...)
|
||||
}
|
||||
|
||||
if (action == addRule && !active) || (action == deleteRule && active) {
|
||||
baseArgs := []string{"advfirewall", "firewall", string(action), "rule", "name=" + ruleName}
|
||||
args := append(baseArgs, args...)
|
||||
|
||||
cmd := exec.Command("netsh", args...)
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
return nil
|
||||
cmd := exec.Command("netsh", args...)
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func isFirewallRuleActive(ruleName string) (bool, error) {
|
||||
func isWindowsFirewallReachable() bool {
|
||||
args := []string{"advfirewall", "show", "allprofiles", "state"}
|
||||
cmd := exec.Command("netsh", args...)
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
||||
|
||||
_, err := cmd.Output()
|
||||
if err != nil {
|
||||
log.Infof("Windows firewall is not reachable, skipping default rule management. Using only user space rules. Error: %s", err)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func isFirewallRuleActive(ruleName string) bool {
|
||||
args := []string{"advfirewall", "firewall", "show", "rule", "name=" + ruleName}
|
||||
|
||||
cmd := exec.Command("netsh", args...)
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
var exitError *exec.ExitError
|
||||
if errors.As(err, &exitError) {
|
||||
// if the firewall rule is not active, we expect last exit code to be 1
|
||||
exitStatus := exitError.Sys().(syscall.WaitStatus).ExitStatus()
|
||||
if exitStatus == 1 {
|
||||
if strings.Contains(string(output), noRulesMatchCriteria) {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
if strings.Contains(string(output), noRulesMatchCriteria) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
_, err := cmd.Output()
|
||||
return err == nil
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ type Manager struct {
|
||||
wgNetwork *net.IPNet
|
||||
decoders sync.Pool
|
||||
wgIface IFaceMapper
|
||||
resetHook func() error
|
||||
resetHook func() error
|
||||
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
@@ -188,7 +188,7 @@ func (m *Manager) DropIncoming(packetData []byte) bool {
|
||||
return m.dropFilter(packetData, m.incomingRules, true)
|
||||
}
|
||||
|
||||
// dropFilter imlements same logic for booth direction of the traffic
|
||||
// dropFilter implements same logic for booth direction of the traffic
|
||||
func (m *Manager) dropFilter(packetData []byte, rules map[string]RuleSet, isIncomingPacket bool) bool {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
@@ -53,7 +53,7 @@ func newDefaultManager(fm firewall.Manager) *DefaultManager {
|
||||
|
||||
// ApplyFiltering firewall rules to the local firewall manager processed by ACL policy.
|
||||
//
|
||||
// If allowByDefault is ture it appends allow ALL traffic rules to input and output chains.
|
||||
// If allowByDefault is true it appends allow ALL traffic rules to input and output chains.
|
||||
func (d *DefaultManager) ApplyFiltering(networkMap *mgmProto.NetworkMap) {
|
||||
d.mutex.Lock()
|
||||
defer d.mutex.Unlock()
|
||||
@@ -366,7 +366,7 @@ func (d *DefaultManager) squashAcceptRules(
|
||||
protocols[r.Protocol] = map[string]int{}
|
||||
}
|
||||
|
||||
// special case, when we recieve this all network IP address
|
||||
// special case, when we receive this all network IP address
|
||||
// it means that rules for that protocol was already optimized on the
|
||||
// management side
|
||||
if r.PeerIP == "0.0.0.0" {
|
||||
@@ -393,7 +393,7 @@ func (d *DefaultManager) squashAcceptRules(
|
||||
}
|
||||
|
||||
// order of squashing by protocol is important
|
||||
// only for ther first element ALL, it must be done first
|
||||
// only for their first element ALL, it must be done first
|
||||
protocolOrders := []mgmProto.FirewallRuleProtocol{
|
||||
mgmProto.FirewallRule_ALL,
|
||||
mgmProto.FirewallRule_ICMP,
|
||||
|
||||
@@ -20,7 +20,7 @@ func Create(iface IFaceMapper) (manager *DefaultManager, err error) {
|
||||
return nil, err
|
||||
}
|
||||
if err := fm.AllowNetbird(); err != nil {
|
||||
log.Errorf("failed to allow netbird interface traffic: %v", err)
|
||||
log.Warnf("failed to allow netbird interface traffic: %v", err)
|
||||
}
|
||||
return newDefaultManager(fm), nil
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
mgm "github.com/netbirdio/netbird/management/client"
|
||||
mgmProto "github.com/netbirdio/netbird/management/proto"
|
||||
signal "github.com/netbirdio/netbird/signal/client"
|
||||
"github.com/netbirdio/netbird/version"
|
||||
)
|
||||
|
||||
// RunClient with main logic.
|
||||
@@ -30,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)
|
||||
}
|
||||
@@ -53,6 +54,8 @@ func RunClientiOS(ctx context.Context, config *Config, statusRecorder *peer.Stat
|
||||
}
|
||||
|
||||
func runClient(ctx context.Context, config *Config, statusRecorder *peer.Status, mobileDependency MobileDependency) error {
|
||||
log.Infof("starting NetBird client version %s", version.NetbirdVersion())
|
||||
|
||||
backOff := &backoff.ExponentialBackOff{
|
||||
InitialInterval: time.Second,
|
||||
RandomizationFactor: 1,
|
||||
@@ -106,7 +109,7 @@ func runClient(ctx context.Context, config *Config, statusRecorder *peer.Status,
|
||||
cancel()
|
||||
}()
|
||||
|
||||
log.Debugf("conecting to the Management service %s", config.ManagementURL.Host)
|
||||
log.Debugf("connecting to the Management service %s", config.ManagementURL.Host)
|
||||
mgmClient, err := mgm.NewClient(engineCtx, config.ManagementURL.Host, myPrivateKey, mgmTlsEnabled)
|
||||
if err != nil {
|
||||
return wrapErr(gstatus.Errorf(codes.FailedPrecondition, "failed connecting to Management Service : %s", err))
|
||||
|
||||
@@ -3,29 +3,25 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
fileGeneratedResolvConfContentHeader = "# Generated by NetBird"
|
||||
fileGeneratedResolvConfSearchBeginContent = "search "
|
||||
fileGeneratedResolvConfContentFormat = fileGeneratedResolvConfContentHeader +
|
||||
"\n# If needed you can restore the original file by copying back %s\n\nnameserver %s\n" +
|
||||
fileGeneratedResolvConfSearchBeginContent + "%s\n\n" +
|
||||
"%s\n"
|
||||
)
|
||||
fileGeneratedResolvConfContentHeader = "# Generated by NetBird"
|
||||
fileGeneratedResolvConfContentHeaderNextLine = fileGeneratedResolvConfContentHeader + `
|
||||
# If needed you can restore the original file by copying back ` + fileDefaultResolvConfBackupLocation + "\n\n"
|
||||
|
||||
const (
|
||||
fileDefaultResolvConfBackupLocation = defaultResolvConfPath + ".original.netbird"
|
||||
fileMaxLineCharsLimit = 256
|
||||
fileMaxNumberOfSearchDomains = 6
|
||||
)
|
||||
|
||||
var fileSearchLineBeginCharCount = len(fileGeneratedResolvConfSearchBeginContent)
|
||||
fileMaxLineCharsLimit = 256
|
||||
fileMaxNumberOfSearchDomains = 6
|
||||
)
|
||||
|
||||
type fileConfigurator struct {
|
||||
originalPerms os.FileMode
|
||||
@@ -55,58 +51,39 @@ func (f *fileConfigurator) applyDNSConfig(config HostDNSConfig) error {
|
||||
}
|
||||
return fmt.Errorf("unable to configure DNS for this peer using file manager without a nameserver group with all domains configured")
|
||||
}
|
||||
managerType, err := getOSDNSManagerType()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch managerType {
|
||||
case fileManager, netbirdManager:
|
||||
if !backupFileExist {
|
||||
err = f.backup()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to backup the resolv.conf file")
|
||||
}
|
||||
}
|
||||
default:
|
||||
// todo improve this and maybe restart DNS manager from scratch
|
||||
return fmt.Errorf("something happened and file manager is not your prefered host dns configurator, restart the agent")
|
||||
}
|
||||
|
||||
var searchDomains string
|
||||
appendedDomains := 0
|
||||
for _, dConf := range config.domains {
|
||||
if dConf.matchOnly || dConf.disabled {
|
||||
continue
|
||||
}
|
||||
if appendedDomains >= fileMaxNumberOfSearchDomains {
|
||||
// lets log all skipped domains
|
||||
log.Infof("already appended %d domains to search list. Skipping append of %s domain", fileMaxNumberOfSearchDomains, dConf.domain)
|
||||
continue
|
||||
}
|
||||
if fileSearchLineBeginCharCount+len(searchDomains) > fileMaxLineCharsLimit {
|
||||
// lets log all skipped domains
|
||||
log.Infof("search list line is larger than %d characters. Skipping append of %s domain", fileMaxLineCharsLimit, dConf.domain)
|
||||
continue
|
||||
}
|
||||
|
||||
searchDomains += " " + dConf.domain
|
||||
appendedDomains++
|
||||
}
|
||||
|
||||
originalContent, err := os.ReadFile(fileDefaultResolvConfBackupLocation)
|
||||
if err != nil {
|
||||
log.Errorf("Could not read existing resolv.conf")
|
||||
}
|
||||
content := fmt.Sprintf(fileGeneratedResolvConfContentFormat, fileDefaultResolvConfBackupLocation, config.serverIP, searchDomains, string(originalContent))
|
||||
err = writeDNSConfig(content, defaultResolvConfPath, f.originalPerms)
|
||||
if err != nil {
|
||||
err = f.restore()
|
||||
if !backupFileExist {
|
||||
err = f.backup()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to backup the resolv.conf file")
|
||||
}
|
||||
}
|
||||
|
||||
searchDomainList := searchDomains(config)
|
||||
|
||||
originalSearchDomains, nameServers, others, err := originalDNSConfigs(fileDefaultResolvConfBackupLocation)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
searchDomainList = mergeSearchDomains(searchDomainList, originalSearchDomains)
|
||||
|
||||
buf := prepareResolvConfContent(
|
||||
searchDomainList,
|
||||
append([]string{config.serverIP}, nameServers...),
|
||||
others)
|
||||
|
||||
log.Debugf("creating managed file %s", defaultResolvConfPath)
|
||||
err = os.WriteFile(defaultResolvConfPath, buf.Bytes(), f.originalPerms)
|
||||
if err != nil {
|
||||
restoreErr := f.restore()
|
||||
if restoreErr != nil {
|
||||
log.Errorf("attempt to restore default file failed with error: %s", err)
|
||||
}
|
||||
return err
|
||||
return fmt.Errorf("got an creating resolver file %s. Error: %s", defaultResolvConfPath, err)
|
||||
}
|
||||
log.Infof("created a NetBird managed %s file with your DNS settings. Added %d search domains. Search list: %s", defaultResolvConfPath, appendedDomains, searchDomains)
|
||||
|
||||
log.Infof("created a NetBird managed %s file with your DNS settings. Added %d search domains. Search list: %s", defaultResolvConfPath, len(searchDomainList), searchDomainList)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -138,15 +115,138 @@ func (f *fileConfigurator) restore() error {
|
||||
return os.RemoveAll(fileDefaultResolvConfBackupLocation)
|
||||
}
|
||||
|
||||
func writeDNSConfig(content, fileName string, permissions os.FileMode) error {
|
||||
log.Debugf("creating managed file %s", fileName)
|
||||
func prepareResolvConfContent(searchDomains, nameServers, others []string) bytes.Buffer {
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString(content)
|
||||
err := os.WriteFile(fileName, buf.Bytes(), permissions)
|
||||
if err != nil {
|
||||
return fmt.Errorf("got an creating resolver file %s. Error: %s", fileName, err)
|
||||
buf.WriteString(fileGeneratedResolvConfContentHeaderNextLine)
|
||||
|
||||
for _, cfgLine := range others {
|
||||
buf.WriteString(cfgLine)
|
||||
buf.WriteString("\n")
|
||||
}
|
||||
return nil
|
||||
|
||||
if len(searchDomains) > 0 {
|
||||
buf.WriteString("search ")
|
||||
buf.WriteString(strings.Join(searchDomains, " "))
|
||||
buf.WriteString("\n")
|
||||
}
|
||||
|
||||
for _, ns := range nameServers {
|
||||
buf.WriteString("nameserver ")
|
||||
buf.WriteString(ns)
|
||||
buf.WriteString("\n")
|
||||
}
|
||||
return buf
|
||||
}
|
||||
|
||||
func searchDomains(config hostDNSConfig) []string {
|
||||
listOfDomains := make([]string, 0)
|
||||
for _, dConf := range config.domains {
|
||||
if dConf.matchOnly || dConf.disabled {
|
||||
continue
|
||||
}
|
||||
|
||||
listOfDomains = append(listOfDomains, dConf.domain)
|
||||
}
|
||||
return listOfDomains
|
||||
}
|
||||
|
||||
func originalDNSConfigs(resolvconfFile string) (searchDomains, nameServers, others []string, err error) {
|
||||
file, err := os.Open(resolvconfFile)
|
||||
if err != nil {
|
||||
err = fmt.Errorf(`could not read existing resolv.conf`)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
reader := bufio.NewReader(file)
|
||||
|
||||
for {
|
||||
lineBytes, isPrefix, readErr := reader.ReadLine()
|
||||
if readErr != nil {
|
||||
break
|
||||
}
|
||||
|
||||
if isPrefix {
|
||||
err = fmt.Errorf(`resolv.conf line too long`)
|
||||
return
|
||||
}
|
||||
|
||||
line := strings.TrimSpace(string(lineBytes))
|
||||
|
||||
if strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(line, "domain") {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(line, "options") && strings.Contains(line, "rotate") {
|
||||
line = strings.ReplaceAll(line, "rotate", "")
|
||||
splitLines := strings.Fields(line)
|
||||
if len(splitLines) == 1 {
|
||||
continue
|
||||
}
|
||||
line = strings.Join(splitLines, " ")
|
||||
}
|
||||
|
||||
if strings.HasPrefix(line, "search") {
|
||||
splitLines := strings.Fields(line)
|
||||
if len(splitLines) < 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
searchDomains = splitLines[1:]
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(line, "nameserver") {
|
||||
splitLines := strings.Fields(line)
|
||||
if len(splitLines) != 2 {
|
||||
continue
|
||||
}
|
||||
nameServers = append(nameServers, splitLines[1])
|
||||
continue
|
||||
}
|
||||
|
||||
others = append(others, line)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// merge search domains lists and cut off the list if it is too long
|
||||
func mergeSearchDomains(searchDomains []string, originalSearchDomains []string) []string {
|
||||
lineSize := len("search")
|
||||
searchDomainsList := make([]string, 0, len(searchDomains)+len(originalSearchDomains))
|
||||
|
||||
lineSize = validateAndFillSearchDomains(lineSize, &searchDomainsList, searchDomains)
|
||||
_ = validateAndFillSearchDomains(lineSize, &searchDomainsList, originalSearchDomains)
|
||||
|
||||
return searchDomainsList
|
||||
}
|
||||
|
||||
// validateAndFillSearchDomains checks if the search domains list is not too long and if the line is not too long
|
||||
// extend s slice with vs elements
|
||||
// return with the number of characters in the searchDomains line
|
||||
func validateAndFillSearchDomains(initialLineChars int, s *[]string, vs []string) int {
|
||||
for _, sd := range vs {
|
||||
tmpCharsNumber := initialLineChars + 1 + len(sd)
|
||||
if tmpCharsNumber > fileMaxLineCharsLimit {
|
||||
// lets log all skipped domains
|
||||
log.Infof("search list line is larger than %d characters. Skipping append of %s domain", fileMaxLineCharsLimit, sd)
|
||||
continue
|
||||
}
|
||||
|
||||
initialLineChars = tmpCharsNumber
|
||||
|
||||
if len(*s) >= fileMaxNumberOfSearchDomains {
|
||||
// lets log all skipped domains
|
||||
log.Infof("already appended %d domains to search list. Skipping append of %s domain", fileMaxNumberOfSearchDomains, sd)
|
||||
continue
|
||||
}
|
||||
*s = append(*s, sd)
|
||||
}
|
||||
return initialLineChars
|
||||
}
|
||||
|
||||
func copyFile(src, dest string) error {
|
||||
|
||||
62
client/internal/dns/file_linux_test.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_mergeSearchDomains(t *testing.T) {
|
||||
searchDomains := []string{"a", "b"}
|
||||
originDomains := []string{"a", "b"}
|
||||
mergedDomains := mergeSearchDomains(searchDomains, originDomains)
|
||||
if len(mergedDomains) != 4 {
|
||||
t.Errorf("invalid len of result domains: %d, want: %d", len(mergedDomains), 4)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_mergeSearchTooMuchDomains(t *testing.T) {
|
||||
searchDomains := []string{"a", "b", "c", "d", "e", "f", "g"}
|
||||
originDomains := []string{"h", "i"}
|
||||
mergedDomains := mergeSearchDomains(searchDomains, originDomains)
|
||||
if len(mergedDomains) != 6 {
|
||||
t.Errorf("invalid len of result domains: %d, want: %d", len(mergedDomains), 6)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_mergeSearchTooMuchDomainsInOrigin(t *testing.T) {
|
||||
searchDomains := []string{"a", "b"}
|
||||
originDomains := []string{"c", "d", "e", "f", "g"}
|
||||
mergedDomains := mergeSearchDomains(searchDomains, originDomains)
|
||||
if len(mergedDomains) != 6 {
|
||||
t.Errorf("invalid len of result domains: %d, want: %d", len(mergedDomains), 6)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_mergeSearchTooLongDomain(t *testing.T) {
|
||||
searchDomains := []string{getLongLine()}
|
||||
originDomains := []string{"b"}
|
||||
mergedDomains := mergeSearchDomains(searchDomains, originDomains)
|
||||
if len(mergedDomains) != 1 {
|
||||
t.Errorf("invalid len of result domains: %d, want: %d", len(mergedDomains), 1)
|
||||
}
|
||||
|
||||
searchDomains = []string{"b"}
|
||||
originDomains = []string{getLongLine()}
|
||||
|
||||
mergedDomains = mergeSearchDomains(searchDomains, originDomains)
|
||||
if len(mergedDomains) != 1 {
|
||||
t.Errorf("invalid len of result domains: %d, want: %d", len(mergedDomains), 1)
|
||||
}
|
||||
}
|
||||
|
||||
func getLongLine() string {
|
||||
x := "search "
|
||||
for {
|
||||
for i := 0; i <= 9; i++ {
|
||||
if len(x) > fileMaxLineCharsLimit {
|
||||
return x
|
||||
}
|
||||
x = fmt.Sprintf("%s%d", x, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -78,7 +78,7 @@ func dnsConfigToHostDNSConfig(dnsConfig nbdns.Config, ip string, port int) HostD
|
||||
for _, domain := range nsConfig.Domains {
|
||||
config.domains = append(config.domains, domainConfig{
|
||||
domain: strings.TrimSuffix(domain, "."),
|
||||
matchOnly: true,
|
||||
matchOnly: !nsConfig.SearchDomainsEnabled,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,13 +22,11 @@ const (
|
||||
interfaceConfigPath = "SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\Interfaces"
|
||||
interfaceConfigNameServerKey = "NameServer"
|
||||
interfaceConfigSearchListKey = "SearchList"
|
||||
tcpipParametersPath = "SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters"
|
||||
)
|
||||
|
||||
type registryConfigurator struct {
|
||||
guid string
|
||||
routingAll bool
|
||||
existingSearchDomains []string
|
||||
guid string
|
||||
routingAll bool
|
||||
}
|
||||
|
||||
func newHostManager(wgInterface WGIface) (hostManager, error) {
|
||||
@@ -148,30 +146,11 @@ func (r *registryConfigurator) restoreHostDNS() error {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
return r.updateSearchDomains([]string{})
|
||||
return r.deleteInterfaceRegistryKeyProperty(interfaceConfigSearchListKey)
|
||||
}
|
||||
|
||||
func (r *registryConfigurator) updateSearchDomains(domains []string) error {
|
||||
value, err := getLocalMachineRegistryKeyStringValue(tcpipParametersPath, interfaceConfigSearchListKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to get current search domains failed with error: %s", err)
|
||||
}
|
||||
|
||||
valueList := strings.Split(value, ",")
|
||||
setExisting := false
|
||||
if len(r.existingSearchDomains) == 0 {
|
||||
r.existingSearchDomains = valueList
|
||||
setExisting = true
|
||||
}
|
||||
|
||||
if len(domains) == 0 && setExisting {
|
||||
log.Infof("added %d search domains to the registry. Domain list: %s", len(domains), domains)
|
||||
return nil
|
||||
}
|
||||
|
||||
newList := append(r.existingSearchDomains, domains...)
|
||||
|
||||
err = setLocalMachineRegistryKeyStringValue(tcpipParametersPath, interfaceConfigSearchListKey, strings.Join(newList, ","))
|
||||
err := r.setInterfaceRegistryKeyStringValue(interfaceConfigSearchListKey, strings.Join(domains, ","))
|
||||
if err != nil {
|
||||
return fmt.Errorf("adding search domain failed with error: %s", err)
|
||||
}
|
||||
@@ -235,33 +214,3 @@ func removeRegistryKeyFromDNSPolicyConfig(regKeyPath string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getLocalMachineRegistryKeyStringValue(keyPath, key string) (string, error) {
|
||||
regKey, err := registry.OpenKey(registry.LOCAL_MACHINE, keyPath, registry.QUERY_VALUE)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to open existing key from registry, key path: HKEY_LOCAL_MACHINE\\%s, error: %s", keyPath, err)
|
||||
}
|
||||
defer regKey.Close()
|
||||
|
||||
val, _, err := regKey.GetStringValue(key)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("getting %s value for key path HKEY_LOCAL_MACHINE\\%s failed with error: %s", key, keyPath, err)
|
||||
}
|
||||
|
||||
return val, nil
|
||||
}
|
||||
|
||||
func setLocalMachineRegistryKeyStringValue(keyPath, key, value string) error {
|
||||
regKey, err := registry.OpenKey(registry.LOCAL_MACHINE, keyPath, registry.SET_VALUE)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to open existing key from registry, key path: HKEY_LOCAL_MACHINE\\%s, error: %s", keyPath, err)
|
||||
}
|
||||
defer regKey.Close()
|
||||
|
||||
err = regKey.SetStringValue(key, value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting %s value %s for key path HKEY_LOCAL_MACHINE\\%s failed with error: %s", key, value, keyPath, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
57
client/internal/dns/notifier.go
Normal file
@@ -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)
|
||||
}
|
||||
@@ -3,10 +3,9 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
@@ -15,11 +14,24 @@ const resolvconfCommand = "resolvconf"
|
||||
|
||||
type resolvconf struct {
|
||||
ifaceName string
|
||||
|
||||
originalSearchDomains []string
|
||||
originalNameServers []string
|
||||
othersConfigs []string
|
||||
}
|
||||
|
||||
// supported "openresolv" only
|
||||
func newResolvConfConfigurator(wgInterface WGIface) (hostManager, error) {
|
||||
originalSearchDomains, nameServers, others, err := originalDNSConfigs("/etc/resolv.conf")
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
return &resolvconf{
|
||||
ifaceName: wgInterface.Name(),
|
||||
ifaceName: wgInterface.Name(),
|
||||
originalSearchDomains: originalSearchDomains,
|
||||
originalNameServers: nameServers,
|
||||
othersConfigs: others,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -37,41 +49,20 @@ func (r *resolvconf) applyDNSConfig(config HostDNSConfig) error {
|
||||
return fmt.Errorf("unable to configure DNS for this peer using resolvconf manager without a nameserver group with all domains configured")
|
||||
}
|
||||
|
||||
var searchDomains string
|
||||
appendedDomains := 0
|
||||
for _, dConf := range config.domains {
|
||||
if dConf.matchOnly || dConf.disabled {
|
||||
continue
|
||||
}
|
||||
searchDomainList := searchDomains(config)
|
||||
searchDomainList = mergeSearchDomains(searchDomainList, r.originalSearchDomains)
|
||||
|
||||
if appendedDomains >= fileMaxNumberOfSearchDomains {
|
||||
// lets log all skipped domains
|
||||
log.Infof("already appended %d domains to search list. Skipping append of %s domain", fileMaxNumberOfSearchDomains, dConf.domain)
|
||||
continue
|
||||
}
|
||||
buf := prepareResolvConfContent(
|
||||
searchDomainList,
|
||||
append([]string{config.serverIP}, r.originalNameServers...),
|
||||
r.othersConfigs)
|
||||
|
||||
if fileSearchLineBeginCharCount+len(searchDomains) > fileMaxLineCharsLimit {
|
||||
// lets log all skipped domains
|
||||
log.Infof("search list line is larger than %d characters. Skipping append of %s domain", fileMaxLineCharsLimit, dConf.domain)
|
||||
continue
|
||||
}
|
||||
|
||||
searchDomains += " " + dConf.domain
|
||||
appendedDomains++
|
||||
}
|
||||
|
||||
originalContent, err := os.ReadFile(fileDefaultResolvConfBackupLocation)
|
||||
if err != nil {
|
||||
log.Errorf("Could not read existing resolv.conf")
|
||||
}
|
||||
content := fmt.Sprintf(fileGeneratedResolvConfContentFormat, fileDefaultResolvConfBackupLocation, config.serverIP, searchDomains, string(originalContent))
|
||||
|
||||
err = r.applyConfig(content)
|
||||
err = r.applyConfig(buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Infof("added %d search domains. Search list: %s", appendedDomains, searchDomains)
|
||||
log.Infof("added %d search domains. Search list: %s", len(searchDomainList), searchDomainList)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -84,12 +75,12 @@ func (r *resolvconf) restoreHostDNS() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *resolvconf) applyConfig(content string) error {
|
||||
func (r *resolvconf) applyConfig(content bytes.Buffer) error {
|
||||
cmd := exec.Command(resolvconfCommand, "-x", "-a", r.ifaceName)
|
||||
cmd.Stdin = strings.NewReader(content)
|
||||
cmd.Stdin = &content
|
||||
_, err := cmd.Output()
|
||||
if err != nil {
|
||||
return fmt.Errorf("got an error while appying resolvconf configuration for %s interface, error: %s", r.ifaceName, err)
|
||||
return fmt.Errorf("got an error while applying resolvconf configuration for %s interface, error: %s", r.ifaceName, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -4,13 +4,13 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"runtime"
|
||||
"sync"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/mitchellh/hashstructure/v2"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/netbirdio/netbird/client/internal/listener"
|
||||
nbdns "github.com/netbirdio/netbird/dns"
|
||||
)
|
||||
|
||||
@@ -31,6 +31,7 @@ type Server interface {
|
||||
DnsIP() string
|
||||
UpdateDNSServer(serial uint64, update nbdns.Config) error
|
||||
OnUpdatedHostDNSServer(strings []string)
|
||||
SearchDomains() []string
|
||||
}
|
||||
|
||||
type registeredHandlerMap map[string]handlerWithStop
|
||||
@@ -56,6 +57,9 @@ type DefaultServer struct {
|
||||
|
||||
interfaceName string
|
||||
wgAddr string
|
||||
|
||||
// make sense on mobile only
|
||||
searchDomainNotifier *notifier
|
||||
}
|
||||
|
||||
type handlerWithStop interface {
|
||||
@@ -90,12 +94,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
|
||||
}
|
||||
@@ -227,6 +234,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
|
||||
@@ -261,6 +283,10 @@ func (s *DefaultServer) applyConfiguration(update nbdns.Config) error {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
if s.searchDomainNotifier != nil {
|
||||
s.searchDomainNotifier.onNewSearchDomains(s.SearchDomains())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -303,7 +329,7 @@ func (s *DefaultServer) buildUpstreamHandlerUpdate(nameServerGroups []*nbdns.Nam
|
||||
handler := newUpstreamResolver(s.ctx, s.interfaceName, s.wgAddr)
|
||||
for _, ns := range nsGroup.NameServers {
|
||||
if ns.NSType != nbdns.UDPNameServerType {
|
||||
log.Warnf("skiping nameserver %s with type %s, this peer supports only %s",
|
||||
log.Warnf("skipping nameserver %s with type %s, this peer supports only %s",
|
||||
ns.IP.String(), ns.NSType.String(), nbdns.UDPNameServerType.String())
|
||||
continue
|
||||
}
|
||||
@@ -321,7 +347,7 @@ func (s *DefaultServer) buildUpstreamHandlerUpdate(nameServerGroups []*nbdns.Nam
|
||||
// reapply DNS settings, but it not touch the original configuration and serial number
|
||||
// because it is temporal deactivation until next try
|
||||
//
|
||||
// after some period defined by upstream it trys to reactivate self by calling this hook
|
||||
// after some period defined by upstream it tries to reactivate self by calling this hook
|
||||
// everything we need here is just to re-apply current configuration because it already
|
||||
// contains this upstream settings (temporal deactivation not removed it)
|
||||
handler.deactivate, handler.reactivate = s.upstreamCallbacks(nsGroup, handler)
|
||||
|
||||
@@ -19,6 +19,6 @@ func TestGetServerDns(t *testing.T) {
|
||||
}
|
||||
|
||||
if srvB != srv {
|
||||
t.Errorf("missmatch dns instances")
|
||||
t.Errorf("mismatch dns instances")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -118,7 +118,7 @@ func (u *upstreamResolver) getClientPrivate() *dns.Client {
|
||||
}
|
||||
|
||||
func (u *upstreamResolver) stop() {
|
||||
log.Debugf("stoping serving DNS for upstreams %s", u.upstreamServers)
|
||||
log.Debugf("stopping serving DNS for upstreams %s", u.upstreamServers)
|
||||
u.cancel()
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
@@ -214,17 +215,16 @@ func (e *Engine) Start() error {
|
||||
}
|
||||
}
|
||||
|
||||
log.Debugf("Initial routes contain %d routes", len(routes))
|
||||
e.routeManager = routemanager.NewManager(e.ctx, e.config.WgPrivateKey.PublicKey().String(), e.wgInterface, e.statusRecorder, routes)
|
||||
e.mobileDep.RouteListener.SetInterfaceIP(wgAddr)
|
||||
|
||||
e.routeManager.SetRouteChangeListener(e.mobileDep.RouteListener)
|
||||
e.mobileDep.NetworkChangeListener.SetInterfaceIP(wgAddr)
|
||||
e.routeManager.SetRouteChangeListener(e.mobileDep.NetworkChangeListener)
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "android":
|
||||
err = e.wgInterface.CreateOnAndroid(iface.MobileIFaceArguments{
|
||||
Routes: e.routeManager.InitialRouteRange(),
|
||||
Dns: e.dnsServer.DnsIP(),
|
||||
Routes: e.routeManager.InitialRouteRange(),
|
||||
Dns: e.dnsServer.DnsIP(),
|
||||
SearchDomains: e.dnsServer.SearchDomains(),
|
||||
})
|
||||
case "ios":
|
||||
err = e.wgInterface.CreateOniOS(e.mobileDep.FileDescriptor)
|
||||
@@ -724,8 +724,9 @@ func toDNSConfig(protoDNSConfig *mgmProto.DNSConfig) nbdns.Config {
|
||||
|
||||
for _, nsGroup := range protoDNSConfig.GetNameServerGroups() {
|
||||
dnsNSGroup := &nbdns.NameServerGroup{
|
||||
Primary: nsGroup.GetPrimary(),
|
||||
Domains: nsGroup.GetDomains(),
|
||||
Primary: nsGroup.GetPrimary(),
|
||||
Domains: nsGroup.GetDomains(),
|
||||
SearchDomainsEnabled: nsGroup.GetSearchDomainsEnabled(),
|
||||
}
|
||||
for _, ns := range nsGroup.GetNameServers() {
|
||||
dnsNS := nbdns.NameServer{
|
||||
@@ -1060,13 +1061,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) {
|
||||
|
||||
@@ -1039,10 +1039,11 @@ func startManagement(dataDir string) (*grpc.Server, string, error) {
|
||||
return nil, "", err
|
||||
}
|
||||
s := grpc.NewServer(grpc.KeepaliveEnforcementPolicy(kaep), grpc.KeepaliveParams(kasp))
|
||||
store, err := server.NewFileStore(config.Datadir, nil)
|
||||
store, err := server.NewStoreFromJson(config.Datadir, nil)
|
||||
if err != nil {
|
||||
log.Fatalf("failed creating a store: %s: %v", config.Datadir, err)
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
peersUpdateManager := server.NewPeersUpdateManager()
|
||||
eventStore := &activity.InMemoryEventStore{}
|
||||
if err != nil {
|
||||
|
||||
7
client/internal/listener/network_change.go
Normal file
@@ -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()
|
||||
}
|
||||
@@ -2,19 +2,19 @@ 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
|
||||
DnsManager dns.IosDnsManager
|
||||
FileDescriptor int32
|
||||
InterfaceName string
|
||||
TunAdapter iface.TunAdapter
|
||||
IFaceDiscover stdnet.ExternalIFaceDiscover
|
||||
NetworkChangeListener listener.NetworkChangeListener
|
||||
HostDNSAddresses []string
|
||||
DnsReadyListener dns.ReadyListener
|
||||
DnsManager dns.IosDnsManager
|
||||
FileDescriptor int32
|
||||
InterfaceName string
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ func (c *clientNetwork) getBestRouteFromStatuses(routePeerStatuses map[string]ro
|
||||
log.Warnf("the network %s has not been assigned a routing peer as no peers from the list %s are currently connected", c.network, peers)
|
||||
|
||||
} else if chosen != currID {
|
||||
log.Infof("new chosen route is %s with peer %s with score %d", chosen, c.routes[chosen].Peer, chosenScore)
|
||||
log.Infof("new chosen route is %s with peer %s with score %d for network %s", chosen, c.routes[chosen].Peer, chosenScore, c.network)
|
||||
}
|
||||
|
||||
return chosen
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -155,7 +156,7 @@ func (m *DefaultManager) classifiesRoutes(newRoutes []*route.Route) (map[string]
|
||||
// if prefix is too small, lets assume is a possible default route which is not yet supported
|
||||
// we skip this route management
|
||||
if newRoute.Network.Bits() < 7 {
|
||||
log.Errorf("this agent version: %s, doesn't support default routes, received %s, skiping this route",
|
||||
log.Errorf("this agent version: %s, doesn't support default routes, received %s, skipping this route",
|
||||
version.NetbirdVersion(), newRoute.Network)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -135,7 +135,8 @@ func (n *nftablesManager) RestoreOrCreateContainers() error {
|
||||
}
|
||||
|
||||
for _, table := range tables {
|
||||
if table.Name == "filter" {
|
||||
if table.Name == "filter" && table.Family == nftables.TableFamilyIPv4 {
|
||||
log.Debugf("nftables: found filter table for ipv4")
|
||||
n.filterTable = table
|
||||
continue
|
||||
}
|
||||
@@ -486,7 +487,7 @@ func (n *nftablesManager) RemoveRoutingRules(pair routerPair) error {
|
||||
if len(n.rules) == 2 && n.defaultForwardRules[0] != nil {
|
||||
err := n.eraseDefaultForwardRule()
|
||||
if err != nil {
|
||||
log.Errorf("failed to delte default fwd rule: %s", err)
|
||||
log.Errorf("failed to delete default fwd rule: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,37 +2,34 @@ package routemanager
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"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(string)
|
||||
SetInterfaceIP(string)
|
||||
}
|
||||
// type RouteListener interface {
|
||||
// // OnNewRouteSetting invoke when new route setting has been arrived
|
||||
// OnNewRouteSetting(string)
|
||||
// SetInterfaceIP(string)
|
||||
// }
|
||||
|
||||
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) {
|
||||
@@ -63,16 +60,16 @@ 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) {
|
||||
go func(l listener.NetworkChangeListener) {
|
||||
log.Debugf("notifying route listener with route ranges: %s", strings.Join(n.routeRangers, ","))
|
||||
l.OnNewRouteSetting(strings.Join(n.routeRangers, ","))
|
||||
}(n.routeListener)
|
||||
l.OnNetworkChanged(strings.Join(n.routeRangers, ","))
|
||||
}(n.listener)
|
||||
}
|
||||
|
||||
func (n *notifier) hasDiff(a []string, b []string) bool {
|
||||
|
||||
5
client/ui/build-ui-linux.sh
Normal file
@@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
sudo apt update
|
||||
sudo apt remove gir1.2-appindicator3-0.1
|
||||
sudo apt install -y libayatana-appindicator3-dev
|
||||
go build
|
||||
@@ -15,8 +15,10 @@ import (
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/app"
|
||||
@@ -74,18 +76,30 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
//go:embed connected.ico
|
||||
//go:embed netbird-systemtray-connected.ico
|
||||
var iconConnectedICO []byte
|
||||
|
||||
//go:embed connected.png
|
||||
//go:embed netbird-systemtray-connected.png
|
||||
var iconConnectedPNG []byte
|
||||
|
||||
//go:embed disconnected.ico
|
||||
//go:embed netbird-systemtray-default.ico
|
||||
var iconDisconnectedICO []byte
|
||||
|
||||
//go:embed disconnected.png
|
||||
//go:embed netbird-systemtray-default.png
|
||||
var iconDisconnectedPNG []byte
|
||||
|
||||
//go:embed netbird-systemtray-update.ico
|
||||
var iconUpdateICO []byte
|
||||
|
||||
//go:embed netbird-systemtray-update.png
|
||||
var iconUpdatePNG []byte
|
||||
|
||||
//go:embed netbird-systemtray-update-cloud.ico
|
||||
var iconUpdateCloudICO []byte
|
||||
|
||||
//go:embed netbird-systemtray-update-cloud.png
|
||||
var iconUpdateCloudPNG []byte
|
||||
|
||||
type serviceClient struct {
|
||||
ctx context.Context
|
||||
addr string
|
||||
@@ -93,14 +107,20 @@ type serviceClient struct {
|
||||
|
||||
icConnected []byte
|
||||
icDisconnected []byte
|
||||
icUpdate []byte
|
||||
icUpdateCloud []byte
|
||||
|
||||
// systray menu items
|
||||
mStatus *systray.MenuItem
|
||||
mUp *systray.MenuItem
|
||||
mDown *systray.MenuItem
|
||||
mAdminPanel *systray.MenuItem
|
||||
mSettings *systray.MenuItem
|
||||
mQuit *systray.MenuItem
|
||||
mStatus *systray.MenuItem
|
||||
mUp *systray.MenuItem
|
||||
mDown *systray.MenuItem
|
||||
mAdminPanel *systray.MenuItem
|
||||
mSettings *systray.MenuItem
|
||||
mAbout *systray.MenuItem
|
||||
mVersionUI *systray.MenuItem
|
||||
mVersionDaemon *systray.MenuItem
|
||||
mUpdate *systray.MenuItem
|
||||
mQuit *systray.MenuItem
|
||||
|
||||
// application with main windows.
|
||||
app fyne.App
|
||||
@@ -118,6 +138,11 @@ type serviceClient struct {
|
||||
managementURL string
|
||||
preSharedKey string
|
||||
adminURL string
|
||||
|
||||
update *version.Update
|
||||
daemonVersion string
|
||||
updateIndicationLock sync.Mutex
|
||||
isUpdateIconActive bool
|
||||
}
|
||||
|
||||
// newServiceClient instance constructor
|
||||
@@ -130,14 +155,20 @@ func newServiceClient(addr string, a fyne.App, showSettings bool) *serviceClient
|
||||
app: a,
|
||||
|
||||
showSettings: showSettings,
|
||||
update: version.NewUpdate(),
|
||||
}
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
s.icConnected = iconConnectedICO
|
||||
s.icDisconnected = iconDisconnectedICO
|
||||
s.icUpdate = iconUpdateICO
|
||||
s.icUpdateCloud = iconUpdateCloudICO
|
||||
|
||||
} else {
|
||||
s.icConnected = iconConnectedPNG
|
||||
s.icDisconnected = iconDisconnectedPNG
|
||||
s.icUpdate = iconUpdatePNG
|
||||
s.icUpdateCloud = iconUpdateCloudPNG
|
||||
}
|
||||
|
||||
if showSettings {
|
||||
@@ -202,9 +233,10 @@ func (s *serviceClient) getSettingsForm() *widget.Form {
|
||||
}
|
||||
|
||||
_, err = client.Login(s.ctx, &proto.LoginRequest{
|
||||
ManagementUrl: s.iMngURL.Text,
|
||||
AdminURL: s.iAdminURL.Text,
|
||||
PreSharedKey: s.iPreSharedKey.Text,
|
||||
ManagementUrl: s.iMngURL.Text,
|
||||
AdminURL: s.iAdminURL.Text,
|
||||
PreSharedKey: s.iPreSharedKey.Text,
|
||||
IsLinuxDesktopClient: runtime.GOOS == "linux",
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorf("login to management URL: %v", err)
|
||||
@@ -233,7 +265,9 @@ func (s *serviceClient) login() error {
|
||||
return err
|
||||
}
|
||||
|
||||
loginResp, err := conn.Login(s.ctx, &proto.LoginRequest{})
|
||||
loginResp, err := conn.Login(s.ctx, &proto.LoginRequest{
|
||||
IsLinuxDesktopClient: runtime.GOOS == "linux",
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorf("login to management URL with: %v", err)
|
||||
return err
|
||||
@@ -325,19 +359,53 @@ func (s *serviceClient) updateStatus() error {
|
||||
return err
|
||||
}
|
||||
|
||||
s.updateIndicationLock.Lock()
|
||||
defer s.updateIndicationLock.Unlock()
|
||||
|
||||
var systrayIconState bool
|
||||
if status.Status == string(internal.StatusConnected) && !s.mUp.Disabled() {
|
||||
systray.SetIcon(s.icConnected)
|
||||
if !s.isUpdateIconActive {
|
||||
systray.SetIcon(s.icConnected)
|
||||
}
|
||||
systray.SetTooltip("NetBird (Connected)")
|
||||
s.mStatus.SetTitle("Connected")
|
||||
s.mUp.Disable()
|
||||
s.mDown.Enable()
|
||||
systrayIconState = true
|
||||
} else if status.Status != string(internal.StatusConnected) && s.mUp.Disabled() {
|
||||
systray.SetIcon(s.icDisconnected)
|
||||
if !s.isUpdateIconActive {
|
||||
systray.SetIcon(s.icDisconnected)
|
||||
}
|
||||
systray.SetTooltip("NetBird (Disconnected)")
|
||||
s.mStatus.SetTitle("Disconnected")
|
||||
s.mDown.Disable()
|
||||
s.mUp.Enable()
|
||||
systrayIconState = false
|
||||
}
|
||||
|
||||
// the updater struct notify by the upgrades available only, but if meanwhile the daemon has successfully
|
||||
// updated must reset the mUpdate visibility state
|
||||
if s.daemonVersion != status.DaemonVersion {
|
||||
s.mUpdate.Hide()
|
||||
s.daemonVersion = status.DaemonVersion
|
||||
|
||||
s.isUpdateIconActive = s.update.SetDaemonVersion(status.DaemonVersion)
|
||||
if !s.isUpdateIconActive {
|
||||
if systrayIconState {
|
||||
systray.SetIcon(s.icConnected)
|
||||
s.mAbout.SetIcon(s.icConnected)
|
||||
} else {
|
||||
systray.SetIcon(s.icDisconnected)
|
||||
s.mAbout.SetIcon(s.icDisconnected)
|
||||
}
|
||||
}
|
||||
|
||||
daemonVersionTitle := normalizedVersion(s.daemonVersion)
|
||||
s.mVersionDaemon.SetTitle(fmt.Sprintf("Daemon: %s", daemonVersionTitle))
|
||||
s.mVersionDaemon.SetTooltip(fmt.Sprintf("Daemon version: %s", daemonVersionTitle))
|
||||
s.mVersionDaemon.Show()
|
||||
}
|
||||
|
||||
return nil
|
||||
}, &backoff.ExponentialBackOff{
|
||||
InitialInterval: time.Second,
|
||||
@@ -371,11 +439,24 @@ func (s *serviceClient) onTrayReady() {
|
||||
systray.AddSeparator()
|
||||
s.mSettings = systray.AddMenuItem("Settings", "Settings of the application")
|
||||
systray.AddSeparator()
|
||||
v := systray.AddMenuItem("v"+version.NetbirdVersion(), "Client Version: "+version.NetbirdVersion())
|
||||
v.Disable()
|
||||
|
||||
s.mAbout = systray.AddMenuItem("About", "About")
|
||||
s.mAbout.SetIcon(s.icDisconnected)
|
||||
versionString := normalizedVersion(version.NetbirdVersion())
|
||||
s.mVersionUI = s.mAbout.AddSubMenuItem(fmt.Sprintf("GUI: %s", versionString), fmt.Sprintf("GUI Version: %s", versionString))
|
||||
s.mVersionUI.Disable()
|
||||
|
||||
s.mVersionDaemon = s.mAbout.AddSubMenuItem("", "")
|
||||
s.mVersionDaemon.Disable()
|
||||
s.mVersionDaemon.Hide()
|
||||
|
||||
s.mUpdate = s.mAbout.AddSubMenuItem("Download latest version", "Download latest version")
|
||||
s.mUpdate.Hide()
|
||||
|
||||
systray.AddSeparator()
|
||||
s.mQuit = systray.AddMenuItem("Quit", "Quit the client app")
|
||||
|
||||
s.update.SetOnUpdateListener(s.onUpdateAvailable)
|
||||
go func() {
|
||||
s.getSrvConfig()
|
||||
for {
|
||||
@@ -433,6 +514,11 @@ func (s *serviceClient) onTrayReady() {
|
||||
case <-s.mQuit.ClickedCh:
|
||||
systray.Quit()
|
||||
return
|
||||
case <-s.mUpdate.ClickedCh:
|
||||
err := openURL(version.DownloadUrl())
|
||||
if err != nil {
|
||||
log.Errorf("%s", err)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
log.Errorf("process connection: %v", err)
|
||||
@@ -441,6 +527,14 @@ func (s *serviceClient) onTrayReady() {
|
||||
}()
|
||||
}
|
||||
|
||||
func normalizedVersion(version string) string {
|
||||
versionString := version
|
||||
if unicode.IsDigit(rune(versionString[0])) {
|
||||
versionString = fmt.Sprintf("v%s", versionString)
|
||||
}
|
||||
return versionString
|
||||
}
|
||||
|
||||
func (s *serviceClient) onTrayExit() {}
|
||||
|
||||
// getSrvClient connection to the service.
|
||||
@@ -501,6 +595,32 @@ func (s *serviceClient) getSrvConfig() {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *serviceClient) onUpdateAvailable() {
|
||||
s.updateIndicationLock.Lock()
|
||||
defer s.updateIndicationLock.Unlock()
|
||||
|
||||
s.mUpdate.Show()
|
||||
s.mAbout.SetIcon(s.icUpdateCloud)
|
||||
|
||||
s.isUpdateIconActive = true
|
||||
systray.SetIcon(s.icUpdate)
|
||||
}
|
||||
|
||||
func openURL(url string) error {
|
||||
var err error
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
|
||||
case "darwin":
|
||||
err = exec.Command("open", url).Start()
|
||||
case "linux":
|
||||
err = exec.Command("xdg-open", url).Start()
|
||||
default:
|
||||
err = fmt.Errorf("unsupported platform")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// checkPIDFile exists and return error, or write new.
|
||||
func checkPIDFile() error {
|
||||
pidFile := path.Join(os.TempDir(), "wiretrustee-ui.pid")
|
||||
|
||||
|
Before Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 6.3 KiB |
BIN
client/ui/netbird-systemtray-connected.ico
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
client/ui/netbird-systemtray-connected.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
client/ui/netbird-systemtray-default.ico
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
client/ui/netbird-systemtray-default.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
client/ui/netbird-systemtray-update-cloud.ico
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
client/ui/netbird-systemtray-update-cloud.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
client/ui/netbird-systemtray-update.ico
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
client/ui/netbird-systemtray-update.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |