From b307298b2ff38816698db1a925d533e2bca95362 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Fri, 21 Feb 2025 16:29:21 +0100 Subject: [PATCH 1/8] [client] Add netbird ui improvements (#3222) --- client/cmd/debug.go | 13 +- client/cmd/status.go | 714 +---------------- client/cmd/status_test.go | 596 -------------- client/internal/connect.go | 11 + client/internal/dns/file_unix.go | 6 +- client/internal/dns/host.go | 15 + client/internal/dns/host_android.go | 4 + client/internal/dns/host_darwin.go | 4 + client/internal/dns/host_ios.go | 4 + client/internal/dns/host_windows.go | 4 + client/internal/dns/network_manager_unix.go | 4 + client/internal/dns/resolvconf_unix.go | 6 +- client/internal/dns/server.go | 23 +- client/internal/dns/server_test.go | 14 +- client/internal/dns/systemd_linux.go | 4 + client/internal/dns/upstream.go | 19 +- client/internal/engine.go | 3 + client/internal/routemanager/client.go | 49 +- client/proto/daemon.pb.go | 203 ++--- client/proto/daemon.proto | 1 + client/server/network.go | 25 + .../{cmd/status_event.go => status/event.go} | 12 +- client/status/status.go | 725 ++++++++++++++++++ client/status/status_test.go | 603 +++++++++++++++ client/ui/bundled.go | 12 - client/ui/client_ui.go | 100 ++- client/ui/config/config.go | 46 -- client/ui/debug.go | 50 ++ client/ui/event/event.go | 41 +- client/ui/network.go | 202 ++++- 30 files changed, 1984 insertions(+), 1529 deletions(-) rename client/{cmd/status_event.go => status/event.go} (86%) create mode 100644 client/status/status.go create mode 100644 client/status/status_test.go delete mode 100644 client/ui/bundled.go delete mode 100644 client/ui/config/config.go create mode 100644 client/ui/debug.go diff --git a/client/cmd/debug.go b/client/cmd/debug.go index c7ab87b47..c02f60aed 100644 --- a/client/cmd/debug.go +++ b/client/cmd/debug.go @@ -13,6 +13,7 @@ import ( "github.com/netbirdio/netbird/client/internal" "github.com/netbirdio/netbird/client/proto" "github.com/netbirdio/netbird/client/server" + nbstatus "github.com/netbirdio/netbird/client/status" ) const errCloseConnection = "Failed to close connection: %v" @@ -85,7 +86,7 @@ func debugBundle(cmd *cobra.Command, _ []string) error { client := proto.NewDaemonServiceClient(conn) resp, err := client.DebugBundle(cmd.Context(), &proto.DebugBundleRequest{ Anonymize: anonymizeFlag, - Status: getStatusOutput(cmd), + Status: getStatusOutput(cmd, anonymizeFlag), SystemInfo: debugSystemInfoFlag, }) if err != nil { @@ -196,7 +197,7 @@ func runForDuration(cmd *cobra.Command, args []string) error { time.Sleep(3 * time.Second) headerPostUp := fmt.Sprintf("----- Netbird post-up - Timestamp: %s", time.Now().Format(time.RFC3339)) - statusOutput := fmt.Sprintf("%s\n%s", headerPostUp, getStatusOutput(cmd)) + statusOutput := fmt.Sprintf("%s\n%s", headerPostUp, getStatusOutput(cmd, anonymizeFlag)) if waitErr := waitForDurationOrCancel(cmd.Context(), duration, cmd); waitErr != nil { return waitErr @@ -206,7 +207,7 @@ func runForDuration(cmd *cobra.Command, args []string) error { cmd.Println("Creating debug bundle...") headerPreDown := fmt.Sprintf("----- Netbird pre-down - Timestamp: %s - Duration: %s", time.Now().Format(time.RFC3339), duration) - statusOutput = fmt.Sprintf("%s\n%s\n%s", statusOutput, headerPreDown, getStatusOutput(cmd)) + statusOutput = fmt.Sprintf("%s\n%s\n%s", statusOutput, headerPreDown, getStatusOutput(cmd, anonymizeFlag)) resp, err := client.DebugBundle(cmd.Context(), &proto.DebugBundleRequest{ Anonymize: anonymizeFlag, @@ -271,13 +272,15 @@ func setNetworkMapPersistence(cmd *cobra.Command, args []string) error { return nil } -func getStatusOutput(cmd *cobra.Command) string { +func getStatusOutput(cmd *cobra.Command, anon bool) string { var statusOutputString string statusResp, err := getStatus(cmd.Context()) if err != nil { cmd.PrintErrf("Failed to get status: %v\n", err) } else { - statusOutputString = parseToFullDetailSummary(convertToStatusOutputOverview(statusResp)) + statusOutputString = nbstatus.ParseToFullDetailSummary( + nbstatus.ConvertToStatusOutputOverview(statusResp, anon, "", nil, nil, nil), + ) } return statusOutputString } diff --git a/client/cmd/status.go b/client/cmd/status.go index bf4588ce4..0ddba8b2f 100644 --- a/client/cmd/status.go +++ b/client/cmd/status.go @@ -2,106 +2,20 @@ package cmd import ( "context" - "encoding/json" "fmt" "net" "net/netip" - "os" - "runtime" - "sort" "strings" - "time" "github.com/spf13/cobra" "google.golang.org/grpc/status" - "gopkg.in/yaml.v3" - "github.com/netbirdio/netbird/client/anonymize" "github.com/netbirdio/netbird/client/internal" - "github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/client/proto" + nbstatus "github.com/netbirdio/netbird/client/status" "github.com/netbirdio/netbird/util" - "github.com/netbirdio/netbird/version" ) -type peerStateDetailOutput struct { - FQDN string `json:"fqdn" yaml:"fqdn"` - IP string `json:"netbirdIp" yaml:"netbirdIp"` - PubKey string `json:"publicKey" yaml:"publicKey"` - Status string `json:"status" yaml:"status"` - LastStatusUpdate time.Time `json:"lastStatusUpdate" yaml:"lastStatusUpdate"` - ConnType string `json:"connectionType" yaml:"connectionType"` - IceCandidateType iceCandidateType `json:"iceCandidateType" yaml:"iceCandidateType"` - IceCandidateEndpoint iceCandidateType `json:"iceCandidateEndpoint" yaml:"iceCandidateEndpoint"` - RelayAddress string `json:"relayAddress" yaml:"relayAddress"` - LastWireguardHandshake time.Time `json:"lastWireguardHandshake" yaml:"lastWireguardHandshake"` - TransferReceived int64 `json:"transferReceived" yaml:"transferReceived"` - TransferSent int64 `json:"transferSent" yaml:"transferSent"` - Latency time.Duration `json:"latency" yaml:"latency"` - RosenpassEnabled bool `json:"quantumResistance" yaml:"quantumResistance"` - Networks []string `json:"networks" yaml:"networks"` -} - -type peersStateOutput struct { - Total int `json:"total" yaml:"total"` - Connected int `json:"connected" yaml:"connected"` - Details []peerStateDetailOutput `json:"details" yaml:"details"` -} - -type signalStateOutput struct { - URL string `json:"url" yaml:"url"` - Connected bool `json:"connected" yaml:"connected"` - Error string `json:"error" yaml:"error"` -} - -type managementStateOutput struct { - URL string `json:"url" yaml:"url"` - Connected bool `json:"connected" yaml:"connected"` - Error string `json:"error" yaml:"error"` -} - -type relayStateOutputDetail struct { - URI string `json:"uri" yaml:"uri"` - Available bool `json:"available" yaml:"available"` - Error string `json:"error" yaml:"error"` -} - -type relayStateOutput struct { - Total int `json:"total" yaml:"total"` - Available int `json:"available" yaml:"available"` - Details []relayStateOutputDetail `json:"details" yaml:"details"` -} - -type iceCandidateType struct { - Local string `json:"local" yaml:"local"` - Remote string `json:"remote" yaml:"remote"` -} - -type nsServerGroupStateOutput struct { - Servers []string `json:"servers" yaml:"servers"` - Domains []string `json:"domains" yaml:"domains"` - Enabled bool `json:"enabled" yaml:"enabled"` - Error string `json:"error" yaml:"error"` -} - -type statusOutputOverview struct { - Peers peersStateOutput `json:"peers" yaml:"peers"` - CliVersion string `json:"cliVersion" yaml:"cliVersion"` - DaemonVersion string `json:"daemonVersion" yaml:"daemonVersion"` - ManagementState managementStateOutput `json:"management" yaml:"management"` - SignalState signalStateOutput `json:"signal" yaml:"signal"` - Relays relayStateOutput `json:"relays" yaml:"relays"` - IP string `json:"netbirdIp" yaml:"netbirdIp"` - PubKey string `json:"publicKey" yaml:"publicKey"` - KernelInterface bool `json:"usesKernelInterface" yaml:"usesKernelInterface"` - FQDN string `json:"fqdn" yaml:"fqdn"` - RosenpassEnabled bool `json:"quantumResistance" yaml:"quantumResistance"` - RosenpassPermissive bool `json:"quantumResistancePermissive" yaml:"quantumResistancePermissive"` - Networks []string `json:"networks" yaml:"networks"` - NSServerGroups []nsServerGroupStateOutput `json:"dnsServers" yaml:"dnsServers"` - Events []systemEventOutput `json:"events" yaml:"events"` -} - var ( detailFlag bool ipv4Flag bool @@ -172,18 +86,17 @@ func statusFunc(cmd *cobra.Command, args []string) error { return nil } - outputInformationHolder := convertToStatusOutputOverview(resp) - + var outputInformationHolder = nbstatus.ConvertToStatusOutputOverview(resp, anonymizeFlag, statusFilter, prefixNamesFilter, prefixNamesFilterMap, ipsFilterMap) var statusOutputString string switch { case detailFlag: - statusOutputString = parseToFullDetailSummary(outputInformationHolder) + statusOutputString = nbstatus.ParseToFullDetailSummary(outputInformationHolder) case jsonFlag: - statusOutputString, err = parseToJSON(outputInformationHolder) + statusOutputString, err = nbstatus.ParseToJSON(outputInformationHolder) case yamlFlag: - statusOutputString, err = parseToYAML(outputInformationHolder) + statusOutputString, err = nbstatus.ParseToYAML(outputInformationHolder) default: - statusOutputString = parseGeneralSummary(outputInformationHolder, false, false, false) + statusOutputString = nbstatus.ParseGeneralSummary(outputInformationHolder, false, false, false) } if err != nil { @@ -213,7 +126,6 @@ func getStatus(ctx context.Context) (*proto.StatusResponse, error) { } func parseFilters() error { - switch strings.ToLower(statusFilter) { case "", "disconnected", "connected": if strings.ToLower(statusFilter) != "" { @@ -250,174 +162,6 @@ func enableDetailFlagWhenFilterFlag() { } } -func convertToStatusOutputOverview(resp *proto.StatusResponse) statusOutputOverview { - pbFullStatus := resp.GetFullStatus() - - managementState := pbFullStatus.GetManagementState() - managementOverview := managementStateOutput{ - URL: managementState.GetURL(), - Connected: managementState.GetConnected(), - Error: managementState.Error, - } - - signalState := pbFullStatus.GetSignalState() - signalOverview := signalStateOutput{ - URL: signalState.GetURL(), - Connected: signalState.GetConnected(), - Error: signalState.Error, - } - - relayOverview := mapRelays(pbFullStatus.GetRelays()) - peersOverview := mapPeers(resp.GetFullStatus().GetPeers()) - - overview := statusOutputOverview{ - Peers: peersOverview, - CliVersion: version.NetbirdVersion(), - DaemonVersion: resp.GetDaemonVersion(), - ManagementState: managementOverview, - SignalState: signalOverview, - Relays: relayOverview, - IP: pbFullStatus.GetLocalPeerState().GetIP(), - PubKey: pbFullStatus.GetLocalPeerState().GetPubKey(), - KernelInterface: pbFullStatus.GetLocalPeerState().GetKernelInterface(), - FQDN: pbFullStatus.GetLocalPeerState().GetFqdn(), - RosenpassEnabled: pbFullStatus.GetLocalPeerState().GetRosenpassEnabled(), - RosenpassPermissive: pbFullStatus.GetLocalPeerState().GetRosenpassPermissive(), - Networks: pbFullStatus.GetLocalPeerState().GetNetworks(), - NSServerGroups: mapNSGroups(pbFullStatus.GetDnsServers()), - Events: mapEvents(pbFullStatus.GetEvents()), - } - - if anonymizeFlag { - anonymizer := anonymize.NewAnonymizer(anonymize.DefaultAddresses()) - anonymizeOverview(anonymizer, &overview) - } - - return overview -} - -func mapRelays(relays []*proto.RelayState) relayStateOutput { - var relayStateDetail []relayStateOutputDetail - - var relaysAvailable int - for _, relay := range relays { - available := relay.GetAvailable() - relayStateDetail = append(relayStateDetail, - relayStateOutputDetail{ - URI: relay.URI, - Available: available, - Error: relay.GetError(), - }, - ) - - if available { - relaysAvailable++ - } - } - - return relayStateOutput{ - Total: len(relays), - Available: relaysAvailable, - Details: relayStateDetail, - } -} - -func mapNSGroups(servers []*proto.NSGroupState) []nsServerGroupStateOutput { - mappedNSGroups := make([]nsServerGroupStateOutput, 0, len(servers)) - for _, pbNsGroupServer := range servers { - mappedNSGroups = append(mappedNSGroups, nsServerGroupStateOutput{ - Servers: pbNsGroupServer.GetServers(), - Domains: pbNsGroupServer.GetDomains(), - Enabled: pbNsGroupServer.GetEnabled(), - Error: pbNsGroupServer.GetError(), - }) - } - return mappedNSGroups -} - -func mapPeers(peers []*proto.PeerState) peersStateOutput { - var peersStateDetail []peerStateDetailOutput - peersConnected := 0 - for _, pbPeerState := range peers { - localICE := "" - remoteICE := "" - localICEEndpoint := "" - remoteICEEndpoint := "" - relayServerAddress := "" - connType := "" - lastHandshake := time.Time{} - transferReceived := int64(0) - transferSent := int64(0) - - isPeerConnected := pbPeerState.ConnStatus == peer.StatusConnected.String() - if skipDetailByFilters(pbPeerState, isPeerConnected) { - continue - } - if isPeerConnected { - peersConnected++ - - localICE = pbPeerState.GetLocalIceCandidateType() - remoteICE = pbPeerState.GetRemoteIceCandidateType() - localICEEndpoint = pbPeerState.GetLocalIceCandidateEndpoint() - remoteICEEndpoint = pbPeerState.GetRemoteIceCandidateEndpoint() - connType = "P2P" - if pbPeerState.Relayed { - connType = "Relayed" - } - relayServerAddress = pbPeerState.GetRelayAddress() - lastHandshake = pbPeerState.GetLastWireguardHandshake().AsTime().Local() - transferReceived = pbPeerState.GetBytesRx() - transferSent = pbPeerState.GetBytesTx() - } - - timeLocal := pbPeerState.GetConnStatusUpdate().AsTime().Local() - peerState := peerStateDetailOutput{ - IP: pbPeerState.GetIP(), - PubKey: pbPeerState.GetPubKey(), - Status: pbPeerState.GetConnStatus(), - LastStatusUpdate: timeLocal, - ConnType: connType, - IceCandidateType: iceCandidateType{ - Local: localICE, - Remote: remoteICE, - }, - IceCandidateEndpoint: iceCandidateType{ - Local: localICEEndpoint, - Remote: remoteICEEndpoint, - }, - RelayAddress: relayServerAddress, - FQDN: pbPeerState.GetFqdn(), - LastWireguardHandshake: lastHandshake, - TransferReceived: transferReceived, - TransferSent: transferSent, - Latency: pbPeerState.GetLatency().AsDuration(), - RosenpassEnabled: pbPeerState.GetRosenpassEnabled(), - Networks: pbPeerState.GetNetworks(), - } - - peersStateDetail = append(peersStateDetail, peerState) - } - - sortPeersByIP(peersStateDetail) - - peersOverview := peersStateOutput{ - Total: len(peersStateDetail), - Connected: peersConnected, - Details: peersStateDetail, - } - return peersOverview -} - -func sortPeersByIP(peersStateDetail []peerStateDetailOutput) { - if len(peersStateDetail) > 0 { - sort.SliceStable(peersStateDetail, func(i, j int) bool { - iAddr, _ := netip.ParseAddr(peersStateDetail[i].IP) - jAddr, _ := netip.ParseAddr(peersStateDetail[j].IP) - return iAddr.Compare(jAddr) == -1 - }) - } -} - func parseInterfaceIP(interfaceIP string) string { ip, _, err := net.ParseCIDR(interfaceIP) if err != nil { @@ -425,449 +169,3 @@ func parseInterfaceIP(interfaceIP string) string { } return fmt.Sprintf("%s\n", ip) } - -func parseToJSON(overview statusOutputOverview) (string, error) { - jsonBytes, err := json.Marshal(overview) - if err != nil { - return "", fmt.Errorf("json marshal failed") - } - return string(jsonBytes), err -} - -func parseToYAML(overview statusOutputOverview) (string, error) { - yamlBytes, err := yaml.Marshal(overview) - if err != nil { - return "", fmt.Errorf("yaml marshal failed") - } - return string(yamlBytes), nil -} - -func parseGeneralSummary(overview statusOutputOverview, showURL bool, showRelays bool, showNameServers bool) string { - var managementConnString string - if overview.ManagementState.Connected { - managementConnString = "Connected" - if showURL { - managementConnString = fmt.Sprintf("%s to %s", managementConnString, overview.ManagementState.URL) - } - } else { - managementConnString = "Disconnected" - if overview.ManagementState.Error != "" { - managementConnString = fmt.Sprintf("%s, reason: %s", managementConnString, overview.ManagementState.Error) - } - } - - var signalConnString string - if overview.SignalState.Connected { - signalConnString = "Connected" - if showURL { - signalConnString = fmt.Sprintf("%s to %s", signalConnString, overview.SignalState.URL) - } - } else { - signalConnString = "Disconnected" - if overview.SignalState.Error != "" { - signalConnString = fmt.Sprintf("%s, reason: %s", signalConnString, overview.SignalState.Error) - } - } - - interfaceTypeString := "Userspace" - interfaceIP := overview.IP - if overview.KernelInterface { - interfaceTypeString = "Kernel" - } else if overview.IP == "" { - interfaceTypeString = "N/A" - interfaceIP = "N/A" - } - - var relaysString string - if showRelays { - for _, relay := range overview.Relays.Details { - available := "Available" - reason := "" - if !relay.Available { - available = "Unavailable" - reason = fmt.Sprintf(", reason: %s", relay.Error) - } - relaysString += fmt.Sprintf("\n [%s] is %s%s", relay.URI, available, reason) - } - } else { - relaysString = fmt.Sprintf("%d/%d Available", overview.Relays.Available, overview.Relays.Total) - } - - networks := "-" - if len(overview.Networks) > 0 { - sort.Strings(overview.Networks) - networks = strings.Join(overview.Networks, ", ") - } - - var dnsServersString string - if showNameServers { - for _, nsServerGroup := range overview.NSServerGroups { - enabled := "Available" - if !nsServerGroup.Enabled { - enabled = "Unavailable" - } - errorString := "" - if nsServerGroup.Error != "" { - errorString = fmt.Sprintf(", reason: %s", nsServerGroup.Error) - errorString = strings.TrimSpace(errorString) - } - - domainsString := strings.Join(nsServerGroup.Domains, ", ") - if domainsString == "" { - domainsString = "." // Show "." for the default zone - } - dnsServersString += fmt.Sprintf( - "\n [%s] for [%s] is %s%s", - strings.Join(nsServerGroup.Servers, ", "), - domainsString, - enabled, - errorString, - ) - } - } else { - dnsServersString = fmt.Sprintf("%d/%d Available", countEnabled(overview.NSServerGroups), len(overview.NSServerGroups)) - } - - rosenpassEnabledStatus := "false" - if overview.RosenpassEnabled { - rosenpassEnabledStatus = "true" - if overview.RosenpassPermissive { - rosenpassEnabledStatus = "true (permissive)" //nolint:gosec - } - } - - peersCountString := fmt.Sprintf("%d/%d Connected", overview.Peers.Connected, overview.Peers.Total) - - goos := runtime.GOOS - goarch := runtime.GOARCH - goarm := "" - if goarch == "arm" { - goarm = fmt.Sprintf(" (ARMv%s)", os.Getenv("GOARM")) - } - - summary := fmt.Sprintf( - "OS: %s\n"+ - "Daemon version: %s\n"+ - "CLI version: %s\n"+ - "Management: %s\n"+ - "Signal: %s\n"+ - "Relays: %s\n"+ - "Nameservers: %s\n"+ - "FQDN: %s\n"+ - "NetBird IP: %s\n"+ - "Interface type: %s\n"+ - "Quantum resistance: %s\n"+ - "Networks: %s\n"+ - "Peers count: %s\n", - fmt.Sprintf("%s/%s%s", goos, goarch, goarm), - overview.DaemonVersion, - version.NetbirdVersion(), - managementConnString, - signalConnString, - relaysString, - dnsServersString, - overview.FQDN, - interfaceIP, - interfaceTypeString, - rosenpassEnabledStatus, - networks, - peersCountString, - ) - return summary -} - -func parseToFullDetailSummary(overview statusOutputOverview) string { - parsedPeersString := parsePeers(overview.Peers, overview.RosenpassEnabled, overview.RosenpassPermissive) - parsedEventsString := parseEvents(overview.Events) - summary := parseGeneralSummary(overview, true, true, true) - - return fmt.Sprintf( - "Peers detail:"+ - "%s\n"+ - "Events:"+ - "%s\n"+ - "%s", - parsedPeersString, - parsedEventsString, - summary, - ) -} - -func parsePeers(peers peersStateOutput, rosenpassEnabled, rosenpassPermissive bool) string { - var ( - peersString = "" - ) - - for _, peerState := range peers.Details { - - localICE := "-" - if peerState.IceCandidateType.Local != "" { - localICE = peerState.IceCandidateType.Local - } - - remoteICE := "-" - if peerState.IceCandidateType.Remote != "" { - remoteICE = peerState.IceCandidateType.Remote - } - - localICEEndpoint := "-" - if peerState.IceCandidateEndpoint.Local != "" { - localICEEndpoint = peerState.IceCandidateEndpoint.Local - } - - remoteICEEndpoint := "-" - if peerState.IceCandidateEndpoint.Remote != "" { - remoteICEEndpoint = peerState.IceCandidateEndpoint.Remote - } - - rosenpassEnabledStatus := "false" - if rosenpassEnabled { - if peerState.RosenpassEnabled { - rosenpassEnabledStatus = "true" - } else { - if rosenpassPermissive { - rosenpassEnabledStatus = "false (remote didn't enable quantum resistance)" - } else { - rosenpassEnabledStatus = "false (connection won't work without a permissive mode)" - } - } - } else { - if peerState.RosenpassEnabled { - rosenpassEnabledStatus = "false (connection might not work without a remote permissive mode)" - } - } - - networks := "-" - if len(peerState.Networks) > 0 { - sort.Strings(peerState.Networks) - networks = strings.Join(peerState.Networks, ", ") - } - - peerString := fmt.Sprintf( - "\n %s:\n"+ - " NetBird IP: %s\n"+ - " Public key: %s\n"+ - " Status: %s\n"+ - " -- detail --\n"+ - " Connection type: %s\n"+ - " ICE candidate (Local/Remote): %s/%s\n"+ - " ICE candidate endpoints (Local/Remote): %s/%s\n"+ - " Relay server address: %s\n"+ - " Last connection update: %s\n"+ - " Last WireGuard handshake: %s\n"+ - " Transfer status (received/sent) %s/%s\n"+ - " Quantum resistance: %s\n"+ - " Networks: %s\n"+ - " Latency: %s\n", - peerState.FQDN, - peerState.IP, - peerState.PubKey, - peerState.Status, - peerState.ConnType, - localICE, - remoteICE, - localICEEndpoint, - remoteICEEndpoint, - peerState.RelayAddress, - timeAgo(peerState.LastStatusUpdate), - timeAgo(peerState.LastWireguardHandshake), - toIEC(peerState.TransferReceived), - toIEC(peerState.TransferSent), - rosenpassEnabledStatus, - networks, - peerState.Latency.String(), - ) - - peersString += peerString - } - return peersString -} - -func skipDetailByFilters(peerState *proto.PeerState, isConnected bool) bool { - statusEval := false - ipEval := false - nameEval := true - - if statusFilter != "" { - lowerStatusFilter := strings.ToLower(statusFilter) - if lowerStatusFilter == "disconnected" && isConnected { - statusEval = true - } else if lowerStatusFilter == "connected" && !isConnected { - statusEval = true - } - } - - if len(ipsFilter) > 0 { - _, ok := ipsFilterMap[peerState.IP] - if !ok { - ipEval = true - } - } - - if len(prefixNamesFilter) > 0 { - for prefixNameFilter := range prefixNamesFilterMap { - if strings.HasPrefix(peerState.Fqdn, prefixNameFilter) { - nameEval = false - break - } - } - } else { - nameEval = false - } - - return statusEval || ipEval || nameEval -} - -func toIEC(b int64) string { - const unit = 1024 - if b < unit { - return fmt.Sprintf("%d B", b) - } - div, exp := int64(unit), 0 - for n := b / unit; n >= unit; n /= unit { - div *= unit - exp++ - } - return fmt.Sprintf("%.1f %ciB", - float64(b)/float64(div), "KMGTPE"[exp]) -} - -func countEnabled(dnsServers []nsServerGroupStateOutput) int { - count := 0 - for _, server := range dnsServers { - if server.Enabled { - count++ - } - } - return count -} - -// timeAgo returns a string representing the duration since the provided time in a human-readable format. -func timeAgo(t time.Time) string { - if t.IsZero() || t.Equal(time.Unix(0, 0)) { - return "-" - } - duration := time.Since(t) - switch { - case duration < time.Second: - return "Now" - case duration < time.Minute: - seconds := int(duration.Seconds()) - if seconds == 1 { - return "1 second ago" - } - return fmt.Sprintf("%d seconds ago", seconds) - case duration < time.Hour: - minutes := int(duration.Minutes()) - seconds := int(duration.Seconds()) % 60 - if minutes == 1 { - if seconds == 1 { - return "1 minute, 1 second ago" - } else if seconds > 0 { - return fmt.Sprintf("1 minute, %d seconds ago", seconds) - } - return "1 minute ago" - } - if seconds > 0 { - return fmt.Sprintf("%d minutes, %d seconds ago", minutes, seconds) - } - return fmt.Sprintf("%d minutes ago", minutes) - case duration < 24*time.Hour: - hours := int(duration.Hours()) - minutes := int(duration.Minutes()) % 60 - if hours == 1 { - if minutes == 1 { - return "1 hour, 1 minute ago" - } else if minutes > 0 { - return fmt.Sprintf("1 hour, %d minutes ago", minutes) - } - return "1 hour ago" - } - if minutes > 0 { - return fmt.Sprintf("%d hours, %d minutes ago", hours, minutes) - } - return fmt.Sprintf("%d hours ago", hours) - } - - days := int(duration.Hours()) / 24 - hours := int(duration.Hours()) % 24 - if days == 1 { - if hours == 1 { - return "1 day, 1 hour ago" - } else if hours > 0 { - return fmt.Sprintf("1 day, %d hours ago", hours) - } - return "1 day ago" - } - if hours > 0 { - return fmt.Sprintf("%d days, %d hours ago", days, hours) - } - return fmt.Sprintf("%d days ago", days) -} - -func anonymizePeerDetail(a *anonymize.Anonymizer, peer *peerStateDetailOutput) { - peer.FQDN = a.AnonymizeDomain(peer.FQDN) - if localIP, port, err := net.SplitHostPort(peer.IceCandidateEndpoint.Local); err == nil { - peer.IceCandidateEndpoint.Local = fmt.Sprintf("%s:%s", a.AnonymizeIPString(localIP), port) - } - if remoteIP, port, err := net.SplitHostPort(peer.IceCandidateEndpoint.Remote); err == nil { - peer.IceCandidateEndpoint.Remote = fmt.Sprintf("%s:%s", a.AnonymizeIPString(remoteIP), port) - } - - peer.RelayAddress = a.AnonymizeURI(peer.RelayAddress) - - for i, route := range peer.Networks { - peer.Networks[i] = a.AnonymizeIPString(route) - } - - for i, route := range peer.Networks { - peer.Networks[i] = a.AnonymizeRoute(route) - } -} - -func anonymizeOverview(a *anonymize.Anonymizer, overview *statusOutputOverview) { - for i, peer := range overview.Peers.Details { - peer := peer - anonymizePeerDetail(a, &peer) - overview.Peers.Details[i] = peer - } - - overview.ManagementState.URL = a.AnonymizeURI(overview.ManagementState.URL) - overview.ManagementState.Error = a.AnonymizeString(overview.ManagementState.Error) - overview.SignalState.URL = a.AnonymizeURI(overview.SignalState.URL) - overview.SignalState.Error = a.AnonymizeString(overview.SignalState.Error) - - overview.IP = a.AnonymizeIPString(overview.IP) - for i, detail := range overview.Relays.Details { - detail.URI = a.AnonymizeURI(detail.URI) - detail.Error = a.AnonymizeString(detail.Error) - overview.Relays.Details[i] = detail - } - - for i, nsGroup := range overview.NSServerGroups { - for j, domain := range nsGroup.Domains { - overview.NSServerGroups[i].Domains[j] = a.AnonymizeDomain(domain) - } - for j, ns := range nsGroup.Servers { - host, port, err := net.SplitHostPort(ns) - if err == nil { - overview.NSServerGroups[i].Servers[j] = fmt.Sprintf("%s:%s", a.AnonymizeIPString(host), port) - } - } - } - - for i, route := range overview.Networks { - overview.Networks[i] = a.AnonymizeRoute(route) - } - - overview.FQDN = a.AnonymizeDomain(overview.FQDN) - - for i, event := range overview.Events { - overview.Events[i].Message = a.AnonymizeString(event.Message) - overview.Events[i].UserMessage = a.AnonymizeString(event.UserMessage) - - for k, v := range event.Metadata { - event.Metadata[k] = a.AnonymizeString(v) - } - } -} diff --git a/client/cmd/status_test.go b/client/cmd/status_test.go index 1e240d192..03608eab0 100644 --- a/client/cmd/status_test.go +++ b/client/cmd/status_test.go @@ -1,579 +1,11 @@ package cmd import ( - "bytes" - "encoding/json" - "fmt" - "runtime" "testing" - "time" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "google.golang.org/protobuf/types/known/durationpb" - "google.golang.org/protobuf/types/known/timestamppb" - - "github.com/netbirdio/netbird/client/proto" - "github.com/netbirdio/netbird/version" ) -func init() { - loc, err := time.LoadLocation("UTC") - if err != nil { - panic(err) - } - - time.Local = loc -} - -var resp = &proto.StatusResponse{ - Status: "Connected", - FullStatus: &proto.FullStatus{ - Peers: []*proto.PeerState{ - { - IP: "192.168.178.101", - PubKey: "Pubkey1", - Fqdn: "peer-1.awesome-domain.com", - ConnStatus: "Connected", - ConnStatusUpdate: timestamppb.New(time.Date(2001, time.Month(1), 1, 1, 1, 1, 0, time.UTC)), - Relayed: false, - LocalIceCandidateType: "", - RemoteIceCandidateType: "", - LocalIceCandidateEndpoint: "", - RemoteIceCandidateEndpoint: "", - LastWireguardHandshake: timestamppb.New(time.Date(2001, time.Month(1), 1, 1, 1, 2, 0, time.UTC)), - BytesRx: 200, - BytesTx: 100, - Networks: []string{ - "10.1.0.0/24", - }, - Latency: durationpb.New(time.Duration(10000000)), - }, - { - IP: "192.168.178.102", - PubKey: "Pubkey2", - Fqdn: "peer-2.awesome-domain.com", - ConnStatus: "Connected", - ConnStatusUpdate: timestamppb.New(time.Date(2002, time.Month(2), 2, 2, 2, 2, 0, time.UTC)), - Relayed: true, - LocalIceCandidateType: "relay", - RemoteIceCandidateType: "prflx", - LocalIceCandidateEndpoint: "10.0.0.1:10001", - RemoteIceCandidateEndpoint: "10.0.10.1:10002", - LastWireguardHandshake: timestamppb.New(time.Date(2002, time.Month(2), 2, 2, 2, 3, 0, time.UTC)), - BytesRx: 2000, - BytesTx: 1000, - Latency: durationpb.New(time.Duration(10000000)), - }, - }, - ManagementState: &proto.ManagementState{ - URL: "my-awesome-management.com:443", - Connected: true, - Error: "", - }, - SignalState: &proto.SignalState{ - URL: "my-awesome-signal.com:443", - Connected: true, - Error: "", - }, - Relays: []*proto.RelayState{ - { - URI: "stun:my-awesome-stun.com:3478", - Available: true, - Error: "", - }, - { - URI: "turns:my-awesome-turn.com:443?transport=tcp", - Available: false, - Error: "context: deadline exceeded", - }, - }, - LocalPeerState: &proto.LocalPeerState{ - IP: "192.168.178.100/16", - PubKey: "Some-Pub-Key", - KernelInterface: true, - Fqdn: "some-localhost.awesome-domain.com", - Networks: []string{ - "10.10.0.0/24", - }, - }, - DnsServers: []*proto.NSGroupState{ - { - Servers: []string{ - "8.8.8.8:53", - }, - Domains: nil, - Enabled: true, - Error: "", - }, - { - Servers: []string{ - "1.1.1.1:53", - "2.2.2.2:53", - }, - Domains: []string{ - "example.com", - "example.net", - }, - Enabled: false, - Error: "timeout", - }, - }, - }, - DaemonVersion: "0.14.1", -} - -var overview = statusOutputOverview{ - Peers: peersStateOutput{ - Total: 2, - Connected: 2, - Details: []peerStateDetailOutput{ - { - IP: "192.168.178.101", - PubKey: "Pubkey1", - FQDN: "peer-1.awesome-domain.com", - Status: "Connected", - LastStatusUpdate: time.Date(2001, 1, 1, 1, 1, 1, 0, time.UTC), - ConnType: "P2P", - IceCandidateType: iceCandidateType{ - Local: "", - Remote: "", - }, - IceCandidateEndpoint: iceCandidateType{ - Local: "", - Remote: "", - }, - LastWireguardHandshake: time.Date(2001, 1, 1, 1, 1, 2, 0, time.UTC), - TransferReceived: 200, - TransferSent: 100, - Networks: []string{ - "10.1.0.0/24", - }, - Latency: time.Duration(10000000), - }, - { - IP: "192.168.178.102", - PubKey: "Pubkey2", - FQDN: "peer-2.awesome-domain.com", - Status: "Connected", - LastStatusUpdate: time.Date(2002, 2, 2, 2, 2, 2, 0, time.UTC), - ConnType: "Relayed", - IceCandidateType: iceCandidateType{ - Local: "relay", - Remote: "prflx", - }, - IceCandidateEndpoint: iceCandidateType{ - Local: "10.0.0.1:10001", - Remote: "10.0.10.1:10002", - }, - LastWireguardHandshake: time.Date(2002, 2, 2, 2, 2, 3, 0, time.UTC), - TransferReceived: 2000, - TransferSent: 1000, - Latency: time.Duration(10000000), - }, - }, - }, - Events: []systemEventOutput{}, - CliVersion: version.NetbirdVersion(), - DaemonVersion: "0.14.1", - ManagementState: managementStateOutput{ - URL: "my-awesome-management.com:443", - Connected: true, - Error: "", - }, - SignalState: signalStateOutput{ - URL: "my-awesome-signal.com:443", - Connected: true, - Error: "", - }, - Relays: relayStateOutput{ - Total: 2, - Available: 1, - Details: []relayStateOutputDetail{ - { - URI: "stun:my-awesome-stun.com:3478", - Available: true, - Error: "", - }, - { - URI: "turns:my-awesome-turn.com:443?transport=tcp", - Available: false, - Error: "context: deadline exceeded", - }, - }, - }, - IP: "192.168.178.100/16", - PubKey: "Some-Pub-Key", - KernelInterface: true, - FQDN: "some-localhost.awesome-domain.com", - NSServerGroups: []nsServerGroupStateOutput{ - { - Servers: []string{ - "8.8.8.8:53", - }, - Domains: nil, - Enabled: true, - Error: "", - }, - { - Servers: []string{ - "1.1.1.1:53", - "2.2.2.2:53", - }, - Domains: []string{ - "example.com", - "example.net", - }, - Enabled: false, - Error: "timeout", - }, - }, - Networks: []string{ - "10.10.0.0/24", - }, -} - -func TestConversionFromFullStatusToOutputOverview(t *testing.T) { - convertedResult := convertToStatusOutputOverview(resp) - - assert.Equal(t, overview, convertedResult) -} - -func TestSortingOfPeers(t *testing.T) { - peers := []peerStateDetailOutput{ - { - IP: "192.168.178.104", - }, - { - IP: "192.168.178.102", - }, - { - IP: "192.168.178.101", - }, - { - IP: "192.168.178.105", - }, - { - IP: "192.168.178.103", - }, - } - - sortPeersByIP(peers) - - assert.Equal(t, peers[3].IP, "192.168.178.104") -} - -func TestParsingToJSON(t *testing.T) { - jsonString, _ := parseToJSON(overview) - - //@formatter:off - expectedJSONString := ` - { - "peers": { - "total": 2, - "connected": 2, - "details": [ - { - "fqdn": "peer-1.awesome-domain.com", - "netbirdIp": "192.168.178.101", - "publicKey": "Pubkey1", - "status": "Connected", - "lastStatusUpdate": "2001-01-01T01:01:01Z", - "connectionType": "P2P", - "iceCandidateType": { - "local": "", - "remote": "" - }, - "iceCandidateEndpoint": { - "local": "", - "remote": "" - }, - "relayAddress": "", - "lastWireguardHandshake": "2001-01-01T01:01:02Z", - "transferReceived": 200, - "transferSent": 100, - "latency": 10000000, - "quantumResistance": false, - "networks": [ - "10.1.0.0/24" - ] - }, - { - "fqdn": "peer-2.awesome-domain.com", - "netbirdIp": "192.168.178.102", - "publicKey": "Pubkey2", - "status": "Connected", - "lastStatusUpdate": "2002-02-02T02:02:02Z", - "connectionType": "Relayed", - "iceCandidateType": { - "local": "relay", - "remote": "prflx" - }, - "iceCandidateEndpoint": { - "local": "10.0.0.1:10001", - "remote": "10.0.10.1:10002" - }, - "relayAddress": "", - "lastWireguardHandshake": "2002-02-02T02:02:03Z", - "transferReceived": 2000, - "transferSent": 1000, - "latency": 10000000, - "quantumResistance": false, - "networks": null - } - ] - }, - "cliVersion": "development", - "daemonVersion": "0.14.1", - "management": { - "url": "my-awesome-management.com:443", - "connected": true, - "error": "" - }, - "signal": { - "url": "my-awesome-signal.com:443", - "connected": true, - "error": "" - }, - "relays": { - "total": 2, - "available": 1, - "details": [ - { - "uri": "stun:my-awesome-stun.com:3478", - "available": true, - "error": "" - }, - { - "uri": "turns:my-awesome-turn.com:443?transport=tcp", - "available": false, - "error": "context: deadline exceeded" - } - ] - }, - "netbirdIp": "192.168.178.100/16", - "publicKey": "Some-Pub-Key", - "usesKernelInterface": true, - "fqdn": "some-localhost.awesome-domain.com", - "quantumResistance": false, - "quantumResistancePermissive": false, - "networks": [ - "10.10.0.0/24" - ], - "dnsServers": [ - { - "servers": [ - "8.8.8.8:53" - ], - "domains": null, - "enabled": true, - "error": "" - }, - { - "servers": [ - "1.1.1.1:53", - "2.2.2.2:53" - ], - "domains": [ - "example.com", - "example.net" - ], - "enabled": false, - "error": "timeout" - } - ], - "events": [] - }` - // @formatter:on - - var expectedJSON bytes.Buffer - require.NoError(t, json.Compact(&expectedJSON, []byte(expectedJSONString))) - - assert.Equal(t, expectedJSON.String(), jsonString) -} - -func TestParsingToYAML(t *testing.T) { - yaml, _ := parseToYAML(overview) - - expectedYAML := - `peers: - total: 2 - connected: 2 - details: - - fqdn: peer-1.awesome-domain.com - netbirdIp: 192.168.178.101 - publicKey: Pubkey1 - status: Connected - lastStatusUpdate: 2001-01-01T01:01:01Z - connectionType: P2P - iceCandidateType: - local: "" - remote: "" - iceCandidateEndpoint: - local: "" - remote: "" - relayAddress: "" - lastWireguardHandshake: 2001-01-01T01:01:02Z - transferReceived: 200 - transferSent: 100 - latency: 10ms - quantumResistance: false - networks: - - 10.1.0.0/24 - - fqdn: peer-2.awesome-domain.com - netbirdIp: 192.168.178.102 - publicKey: Pubkey2 - status: Connected - lastStatusUpdate: 2002-02-02T02:02:02Z - connectionType: Relayed - iceCandidateType: - local: relay - remote: prflx - iceCandidateEndpoint: - local: 10.0.0.1:10001 - remote: 10.0.10.1:10002 - relayAddress: "" - lastWireguardHandshake: 2002-02-02T02:02:03Z - transferReceived: 2000 - transferSent: 1000 - latency: 10ms - quantumResistance: false - networks: [] -cliVersion: development -daemonVersion: 0.14.1 -management: - url: my-awesome-management.com:443 - connected: true - error: "" -signal: - url: my-awesome-signal.com:443 - connected: true - error: "" -relays: - total: 2 - available: 1 - details: - - uri: stun:my-awesome-stun.com:3478 - available: true - error: "" - - uri: turns:my-awesome-turn.com:443?transport=tcp - available: false - error: 'context: deadline exceeded' -netbirdIp: 192.168.178.100/16 -publicKey: Some-Pub-Key -usesKernelInterface: true -fqdn: some-localhost.awesome-domain.com -quantumResistance: false -quantumResistancePermissive: false -networks: - - 10.10.0.0/24 -dnsServers: - - servers: - - 8.8.8.8:53 - domains: [] - enabled: true - error: "" - - servers: - - 1.1.1.1:53 - - 2.2.2.2:53 - domains: - - example.com - - example.net - enabled: false - error: timeout -events: [] -` - - assert.Equal(t, expectedYAML, yaml) -} - -func TestParsingToDetail(t *testing.T) { - // Calculate time ago based on the fixture dates - lastConnectionUpdate1 := timeAgo(overview.Peers.Details[0].LastStatusUpdate) - lastHandshake1 := timeAgo(overview.Peers.Details[0].LastWireguardHandshake) - lastConnectionUpdate2 := timeAgo(overview.Peers.Details[1].LastStatusUpdate) - lastHandshake2 := timeAgo(overview.Peers.Details[1].LastWireguardHandshake) - - detail := parseToFullDetailSummary(overview) - - expectedDetail := fmt.Sprintf( - `Peers detail: - peer-1.awesome-domain.com: - NetBird IP: 192.168.178.101 - Public key: Pubkey1 - Status: Connected - -- detail -- - Connection type: P2P - ICE candidate (Local/Remote): -/- - ICE candidate endpoints (Local/Remote): -/- - Relay server address: - Last connection update: %s - Last WireGuard handshake: %s - Transfer status (received/sent) 200 B/100 B - Quantum resistance: false - Networks: 10.1.0.0/24 - Latency: 10ms - - peer-2.awesome-domain.com: - NetBird IP: 192.168.178.102 - Public key: Pubkey2 - Status: Connected - -- detail -- - Connection type: Relayed - ICE candidate (Local/Remote): relay/prflx - ICE candidate endpoints (Local/Remote): 10.0.0.1:10001/10.0.10.1:10002 - Relay server address: - Last connection update: %s - Last WireGuard handshake: %s - Transfer status (received/sent) 2.0 KiB/1000 B - Quantum resistance: false - Networks: - - Latency: 10ms - -Events: No events recorded -OS: %s/%s -Daemon version: 0.14.1 -CLI version: %s -Management: Connected to my-awesome-management.com:443 -Signal: Connected to my-awesome-signal.com:443 -Relays: - [stun:my-awesome-stun.com:3478] is Available - [turns:my-awesome-turn.com:443?transport=tcp] is Unavailable, reason: context: deadline exceeded -Nameservers: - [8.8.8.8:53] for [.] is Available - [1.1.1.1:53, 2.2.2.2:53] for [example.com, example.net] is Unavailable, reason: timeout -FQDN: some-localhost.awesome-domain.com -NetBird IP: 192.168.178.100/16 -Interface type: Kernel -Quantum resistance: false -Networks: 10.10.0.0/24 -Peers count: 2/2 Connected -`, lastConnectionUpdate1, lastHandshake1, lastConnectionUpdate2, lastHandshake2, runtime.GOOS, runtime.GOARCH, overview.CliVersion) - - assert.Equal(t, expectedDetail, detail) -} - -func TestParsingToShortVersion(t *testing.T) { - shortVersion := parseGeneralSummary(overview, false, false, false) - - expectedString := fmt.Sprintf("OS: %s/%s", runtime.GOOS, runtime.GOARCH) + ` -Daemon version: 0.14.1 -CLI version: development -Management: Connected -Signal: Connected -Relays: 1/2 Available -Nameservers: 1/2 Available -FQDN: some-localhost.awesome-domain.com -NetBird IP: 192.168.178.100/16 -Interface type: Kernel -Quantum resistance: false -Networks: 10.10.0.0/24 -Peers count: 2/2 Connected -` - - assert.Equal(t, expectedString, shortVersion) -} - func TestParsingOfIP(t *testing.T) { InterfaceIP := "192.168.178.123/16" @@ -581,31 +13,3 @@ func TestParsingOfIP(t *testing.T) { assert.Equal(t, "192.168.178.123\n", parsedIP) } - -func TestTimeAgo(t *testing.T) { - now := time.Now() - - cases := []struct { - name string - input time.Time - expected string - }{ - {"Now", now, "Now"}, - {"Seconds ago", now.Add(-10 * time.Second), "10 seconds ago"}, - {"One minute ago", now.Add(-1 * time.Minute), "1 minute ago"}, - {"Minutes and seconds ago", now.Add(-(1*time.Minute + 30*time.Second)), "1 minute, 30 seconds ago"}, - {"One hour ago", now.Add(-1 * time.Hour), "1 hour ago"}, - {"Hours and minutes ago", now.Add(-(2*time.Hour + 15*time.Minute)), "2 hours, 15 minutes ago"}, - {"One day ago", now.Add(-24 * time.Hour), "1 day ago"}, - {"Multiple days ago", now.Add(-(72*time.Hour + 20*time.Minute)), "3 days ago"}, - {"Zero time", time.Time{}, "-"}, - {"Unix zero time", time.Unix(0, 0), "-"}, - } - - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - result := timeAgo(tc.input) - assert.Equal(t, tc.expected, result, "Failed %s", tc.name) - }) - } -} diff --git a/client/internal/connect.go b/client/internal/connect.go index 26ae3b687..bf513ed39 100644 --- a/client/internal/connect.go +++ b/client/internal/connect.go @@ -23,6 +23,7 @@ import ( "github.com/netbirdio/netbird/client/internal/listener" "github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/client/internal/stdnet" + cProto "github.com/netbirdio/netbird/client/proto" "github.com/netbirdio/netbird/client/ssh" "github.com/netbirdio/netbird/client/system" mgm "github.com/netbirdio/netbird/management/client" @@ -104,6 +105,16 @@ func (c *ConnectClient) RunOniOS( func (c *ConnectClient) run(mobileDependency MobileDependency, runningChan chan error) error { defer func() { if r := recover(); r != nil { + rec := c.statusRecorder + if rec != nil { + rec.PublishEvent( + cProto.SystemEvent_CRITICAL, cProto.SystemEvent_SYSTEM, + "panic occurred", + "The Netbird service panicked. Please restart the service and submit a bug report with the client logs.", + nil, + ) + } + log.Panicf("Panic occurred: %v, stack trace: %s", r, string(debug.Stack())) } }() diff --git a/client/internal/dns/file_unix.go b/client/internal/dns/file_unix.go index 02ae26e10..1f4ddb67c 100644 --- a/client/internal/dns/file_unix.go +++ b/client/internal/dns/file_unix.go @@ -58,7 +58,7 @@ func (f *fileConfigurator) applyDNSConfig(config HostDNSConfig, stateManager *st return fmt.Errorf("restoring the original resolv.conf file return err: %w", err) } } - return fmt.Errorf("unable to configure DNS for this peer using file manager without a nameserver group with all domains configured") + return ErrRouteAllWithoutNameserverGroup } if !backupFileExist { @@ -121,6 +121,10 @@ func (f *fileConfigurator) restoreHostDNS() error { return f.restore() } +func (f *fileConfigurator) string() string { + return "file" +} + func (f *fileConfigurator) backup() error { stats, err := os.Stat(defaultResolvConfPath) if err != nil { diff --git a/client/internal/dns/host.go b/client/internal/dns/host.go index cfc0cc3c3..25e9ff7e5 100644 --- a/client/internal/dns/host.go +++ b/client/internal/dns/host.go @@ -9,6 +9,8 @@ import ( nbdns "github.com/netbirdio/netbird/dns" ) +var ErrRouteAllWithoutNameserverGroup = fmt.Errorf("unable to configure DNS for this peer using file manager without a nameserver group with all domains configured") + const ( ipv4ReverseZone = ".in-addr.arpa" ipv6ReverseZone = ".ip6.arpa" @@ -18,6 +20,7 @@ type hostManager interface { applyDNSConfig(config HostDNSConfig, stateManager *statemanager.Manager) error restoreHostDNS() error supportCustomPort() bool + string() string } type SystemDNSSettings struct { @@ -44,6 +47,7 @@ type mockHostConfigurator struct { restoreHostDNSFunc func() error supportCustomPortFunc func() bool restoreUncleanShutdownDNSFunc func(*netip.Addr) error + stringFunc func() string } func (m *mockHostConfigurator) applyDNSConfig(config HostDNSConfig, stateManager *statemanager.Manager) error { @@ -67,6 +71,13 @@ func (m *mockHostConfigurator) supportCustomPort() bool { return false } +func (m *mockHostConfigurator) string() string { + if m.stringFunc != nil { + return m.stringFunc() + } + return "mock" +} + func newNoopHostMocker() hostManager { return &mockHostConfigurator{ applyDNSConfigFunc: func(config HostDNSConfig, stateManager *statemanager.Manager) error { return nil }, @@ -122,3 +133,7 @@ func (n noopHostConfigurator) restoreHostDNS() error { func (n noopHostConfigurator) supportCustomPort() bool { return true } + +func (n noopHostConfigurator) string() string { + return "noop" +} diff --git a/client/internal/dns/host_android.go b/client/internal/dns/host_android.go index 5653710d7..dfa3e5712 100644 --- a/client/internal/dns/host_android.go +++ b/client/internal/dns/host_android.go @@ -22,3 +22,7 @@ func (a androidHostManager) restoreHostDNS() error { func (a androidHostManager) supportCustomPort() bool { return false } + +func (a androidHostManager) string() string { + return "none" +} diff --git a/client/internal/dns/host_darwin.go b/client/internal/dns/host_darwin.go index 2f92dd367..f727f68b5 100644 --- a/client/internal/dns/host_darwin.go +++ b/client/internal/dns/host_darwin.go @@ -114,6 +114,10 @@ func (s *systemConfigurator) applyDNSConfig(config HostDNSConfig, stateManager * return nil } +func (s *systemConfigurator) string() string { + return "scutil" +} + func (s *systemConfigurator) restoreHostDNS() error { keys := s.getRemovableKeysWithDefaults() for _, key := range keys { diff --git a/client/internal/dns/host_ios.go b/client/internal/dns/host_ios.go index 4a0acf572..1c0ac63e9 100644 --- a/client/internal/dns/host_ios.go +++ b/client/internal/dns/host_ios.go @@ -38,3 +38,7 @@ func (a iosHostManager) restoreHostDNS() error { func (a iosHostManager) supportCustomPort() bool { return false } + +func (a iosHostManager) string() string { + return "none" +} diff --git a/client/internal/dns/host_windows.go b/client/internal/dns/host_windows.go index 58b0a14de..dceb24420 100644 --- a/client/internal/dns/host_windows.go +++ b/client/internal/dns/host_windows.go @@ -184,6 +184,10 @@ func (r *registryConfigurator) configureDNSPolicy(policyPath string, domains []s return nil } +func (r *registryConfigurator) string() string { + return "registry" +} + func (r *registryConfigurator) updateSearchDomains(domains []string) error { if err := r.setInterfaceRegistryKeyStringValue(interfaceConfigSearchListKey, strings.Join(domains, ",")); err != nil { return fmt.Errorf("update search domains: %w", err) diff --git a/client/internal/dns/network_manager_unix.go b/client/internal/dns/network_manager_unix.go index 63bbead77..10b4e6a6e 100644 --- a/client/internal/dns/network_manager_unix.go +++ b/client/internal/dns/network_manager_unix.go @@ -179,6 +179,10 @@ func (n *networkManagerDbusConfigurator) restoreHostDNS() error { return nil } +func (n *networkManagerDbusConfigurator) string() string { + return "network-manager" +} + func (n *networkManagerDbusConfigurator) getAppliedConnectionSettings() (networkManagerConnSettings, networkManagerConfigVersion, error) { obj, closeConn, err := getDbusObject(networkManagerDest, n.dbusLinkObject) if err != nil { diff --git a/client/internal/dns/resolvconf_unix.go b/client/internal/dns/resolvconf_unix.go index 6b5fdaf86..54c4c75bf 100644 --- a/client/internal/dns/resolvconf_unix.go +++ b/client/internal/dns/resolvconf_unix.go @@ -91,7 +91,7 @@ func (r *resolvconf) applyDNSConfig(config HostDNSConfig, stateManager *stateman if err != nil { log.Errorf("restore host dns: %s", err) } - return fmt.Errorf("unable to configure DNS for this peer using resolvconf manager without a nameserver group with all domains configured") + return ErrRouteAllWithoutNameserverGroup } searchDomainList := searchDomains(config) @@ -139,6 +139,10 @@ func (r *resolvconf) restoreHostDNS() error { return nil } +func (r *resolvconf) string() string { + return fmt.Sprintf("resolvconf (%s)", r.implType) +} + func (r *resolvconf) applyConfig(content bytes.Buffer) error { var cmd *exec.Cmd diff --git a/client/internal/dns/server.go b/client/internal/dns/server.go index f536a1434..bc87012f2 100644 --- a/client/internal/dns/server.go +++ b/client/internal/dns/server.go @@ -2,6 +2,7 @@ package dns import ( "context" + "errors" "fmt" "net/netip" "runtime" @@ -15,6 +16,7 @@ import ( "github.com/netbirdio/netbird/client/internal/listener" "github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/client/internal/statemanager" + cProto "github.com/netbirdio/netbird/client/proto" nbdns "github.com/netbirdio/netbird/dns" ) @@ -420,6 +422,7 @@ func (s *DefaultServer) applyConfiguration(update nbdns.Config) error { if err = s.hostManager.applyDNSConfig(hostUpdate, s.stateManager); err != nil { log.Error(err) + s.handleErrNoGroupaAll(err) } go func() { @@ -438,10 +441,26 @@ func (s *DefaultServer) applyConfiguration(update nbdns.Config) error { return nil } +func (s *DefaultServer) handleErrNoGroupaAll(err error) { + if !errors.Is(ErrRouteAllWithoutNameserverGroup, err) { + return + } + + if s.statusRecorder == nil { + return + } + + s.statusRecorder.PublishEvent( + cProto.SystemEvent_WARNING, cProto.SystemEvent_DNS, + "The host dns manager does not support match domains", + "The host dns manager does not support match domains without a catch-all nameserver group.", + map[string]string{"manager": s.hostManager.string()}, + ) +} + func (s *DefaultServer) buildLocalHandlerUpdate( customZones []nbdns.CustomZone, ) ([]handlerWrapper, map[string][]nbdns.SimpleRecord, error) { - var muxUpdates []handlerWrapper localRecords := make(map[string][]nbdns.SimpleRecord) @@ -672,6 +691,7 @@ func (s *DefaultServer) upstreamCallbacks( } if err := s.hostManager.applyDNSConfig(s.currentConfig, s.stateManager); err != nil { + s.handleErrNoGroupaAll(err) l.Errorf("Failed to apply nameserver deactivation on the host: %v", err) } @@ -710,6 +730,7 @@ func (s *DefaultServer) upstreamCallbacks( if s.hostManager != nil { if err := s.hostManager.applyDNSConfig(s.currentConfig, s.stateManager); err != nil { + s.handleErrNoGroupaAll(err) l.WithError(err).Error("reactivate temporary disabled nameserver group, DNS update apply") } } diff --git a/client/internal/dns/server_test.go b/client/internal/dns/server_test.go index 1354462d9..84779256f 100644 --- a/client/internal/dns/server_test.go +++ b/client/internal/dns/server_test.go @@ -356,7 +356,7 @@ func TestUpdateDNSServer(t *testing.T) { t.Log(err) } }() - dnsServer, err := NewDefaultServer(context.Background(), wgIface, "", &peer.Status{}, nil, false) + dnsServer, err := NewDefaultServer(context.Background(), wgIface, "", peer.NewRecorder("mgm"), nil, false) if err != nil { t.Fatal(err) } @@ -465,7 +465,7 @@ func TestDNSFakeResolverHandleUpdates(t *testing.T) { return } - dnsServer, err := NewDefaultServer(context.Background(), wgIface, "", &peer.Status{}, nil, false) + dnsServer, err := NewDefaultServer(context.Background(), wgIface, "", peer.NewRecorder("mgm"), nil, false) if err != nil { t.Errorf("create DNS server: %v", err) return @@ -566,7 +566,7 @@ func TestDNSServerStartStop(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { - dnsServer, err := NewDefaultServer(context.Background(), &mocWGIface{}, testCase.addrPort, &peer.Status{}, nil, false) + dnsServer, err := NewDefaultServer(context.Background(), &mocWGIface{}, testCase.addrPort, peer.NewRecorder("mgm"), nil, false) if err != nil { t.Fatalf("%v", err) } @@ -639,7 +639,7 @@ func TestDNSServerUpstreamDeactivateCallback(t *testing.T) { {false, "domain2", false}, }, }, - statusRecorder: &peer.Status{}, + statusRecorder: peer.NewRecorder("mgm"), } var domainsUpdate string @@ -700,7 +700,7 @@ func TestDNSPermanent_updateHostDNS_emptyUpstream(t *testing.T) { var dnsList []string dnsConfig := nbdns.Config{} - dnsServer := NewDefaultServerPermanentUpstream(context.Background(), wgIFace, dnsList, dnsConfig, nil, &peer.Status{}, false) + dnsServer := NewDefaultServerPermanentUpstream(context.Background(), wgIFace, dnsList, dnsConfig, nil, peer.NewRecorder("mgm"), false) err = dnsServer.Initialize() if err != nil { t.Errorf("failed to initialize DNS server: %v", err) @@ -724,7 +724,7 @@ func TestDNSPermanent_updateUpstream(t *testing.T) { } defer wgIFace.Close() dnsConfig := nbdns.Config{} - dnsServer := NewDefaultServerPermanentUpstream(context.Background(), wgIFace, []string{"8.8.8.8"}, dnsConfig, nil, &peer.Status{}, false) + dnsServer := NewDefaultServerPermanentUpstream(context.Background(), wgIFace, []string{"8.8.8.8"}, dnsConfig, nil, peer.NewRecorder("mgm"), false) err = dnsServer.Initialize() if err != nil { t.Errorf("failed to initialize DNS server: %v", err) @@ -816,7 +816,7 @@ func TestDNSPermanent_matchOnly(t *testing.T) { } defer wgIFace.Close() dnsConfig := nbdns.Config{} - dnsServer := NewDefaultServerPermanentUpstream(context.Background(), wgIFace, []string{"8.8.8.8"}, dnsConfig, nil, &peer.Status{}, false) + dnsServer := NewDefaultServerPermanentUpstream(context.Background(), wgIFace, []string{"8.8.8.8"}, dnsConfig, nil, peer.NewRecorder("mgm"), false) err = dnsServer.Initialize() if err != nil { t.Errorf("failed to initialize DNS server: %v", err) diff --git a/client/internal/dns/systemd_linux.go b/client/internal/dns/systemd_linux.go index a031be582..a87cc73e5 100644 --- a/client/internal/dns/systemd_linux.go +++ b/client/internal/dns/systemd_linux.go @@ -154,6 +154,10 @@ func (s *systemdDbusConfigurator) applyDNSConfig(config HostDNSConfig, stateMana return nil } +func (s *systemdDbusConfigurator) string() string { + return "dbus" +} + func (s *systemdDbusConfigurator) setDomainsForInterface(domainsInput []systemdDbusLinkDomainsInput) error { err := s.callLinkMethod(systemdDbusSetDomainsMethodSuffix, domainsInput) if err != nil { diff --git a/client/internal/dns/upstream.go b/client/internal/dns/upstream.go index d269107e3..a22689cf9 100644 --- a/client/internal/dns/upstream.go +++ b/client/internal/dns/upstream.go @@ -183,6 +183,19 @@ func (u *upstreamResolverBase) checkUpstreamFails(err error) { } u.disable(err) + + if u.statusRecorder == nil { + return + } + + u.statusRecorder.PublishEvent( + proto.SystemEvent_WARNING, + proto.SystemEvent_DNS, + "All upstream servers failed (fail count exceeded)", + "Unable to reach one or more DNS servers. This might affect your ability to connect to some services.", + map[string]string{"upstreams": strings.Join(u.upstreamServers, ", ")}, + // TODO add domain meta + ) } // probeAvailability tests all upstream servers simultaneously and @@ -232,10 +245,14 @@ func (u *upstreamResolverBase) probeAvailability() { if !success { u.disable(errors.ErrorOrNil()) + if u.statusRecorder == nil { + return + } + u.statusRecorder.PublishEvent( proto.SystemEvent_WARNING, proto.SystemEvent_DNS, - "All upstream servers failed", + "All upstream servers failed (probe failed)", "Unable to reach one or more DNS servers. This might affect your ability to connect to some services.", map[string]string{"upstreams": strings.Join(u.upstreamServers, ", ")}, ) diff --git a/client/internal/engine.go b/client/internal/engine.go index ebb68b98b..c939240d9 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -43,6 +43,7 @@ import ( "github.com/netbirdio/netbird/client/internal/routemanager" "github.com/netbirdio/netbird/client/internal/routemanager/systemops" "github.com/netbirdio/netbird/client/internal/statemanager" + cProto "github.com/netbirdio/netbird/client/proto" "github.com/netbirdio/netbird/management/domain" semaphoregroup "github.com/netbirdio/netbird/util/semaphore-group" @@ -673,6 +674,8 @@ func (e *Engine) handleSync(update *mgmProto.SyncResponse) error { return err } + e.statusRecorder.PublishEvent(cProto.SystemEvent_INFO, cProto.SystemEvent_SYSTEM, "Network map updated", "", nil) + return nil } diff --git a/client/internal/routemanager/client.go b/client/internal/routemanager/client.go index 24a7ef467..2f0b78e7b 100644 --- a/client/internal/routemanager/client.go +++ b/client/internal/routemanager/client.go @@ -306,11 +306,13 @@ func (c *clientNetwork) recalculateRouteAndUpdatePeerAndSystem(rsn reason) error return nil } + var isNew bool if c.currentChosen == nil { // If they were not previously assigned to another peer, add routes to the system first if err := c.handler.AddRoute(c.ctx); err != nil { return fmt.Errorf("add route: %w", err) } + isNew = true } else { // Otherwise, remove the allowed IPs from the previous peer first if err := c.removeRouteFromWireGuardPeer(); err != nil { @@ -324,6 +326,10 @@ func (c *clientNetwork) recalculateRouteAndUpdatePeerAndSystem(rsn reason) error return fmt.Errorf("add allowed IPs for peer %s: %w", c.currentChosen.Peer, err) } + if isNew { + c.connectEvent() + } + err := c.statusRecorder.AddPeerStateRoute(c.currentChosen.Peer, c.handler.String()) if err != nil { return fmt.Errorf("add peer state route: %w", err) @@ -331,6 +337,35 @@ func (c *clientNetwork) recalculateRouteAndUpdatePeerAndSystem(rsn reason) error return nil } +func (c *clientNetwork) connectEvent() { + var defaultRoute bool + for _, r := range c.routes { + if r.Network.Bits() == 0 { + defaultRoute = true + break + } + } + + if !defaultRoute { + return + } + + meta := map[string]string{ + "network": c.handler.String(), + } + if c.currentChosen != nil { + meta["id"] = string(c.currentChosen.NetID) + meta["peer"] = c.currentChosen.Peer + } + c.statusRecorder.PublishEvent( + proto.SystemEvent_INFO, + proto.SystemEvent_NETWORK, + "Default route added", + "Exit node connected.", + meta, + ) +} + func (c *clientNetwork) disconnectEvent(rsn reason) { var defaultRoute bool for _, r := range c.routes { @@ -349,29 +384,27 @@ func (c *clientNetwork) disconnectEvent(rsn reason) { var userMessage string meta := make(map[string]string) + if c.currentChosen != nil { + meta["id"] = string(c.currentChosen.NetID) + meta["peer"] = c.currentChosen.Peer + } + meta["network"] = c.handler.String() switch rsn { case reasonShutdown: severity = proto.SystemEvent_INFO message = "Default route removed" userMessage = "Exit node disconnected." - meta["network"] = c.handler.String() case reasonRouteUpdate: severity = proto.SystemEvent_INFO message = "Default route updated due to configuration change" - meta["network"] = c.handler.String() case reasonPeerUpdate: severity = proto.SystemEvent_WARNING message = "Default route disconnected due to peer unreachability" userMessage = "Exit node connection lost. Your internet access might be affected." - if c.currentChosen != nil { - meta["peer"] = c.currentChosen.Peer - meta["network"] = c.handler.String() - } default: severity = proto.SystemEvent_ERROR - message = "Default route disconnected for unknown reason" + message = "Default route disconnected for unknown reasons" userMessage = "Exit node disconnected for unknown reasons." - meta["network"] = c.handler.String() } c.statusRecorder.PublishEvent( diff --git a/client/proto/daemon.pb.go b/client/proto/daemon.pb.go index 3aa57da8f..55b7aa7e9 100644 --- a/client/proto/daemon.pb.go +++ b/client/proto/daemon.pb.go @@ -146,6 +146,7 @@ const ( SystemEvent_DNS SystemEvent_Category = 1 SystemEvent_AUTHENTICATION SystemEvent_Category = 2 SystemEvent_CONNECTIVITY SystemEvent_Category = 3 + SystemEvent_SYSTEM SystemEvent_Category = 4 ) // Enum value maps for SystemEvent_Category. @@ -155,12 +156,14 @@ var ( 1: "DNS", 2: "AUTHENTICATION", 3: "CONNECTIVITY", + 4: "SYSTEM", } SystemEvent_Category_value = map[string]int32{ "NETWORK": 0, "DNS": 1, "AUTHENTICATION": 2, "CONNECTIVITY": 3, + "SYSTEM": 4, } ) @@ -3675,7 +3678,7 @@ var file_daemon_proto_rawDesc = []byte{ 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x66, 0x69, 0x6e, 0x61, 0x6c, 0x44, 0x69, 0x73, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x12, 0x0a, 0x10, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x22, 0x87, 0x04, 0x0a, 0x0b, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, + 0x74, 0x22, 0x93, 0x04, 0x0a, 0x0b, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x38, 0x0a, 0x08, 0x73, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, @@ -3703,111 +3706,111 @@ var file_daemon_proto_rawDesc = []byte{ 0x69, 0x74, 0x79, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x57, 0x41, 0x52, 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x02, 0x12, 0x0c, 0x0a, 0x08, 0x43, 0x52, 0x49, 0x54, 0x49, 0x43, 0x41, - 0x4c, 0x10, 0x03, 0x22, 0x46, 0x0a, 0x08, 0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x12, + 0x4c, 0x10, 0x03, 0x22, 0x52, 0x0a, 0x08, 0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x12, 0x0b, 0x0a, 0x07, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x44, 0x4e, 0x53, 0x10, 0x01, 0x12, 0x12, 0x0a, 0x0e, 0x41, 0x55, 0x54, 0x48, 0x45, 0x4e, 0x54, 0x49, 0x43, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x10, 0x02, 0x12, 0x10, 0x0a, 0x0c, 0x43, 0x4f, 0x4e, - 0x4e, 0x45, 0x43, 0x54, 0x49, 0x56, 0x49, 0x54, 0x59, 0x10, 0x03, 0x22, 0x12, 0x0a, 0x10, 0x47, - 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, - 0x40, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, - 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, - 0x73, 0x2a, 0x62, 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x0b, 0x0a, - 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x50, 0x41, - 0x4e, 0x49, 0x43, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x46, 0x41, 0x54, 0x41, 0x4c, 0x10, 0x02, - 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x57, - 0x41, 0x52, 0x4e, 0x10, 0x04, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x05, 0x12, - 0x09, 0x0a, 0x05, 0x44, 0x45, 0x42, 0x55, 0x47, 0x10, 0x06, 0x12, 0x09, 0x0a, 0x05, 0x54, 0x52, - 0x41, 0x43, 0x45, 0x10, 0x07, 0x32, 0xe7, 0x0a, 0x0a, 0x0d, 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, - 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x36, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, - 0x12, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, - 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, - 0x4b, 0x0a, 0x0c, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, - 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, - 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64, - 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, - 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x2d, 0x0a, 0x02, - 0x55, 0x70, 0x12, 0x11, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, - 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x39, 0x0a, 0x06, 0x53, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x64, - 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x04, 0x44, 0x6f, 0x77, 0x6e, 0x12, 0x13, - 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77, - 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x42, 0x0a, 0x09, 0x47, - 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, - 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, - 0x4b, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, - 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, - 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64, - 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, - 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x51, 0x0a, 0x0e, - 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x1d, + 0x4e, 0x45, 0x43, 0x54, 0x49, 0x56, 0x49, 0x54, 0x59, 0x10, 0x03, 0x12, 0x0a, 0x0a, 0x06, 0x53, + 0x59, 0x53, 0x54, 0x45, 0x4d, 0x10, 0x04, 0x22, 0x12, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x45, 0x76, + 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x40, 0x0a, 0x11, 0x47, + 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x2b, 0x0a, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, + 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x2a, 0x62, 0x0a, + 0x08, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, + 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x50, 0x41, 0x4e, 0x49, 0x43, 0x10, + 0x01, 0x12, 0x09, 0x0a, 0x05, 0x46, 0x41, 0x54, 0x41, 0x4c, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, + 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x57, 0x41, 0x52, 0x4e, 0x10, + 0x04, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x05, 0x12, 0x09, 0x0a, 0x05, 0x44, + 0x45, 0x42, 0x55, 0x47, 0x10, 0x06, 0x12, 0x09, 0x0a, 0x05, 0x54, 0x52, 0x41, 0x43, 0x45, 0x10, + 0x07, 0x32, 0xe7, 0x0a, 0x0a, 0x0d, 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x12, 0x36, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x14, 0x2e, 0x64, + 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, + 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x0c, 0x57, + 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1b, 0x2e, 0x64, 0x61, + 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, + 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, + 0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x2d, 0x0a, 0x02, 0x55, 0x70, 0x12, 0x11, + 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x39, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x12, 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, + 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x00, 0x12, 0x33, 0x0a, 0x04, 0x44, 0x6f, 0x77, 0x6e, 0x12, 0x13, 0x2e, 0x64, 0x61, 0x65, + 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x42, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, + 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, + 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x0c, 0x4c, + 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x1b, 0x2e, 0x64, 0x61, + 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, + 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x51, 0x0a, 0x0e, 0x53, 0x65, 0x6c, 0x65, + 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x1d, 0x2e, 0x64, 0x61, 0x65, + 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, + 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x64, 0x61, 0x65, 0x6d, + 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, + 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x53, 0x0a, 0x10, 0x44, + 0x65, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, + 0x1d, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, + 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, - 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, - 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, - 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, - 0x53, 0x0a, 0x10, 0x44, 0x65, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, - 0x72, 0x6b, 0x73, 0x12, 0x1d, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, - 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, - 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, - 0x64, 0x6c, 0x65, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x62, - 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, - 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, - 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1a, 0x2e, - 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, - 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, + 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, + 0x12, 0x48, 0x0a, 0x0b, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x12, + 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, + 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, + 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x47, 0x65, + 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x53, 0x65, 0x74, 0x4c, - 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, - 0x2e, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, - 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x22, 0x00, 0x12, 0x45, 0x0a, 0x0a, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, - 0x12, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, - 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x64, 0x61, - 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x45, 0x0a, 0x0a, 0x43, 0x6c, 0x65, - 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, - 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6c, 0x65, 0x61, - 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, - 0x12, 0x48, 0x0a, 0x0b, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, - 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, - 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, + 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, + 0x76, 0x65, 0x6c, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, + 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, + 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x45, + 0x0a, 0x0a, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x12, 0x19, 0x2e, 0x64, + 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, + 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x45, 0x0a, 0x0a, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, + 0x61, 0x74, 0x65, 0x12, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6c, 0x65, + 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, + 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, + 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, + 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x6f, 0x0a, 0x18, 0x53, 0x65, - 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, - 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x27, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, - 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, - 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x28, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, - 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, - 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x54, - 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, - 0x6d, 0x6f, 0x6e, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, - 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x44, 0x0a, 0x0f, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, - 0x62, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, - 0x6e, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, - 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x00, 0x30, 0x01, 0x12, 0x42, 0x0a, 0x09, 0x47, - 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, - 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x45, - 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, - 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x33, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, + 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x6f, 0x0a, 0x18, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, + 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, + 0x63, 0x65, 0x12, 0x27, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4e, + 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, + 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x64, 0x61, + 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, + 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x54, 0x72, 0x61, 0x63, 0x65, + 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, + 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x54, 0x72, 0x61, 0x63, + 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0x00, 0x12, 0x44, 0x0a, 0x0f, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x45, 0x76, + 0x65, 0x6e, 0x74, 0x73, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x75, + 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, + 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, + 0x65, 0x6e, 0x74, 0x22, 0x00, 0x30, 0x01, 0x12, 0x42, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x45, 0x76, + 0x65, 0x6e, 0x74, 0x73, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, + 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, + 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, + 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x08, 0x5a, 0x06, 0x2f, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/client/proto/daemon.proto b/client/proto/daemon.proto index 012b8b4db..b1a6a6614 100644 --- a/client/proto/daemon.proto +++ b/client/proto/daemon.proto @@ -425,6 +425,7 @@ message SystemEvent { DNS = 1; AUTHENTICATION = 2; CONNECTIVITY = 3; + SYSTEM = 4; } string id = 1; diff --git a/client/server/network.go b/client/server/network.go index aaf361524..d310f4da1 100644 --- a/client/server/network.go +++ b/client/server/network.go @@ -6,6 +6,7 @@ import ( "net/netip" "slices" "sort" + "strings" "golang.org/x/exp/maps" @@ -134,6 +135,18 @@ func (s *Server) SelectNetworks(_ context.Context, req *proto.SelectNetworksRequ } routeManager.TriggerSelection(routeManager.GetClientRoutes()) + s.statusRecorder.PublishEvent( + proto.SystemEvent_INFO, + proto.SystemEvent_SYSTEM, + "Network selection changed", + "", + map[string]string{ + "networks": strings.Join(req.GetNetworkIDs(), ", "), + "append": fmt.Sprint(req.GetAppend()), + "all": fmt.Sprint(req.GetAll()), + }, + ) + return &proto.SelectNetworksResponse{}, nil } @@ -164,6 +177,18 @@ func (s *Server) DeselectNetworks(_ context.Context, req *proto.SelectNetworksRe } routeManager.TriggerSelection(routeManager.GetClientRoutes()) + s.statusRecorder.PublishEvent( + proto.SystemEvent_INFO, + proto.SystemEvent_SYSTEM, + "Network deselection changed", + "", + map[string]string{ + "networks": strings.Join(req.GetNetworkIDs(), ", "), + "append": fmt.Sprint(req.GetAppend()), + "all": fmt.Sprint(req.GetAll()), + }, + ) + return &proto.SelectNetworksResponse{}, nil } diff --git a/client/cmd/status_event.go b/client/status/event.go similarity index 86% rename from client/cmd/status_event.go rename to client/status/event.go index 9331570e6..2b65c9fa3 100644 --- a/client/cmd/status_event.go +++ b/client/status/event.go @@ -1,4 +1,4 @@ -package cmd +package status import ( "fmt" @@ -9,7 +9,7 @@ import ( "github.com/netbirdio/netbird/client/proto" ) -type systemEventOutput struct { +type SystemEventOutput struct { ID string `json:"id" yaml:"id"` Severity string `json:"severity" yaml:"severity"` Category string `json:"category" yaml:"category"` @@ -19,10 +19,10 @@ type systemEventOutput struct { Metadata map[string]string `json:"metadata" yaml:"metadata"` } -func mapEvents(protoEvents []*proto.SystemEvent) []systemEventOutput { - events := make([]systemEventOutput, len(protoEvents)) +func mapEvents(protoEvents []*proto.SystemEvent) []SystemEventOutput { + events := make([]SystemEventOutput, len(protoEvents)) for i, event := range protoEvents { - events[i] = systemEventOutput{ + events[i] = SystemEventOutput{ ID: event.GetId(), Severity: event.GetSeverity().String(), Category: event.GetCategory().String(), @@ -35,7 +35,7 @@ func mapEvents(protoEvents []*proto.SystemEvent) []systemEventOutput { return events } -func parseEvents(events []systemEventOutput) string { +func parseEvents(events []SystemEventOutput) string { if len(events) == 0 { return " No events recorded" } diff --git a/client/status/status.go b/client/status/status.go new file mode 100644 index 000000000..2d11ee3ba --- /dev/null +++ b/client/status/status.go @@ -0,0 +1,725 @@ +package status + +import ( + "encoding/json" + "fmt" + "net" + "net/netip" + "os" + "runtime" + "sort" + "strings" + "time" + + "gopkg.in/yaml.v3" + + "github.com/netbirdio/netbird/client/anonymize" + "github.com/netbirdio/netbird/client/internal/peer" + "github.com/netbirdio/netbird/client/proto" + "github.com/netbirdio/netbird/version" +) + +type PeerStateDetailOutput struct { + FQDN string `json:"fqdn" yaml:"fqdn"` + IP string `json:"netbirdIp" yaml:"netbirdIp"` + PubKey string `json:"publicKey" yaml:"publicKey"` + Status string `json:"status" yaml:"status"` + LastStatusUpdate time.Time `json:"lastStatusUpdate" yaml:"lastStatusUpdate"` + ConnType string `json:"connectionType" yaml:"connectionType"` + IceCandidateType IceCandidateType `json:"iceCandidateType" yaml:"iceCandidateType"` + IceCandidateEndpoint IceCandidateType `json:"iceCandidateEndpoint" yaml:"iceCandidateEndpoint"` + RelayAddress string `json:"relayAddress" yaml:"relayAddress"` + LastWireguardHandshake time.Time `json:"lastWireguardHandshake" yaml:"lastWireguardHandshake"` + TransferReceived int64 `json:"transferReceived" yaml:"transferReceived"` + TransferSent int64 `json:"transferSent" yaml:"transferSent"` + Latency time.Duration `json:"latency" yaml:"latency"` + RosenpassEnabled bool `json:"quantumResistance" yaml:"quantumResistance"` + Networks []string `json:"networks" yaml:"networks"` +} + +type PeersStateOutput struct { + Total int `json:"total" yaml:"total"` + Connected int `json:"connected" yaml:"connected"` + Details []PeerStateDetailOutput `json:"details" yaml:"details"` +} + +type SignalStateOutput struct { + URL string `json:"url" yaml:"url"` + Connected bool `json:"connected" yaml:"connected"` + Error string `json:"error" yaml:"error"` +} + +type ManagementStateOutput struct { + URL string `json:"url" yaml:"url"` + Connected bool `json:"connected" yaml:"connected"` + Error string `json:"error" yaml:"error"` +} + +type RelayStateOutputDetail struct { + URI string `json:"uri" yaml:"uri"` + Available bool `json:"available" yaml:"available"` + Error string `json:"error" yaml:"error"` +} + +type RelayStateOutput struct { + Total int `json:"total" yaml:"total"` + Available int `json:"available" yaml:"available"` + Details []RelayStateOutputDetail `json:"details" yaml:"details"` +} + +type IceCandidateType struct { + Local string `json:"local" yaml:"local"` + Remote string `json:"remote" yaml:"remote"` +} + +type NsServerGroupStateOutput struct { + Servers []string `json:"servers" yaml:"servers"` + Domains []string `json:"domains" yaml:"domains"` + Enabled bool `json:"enabled" yaml:"enabled"` + Error string `json:"error" yaml:"error"` +} + +type OutputOverview struct { + Peers PeersStateOutput `json:"peers" yaml:"peers"` + CliVersion string `json:"cliVersion" yaml:"cliVersion"` + DaemonVersion string `json:"daemonVersion" yaml:"daemonVersion"` + ManagementState ManagementStateOutput `json:"management" yaml:"management"` + SignalState SignalStateOutput `json:"signal" yaml:"signal"` + Relays RelayStateOutput `json:"relays" yaml:"relays"` + IP string `json:"netbirdIp" yaml:"netbirdIp"` + PubKey string `json:"publicKey" yaml:"publicKey"` + KernelInterface bool `json:"usesKernelInterface" yaml:"usesKernelInterface"` + FQDN string `json:"fqdn" yaml:"fqdn"` + RosenpassEnabled bool `json:"quantumResistance" yaml:"quantumResistance"` + RosenpassPermissive bool `json:"quantumResistancePermissive" yaml:"quantumResistancePermissive"` + Networks []string `json:"networks" yaml:"networks"` + NSServerGroups []NsServerGroupStateOutput `json:"dnsServers" yaml:"dnsServers"` + Events []SystemEventOutput `json:"events" yaml:"events"` +} + +func ConvertToStatusOutputOverview(resp *proto.StatusResponse, anon bool, statusFilter string, prefixNamesFilter []string, prefixNamesFilterMap map[string]struct{}, ipsFilter map[string]struct{}) OutputOverview { + pbFullStatus := resp.GetFullStatus() + + managementState := pbFullStatus.GetManagementState() + managementOverview := ManagementStateOutput{ + URL: managementState.GetURL(), + Connected: managementState.GetConnected(), + Error: managementState.Error, + } + + signalState := pbFullStatus.GetSignalState() + signalOverview := SignalStateOutput{ + URL: signalState.GetURL(), + Connected: signalState.GetConnected(), + Error: signalState.Error, + } + + relayOverview := mapRelays(pbFullStatus.GetRelays()) + peersOverview := mapPeers(resp.GetFullStatus().GetPeers(), statusFilter, prefixNamesFilter, prefixNamesFilterMap, ipsFilter) + + overview := OutputOverview{ + Peers: peersOverview, + CliVersion: version.NetbirdVersion(), + DaemonVersion: resp.GetDaemonVersion(), + ManagementState: managementOverview, + SignalState: signalOverview, + Relays: relayOverview, + IP: pbFullStatus.GetLocalPeerState().GetIP(), + PubKey: pbFullStatus.GetLocalPeerState().GetPubKey(), + KernelInterface: pbFullStatus.GetLocalPeerState().GetKernelInterface(), + FQDN: pbFullStatus.GetLocalPeerState().GetFqdn(), + RosenpassEnabled: pbFullStatus.GetLocalPeerState().GetRosenpassEnabled(), + RosenpassPermissive: pbFullStatus.GetLocalPeerState().GetRosenpassPermissive(), + Networks: pbFullStatus.GetLocalPeerState().GetNetworks(), + NSServerGroups: mapNSGroups(pbFullStatus.GetDnsServers()), + Events: mapEvents(pbFullStatus.GetEvents()), + } + + if anon { + anonymizer := anonymize.NewAnonymizer(anonymize.DefaultAddresses()) + anonymizeOverview(anonymizer, &overview) + } + + return overview +} + +func mapRelays(relays []*proto.RelayState) RelayStateOutput { + var relayStateDetail []RelayStateOutputDetail + + var relaysAvailable int + for _, relay := range relays { + available := relay.GetAvailable() + relayStateDetail = append(relayStateDetail, + RelayStateOutputDetail{ + URI: relay.URI, + Available: available, + Error: relay.GetError(), + }, + ) + + if available { + relaysAvailable++ + } + } + + return RelayStateOutput{ + Total: len(relays), + Available: relaysAvailable, + Details: relayStateDetail, + } +} + +func mapNSGroups(servers []*proto.NSGroupState) []NsServerGroupStateOutput { + mappedNSGroups := make([]NsServerGroupStateOutput, 0, len(servers)) + for _, pbNsGroupServer := range servers { + mappedNSGroups = append(mappedNSGroups, NsServerGroupStateOutput{ + Servers: pbNsGroupServer.GetServers(), + Domains: pbNsGroupServer.GetDomains(), + Enabled: pbNsGroupServer.GetEnabled(), + Error: pbNsGroupServer.GetError(), + }) + } + return mappedNSGroups +} + +func mapPeers( + peers []*proto.PeerState, + statusFilter string, + prefixNamesFilter []string, + prefixNamesFilterMap map[string]struct{}, + ipsFilter map[string]struct{}, +) PeersStateOutput { + var peersStateDetail []PeerStateDetailOutput + peersConnected := 0 + for _, pbPeerState := range peers { + localICE := "" + remoteICE := "" + localICEEndpoint := "" + remoteICEEndpoint := "" + relayServerAddress := "" + connType := "" + lastHandshake := time.Time{} + transferReceived := int64(0) + transferSent := int64(0) + + isPeerConnected := pbPeerState.ConnStatus == peer.StatusConnected.String() + if skipDetailByFilters(pbPeerState, isPeerConnected, statusFilter, prefixNamesFilter, prefixNamesFilterMap, ipsFilter) { + continue + } + if isPeerConnected { + peersConnected++ + + localICE = pbPeerState.GetLocalIceCandidateType() + remoteICE = pbPeerState.GetRemoteIceCandidateType() + localICEEndpoint = pbPeerState.GetLocalIceCandidateEndpoint() + remoteICEEndpoint = pbPeerState.GetRemoteIceCandidateEndpoint() + connType = "P2P" + if pbPeerState.Relayed { + connType = "Relayed" + } + relayServerAddress = pbPeerState.GetRelayAddress() + lastHandshake = pbPeerState.GetLastWireguardHandshake().AsTime().Local() + transferReceived = pbPeerState.GetBytesRx() + transferSent = pbPeerState.GetBytesTx() + } + + timeLocal := pbPeerState.GetConnStatusUpdate().AsTime().Local() + peerState := PeerStateDetailOutput{ + IP: pbPeerState.GetIP(), + PubKey: pbPeerState.GetPubKey(), + Status: pbPeerState.GetConnStatus(), + LastStatusUpdate: timeLocal, + ConnType: connType, + IceCandidateType: IceCandidateType{ + Local: localICE, + Remote: remoteICE, + }, + IceCandidateEndpoint: IceCandidateType{ + Local: localICEEndpoint, + Remote: remoteICEEndpoint, + }, + RelayAddress: relayServerAddress, + FQDN: pbPeerState.GetFqdn(), + LastWireguardHandshake: lastHandshake, + TransferReceived: transferReceived, + TransferSent: transferSent, + Latency: pbPeerState.GetLatency().AsDuration(), + RosenpassEnabled: pbPeerState.GetRosenpassEnabled(), + Networks: pbPeerState.GetNetworks(), + } + + peersStateDetail = append(peersStateDetail, peerState) + } + + sortPeersByIP(peersStateDetail) + + peersOverview := PeersStateOutput{ + Total: len(peersStateDetail), + Connected: peersConnected, + Details: peersStateDetail, + } + return peersOverview +} + +func sortPeersByIP(peersStateDetail []PeerStateDetailOutput) { + if len(peersStateDetail) > 0 { + sort.SliceStable(peersStateDetail, func(i, j int) bool { + iAddr, _ := netip.ParseAddr(peersStateDetail[i].IP) + jAddr, _ := netip.ParseAddr(peersStateDetail[j].IP) + return iAddr.Compare(jAddr) == -1 + }) + } +} + +func ParseToJSON(overview OutputOverview) (string, error) { + jsonBytes, err := json.Marshal(overview) + if err != nil { + return "", fmt.Errorf("json marshal failed") + } + return string(jsonBytes), err +} + +func ParseToYAML(overview OutputOverview) (string, error) { + yamlBytes, err := yaml.Marshal(overview) + if err != nil { + return "", fmt.Errorf("yaml marshal failed") + } + return string(yamlBytes), nil +} + +func ParseGeneralSummary(overview OutputOverview, showURL bool, showRelays bool, showNameServers bool) string { + var managementConnString string + if overview.ManagementState.Connected { + managementConnString = "Connected" + if showURL { + managementConnString = fmt.Sprintf("%s to %s", managementConnString, overview.ManagementState.URL) + } + } else { + managementConnString = "Disconnected" + if overview.ManagementState.Error != "" { + managementConnString = fmt.Sprintf("%s, reason: %s", managementConnString, overview.ManagementState.Error) + } + } + + var signalConnString string + if overview.SignalState.Connected { + signalConnString = "Connected" + if showURL { + signalConnString = fmt.Sprintf("%s to %s", signalConnString, overview.SignalState.URL) + } + } else { + signalConnString = "Disconnected" + if overview.SignalState.Error != "" { + signalConnString = fmt.Sprintf("%s, reason: %s", signalConnString, overview.SignalState.Error) + } + } + + interfaceTypeString := "Userspace" + interfaceIP := overview.IP + if overview.KernelInterface { + interfaceTypeString = "Kernel" + } else if overview.IP == "" { + interfaceTypeString = "N/A" + interfaceIP = "N/A" + } + + var relaysString string + if showRelays { + for _, relay := range overview.Relays.Details { + available := "Available" + reason := "" + if !relay.Available { + available = "Unavailable" + reason = fmt.Sprintf(", reason: %s", relay.Error) + } + relaysString += fmt.Sprintf("\n [%s] is %s%s", relay.URI, available, reason) + } + } else { + relaysString = fmt.Sprintf("%d/%d Available", overview.Relays.Available, overview.Relays.Total) + } + + networks := "-" + if len(overview.Networks) > 0 { + sort.Strings(overview.Networks) + networks = strings.Join(overview.Networks, ", ") + } + + var dnsServersString string + if showNameServers { + for _, nsServerGroup := range overview.NSServerGroups { + enabled := "Available" + if !nsServerGroup.Enabled { + enabled = "Unavailable" + } + errorString := "" + if nsServerGroup.Error != "" { + errorString = fmt.Sprintf(", reason: %s", nsServerGroup.Error) + errorString = strings.TrimSpace(errorString) + } + + domainsString := strings.Join(nsServerGroup.Domains, ", ") + if domainsString == "" { + domainsString = "." // Show "." for the default zone + } + dnsServersString += fmt.Sprintf( + "\n [%s] for [%s] is %s%s", + strings.Join(nsServerGroup.Servers, ", "), + domainsString, + enabled, + errorString, + ) + } + } else { + dnsServersString = fmt.Sprintf("%d/%d Available", countEnabled(overview.NSServerGroups), len(overview.NSServerGroups)) + } + + rosenpassEnabledStatus := "false" + if overview.RosenpassEnabled { + rosenpassEnabledStatus = "true" + if overview.RosenpassPermissive { + rosenpassEnabledStatus = "true (permissive)" //nolint:gosec + } + } + + peersCountString := fmt.Sprintf("%d/%d Connected", overview.Peers.Connected, overview.Peers.Total) + + goos := runtime.GOOS + goarch := runtime.GOARCH + goarm := "" + if goarch == "arm" { + goarm = fmt.Sprintf(" (ARMv%s)", os.Getenv("GOARM")) + } + + summary := fmt.Sprintf( + "OS: %s\n"+ + "Daemon version: %s\n"+ + "CLI version: %s\n"+ + "Management: %s\n"+ + "Signal: %s\n"+ + "Relays: %s\n"+ + "Nameservers: %s\n"+ + "FQDN: %s\n"+ + "NetBird IP: %s\n"+ + "Interface type: %s\n"+ + "Quantum resistance: %s\n"+ + "Networks: %s\n"+ + "Peers count: %s\n", + fmt.Sprintf("%s/%s%s", goos, goarch, goarm), + overview.DaemonVersion, + version.NetbirdVersion(), + managementConnString, + signalConnString, + relaysString, + dnsServersString, + overview.FQDN, + interfaceIP, + interfaceTypeString, + rosenpassEnabledStatus, + networks, + peersCountString, + ) + return summary +} + +func ParseToFullDetailSummary(overview OutputOverview) string { + parsedPeersString := parsePeers(overview.Peers, overview.RosenpassEnabled, overview.RosenpassPermissive) + parsedEventsString := parseEvents(overview.Events) + summary := ParseGeneralSummary(overview, true, true, true) + + return fmt.Sprintf( + "Peers detail:"+ + "%s\n"+ + "Events:"+ + "%s\n"+ + "%s", + parsedPeersString, + parsedEventsString, + summary, + ) +} + +func parsePeers(peers PeersStateOutput, rosenpassEnabled, rosenpassPermissive bool) string { + var ( + peersString = "" + ) + + for _, peerState := range peers.Details { + + localICE := "-" + if peerState.IceCandidateType.Local != "" { + localICE = peerState.IceCandidateType.Local + } + + remoteICE := "-" + if peerState.IceCandidateType.Remote != "" { + remoteICE = peerState.IceCandidateType.Remote + } + + localICEEndpoint := "-" + if peerState.IceCandidateEndpoint.Local != "" { + localICEEndpoint = peerState.IceCandidateEndpoint.Local + } + + remoteICEEndpoint := "-" + if peerState.IceCandidateEndpoint.Remote != "" { + remoteICEEndpoint = peerState.IceCandidateEndpoint.Remote + } + + rosenpassEnabledStatus := "false" + if rosenpassEnabled { + if peerState.RosenpassEnabled { + rosenpassEnabledStatus = "true" + } else { + if rosenpassPermissive { + rosenpassEnabledStatus = "false (remote didn't enable quantum resistance)" + } else { + rosenpassEnabledStatus = "false (connection won't work without a permissive mode)" + } + } + } else { + if peerState.RosenpassEnabled { + rosenpassEnabledStatus = "false (connection might not work without a remote permissive mode)" + } + } + + networks := "-" + if len(peerState.Networks) > 0 { + sort.Strings(peerState.Networks) + networks = strings.Join(peerState.Networks, ", ") + } + + peerString := fmt.Sprintf( + "\n %s:\n"+ + " NetBird IP: %s\n"+ + " Public key: %s\n"+ + " Status: %s\n"+ + " -- detail --\n"+ + " Connection type: %s\n"+ + " ICE candidate (Local/Remote): %s/%s\n"+ + " ICE candidate endpoints (Local/Remote): %s/%s\n"+ + " Relay server address: %s\n"+ + " Last connection update: %s\n"+ + " Last WireGuard handshake: %s\n"+ + " Transfer status (received/sent) %s/%s\n"+ + " Quantum resistance: %s\n"+ + " Networks: %s\n"+ + " Latency: %s\n", + peerState.FQDN, + peerState.IP, + peerState.PubKey, + peerState.Status, + peerState.ConnType, + localICE, + remoteICE, + localICEEndpoint, + remoteICEEndpoint, + peerState.RelayAddress, + timeAgo(peerState.LastStatusUpdate), + timeAgo(peerState.LastWireguardHandshake), + toIEC(peerState.TransferReceived), + toIEC(peerState.TransferSent), + rosenpassEnabledStatus, + networks, + peerState.Latency.String(), + ) + + peersString += peerString + } + return peersString +} + +func skipDetailByFilters( + peerState *proto.PeerState, + isConnected bool, + statusFilter string, + prefixNamesFilter []string, + prefixNamesFilterMap map[string]struct{}, + ipsFilter map[string]struct{}, +) bool { + statusEval := false + ipEval := false + nameEval := true + + if statusFilter != "" { + lowerStatusFilter := strings.ToLower(statusFilter) + if lowerStatusFilter == "disconnected" && isConnected { + statusEval = true + } else if lowerStatusFilter == "connected" && !isConnected { + statusEval = true + } + } + + if len(ipsFilter) > 0 { + _, ok := ipsFilter[peerState.IP] + if !ok { + ipEval = true + } + } + + if len(prefixNamesFilter) > 0 { + for prefixNameFilter := range prefixNamesFilterMap { + if strings.HasPrefix(peerState.Fqdn, prefixNameFilter) { + nameEval = false + break + } + } + } else { + nameEval = false + } + + return statusEval || ipEval || nameEval +} + +func toIEC(b int64) string { + const unit = 1024 + if b < unit { + return fmt.Sprintf("%d B", b) + } + div, exp := int64(unit), 0 + for n := b / unit; n >= unit; n /= unit { + div *= unit + exp++ + } + return fmt.Sprintf("%.1f %ciB", + float64(b)/float64(div), "KMGTPE"[exp]) +} + +func countEnabled(dnsServers []NsServerGroupStateOutput) int { + count := 0 + for _, server := range dnsServers { + if server.Enabled { + count++ + } + } + return count +} + +// timeAgo returns a string representing the duration since the provided time in a human-readable format. +func timeAgo(t time.Time) string { + if t.IsZero() || t.Equal(time.Unix(0, 0)) { + return "-" + } + duration := time.Since(t) + switch { + case duration < time.Second: + return "Now" + case duration < time.Minute: + seconds := int(duration.Seconds()) + if seconds == 1 { + return "1 second ago" + } + return fmt.Sprintf("%d seconds ago", seconds) + case duration < time.Hour: + minutes := int(duration.Minutes()) + seconds := int(duration.Seconds()) % 60 + if minutes == 1 { + if seconds == 1 { + return "1 minute, 1 second ago" + } else if seconds > 0 { + return fmt.Sprintf("1 minute, %d seconds ago", seconds) + } + return "1 minute ago" + } + if seconds > 0 { + return fmt.Sprintf("%d minutes, %d seconds ago", minutes, seconds) + } + return fmt.Sprintf("%d minutes ago", minutes) + case duration < 24*time.Hour: + hours := int(duration.Hours()) + minutes := int(duration.Minutes()) % 60 + if hours == 1 { + if minutes == 1 { + return "1 hour, 1 minute ago" + } else if minutes > 0 { + return fmt.Sprintf("1 hour, %d minutes ago", minutes) + } + return "1 hour ago" + } + if minutes > 0 { + return fmt.Sprintf("%d hours, %d minutes ago", hours, minutes) + } + return fmt.Sprintf("%d hours ago", hours) + } + + days := int(duration.Hours()) / 24 + hours := int(duration.Hours()) % 24 + if days == 1 { + if hours == 1 { + return "1 day, 1 hour ago" + } else if hours > 0 { + return fmt.Sprintf("1 day, %d hours ago", hours) + } + return "1 day ago" + } + if hours > 0 { + return fmt.Sprintf("%d days, %d hours ago", days, hours) + } + return fmt.Sprintf("%d days ago", days) +} + +func anonymizePeerDetail(a *anonymize.Anonymizer, peer *PeerStateDetailOutput) { + peer.FQDN = a.AnonymizeDomain(peer.FQDN) + if localIP, port, err := net.SplitHostPort(peer.IceCandidateEndpoint.Local); err == nil { + peer.IceCandidateEndpoint.Local = fmt.Sprintf("%s:%s", a.AnonymizeIPString(localIP), port) + } + if remoteIP, port, err := net.SplitHostPort(peer.IceCandidateEndpoint.Remote); err == nil { + peer.IceCandidateEndpoint.Remote = fmt.Sprintf("%s:%s", a.AnonymizeIPString(remoteIP), port) + } + + peer.RelayAddress = a.AnonymizeURI(peer.RelayAddress) + + for i, route := range peer.Networks { + peer.Networks[i] = a.AnonymizeIPString(route) + } + + for i, route := range peer.Networks { + peer.Networks[i] = a.AnonymizeRoute(route) + } +} + +func anonymizeOverview(a *anonymize.Anonymizer, overview *OutputOverview) { + for i, peer := range overview.Peers.Details { + peer := peer + anonymizePeerDetail(a, &peer) + overview.Peers.Details[i] = peer + } + + overview.ManagementState.URL = a.AnonymizeURI(overview.ManagementState.URL) + overview.ManagementState.Error = a.AnonymizeString(overview.ManagementState.Error) + overview.SignalState.URL = a.AnonymizeURI(overview.SignalState.URL) + overview.SignalState.Error = a.AnonymizeString(overview.SignalState.Error) + + overview.IP = a.AnonymizeIPString(overview.IP) + for i, detail := range overview.Relays.Details { + detail.URI = a.AnonymizeURI(detail.URI) + detail.Error = a.AnonymizeString(detail.Error) + overview.Relays.Details[i] = detail + } + + for i, nsGroup := range overview.NSServerGroups { + for j, domain := range nsGroup.Domains { + overview.NSServerGroups[i].Domains[j] = a.AnonymizeDomain(domain) + } + for j, ns := range nsGroup.Servers { + host, port, err := net.SplitHostPort(ns) + if err == nil { + overview.NSServerGroups[i].Servers[j] = fmt.Sprintf("%s:%s", a.AnonymizeIPString(host), port) + } + } + } + + for i, route := range overview.Networks { + overview.Networks[i] = a.AnonymizeRoute(route) + } + + overview.FQDN = a.AnonymizeDomain(overview.FQDN) + + for i, event := range overview.Events { + overview.Events[i].Message = a.AnonymizeString(event.Message) + overview.Events[i].UserMessage = a.AnonymizeString(event.UserMessage) + + for k, v := range event.Metadata { + event.Metadata[k] = a.AnonymizeString(v) + } + } +} diff --git a/client/status/status_test.go b/client/status/status_test.go new file mode 100644 index 000000000..24c4827d3 --- /dev/null +++ b/client/status/status_test.go @@ -0,0 +1,603 @@ +package status + +import ( + "bytes" + "encoding/json" + "fmt" + "runtime" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/netbirdio/netbird/client/proto" + "github.com/netbirdio/netbird/version" +) + +func init() { + loc, err := time.LoadLocation("UTC") + if err != nil { + panic(err) + } + + time.Local = loc +} + +var resp = &proto.StatusResponse{ + Status: "Connected", + FullStatus: &proto.FullStatus{ + Peers: []*proto.PeerState{ + { + IP: "192.168.178.101", + PubKey: "Pubkey1", + Fqdn: "peer-1.awesome-domain.com", + ConnStatus: "Connected", + ConnStatusUpdate: timestamppb.New(time.Date(2001, time.Month(1), 1, 1, 1, 1, 0, time.UTC)), + Relayed: false, + LocalIceCandidateType: "", + RemoteIceCandidateType: "", + LocalIceCandidateEndpoint: "", + RemoteIceCandidateEndpoint: "", + LastWireguardHandshake: timestamppb.New(time.Date(2001, time.Month(1), 1, 1, 1, 2, 0, time.UTC)), + BytesRx: 200, + BytesTx: 100, + Networks: []string{ + "10.1.0.0/24", + }, + Latency: durationpb.New(time.Duration(10000000)), + }, + { + IP: "192.168.178.102", + PubKey: "Pubkey2", + Fqdn: "peer-2.awesome-domain.com", + ConnStatus: "Connected", + ConnStatusUpdate: timestamppb.New(time.Date(2002, time.Month(2), 2, 2, 2, 2, 0, time.UTC)), + Relayed: true, + LocalIceCandidateType: "relay", + RemoteIceCandidateType: "prflx", + LocalIceCandidateEndpoint: "10.0.0.1:10001", + RemoteIceCandidateEndpoint: "10.0.10.1:10002", + LastWireguardHandshake: timestamppb.New(time.Date(2002, time.Month(2), 2, 2, 2, 3, 0, time.UTC)), + BytesRx: 2000, + BytesTx: 1000, + Latency: durationpb.New(time.Duration(10000000)), + }, + }, + ManagementState: &proto.ManagementState{ + URL: "my-awesome-management.com:443", + Connected: true, + Error: "", + }, + SignalState: &proto.SignalState{ + URL: "my-awesome-signal.com:443", + Connected: true, + Error: "", + }, + Relays: []*proto.RelayState{ + { + URI: "stun:my-awesome-stun.com:3478", + Available: true, + Error: "", + }, + { + URI: "turns:my-awesome-turn.com:443?transport=tcp", + Available: false, + Error: "context: deadline exceeded", + }, + }, + LocalPeerState: &proto.LocalPeerState{ + IP: "192.168.178.100/16", + PubKey: "Some-Pub-Key", + KernelInterface: true, + Fqdn: "some-localhost.awesome-domain.com", + Networks: []string{ + "10.10.0.0/24", + }, + }, + DnsServers: []*proto.NSGroupState{ + { + Servers: []string{ + "8.8.8.8:53", + }, + Domains: nil, + Enabled: true, + Error: "", + }, + { + Servers: []string{ + "1.1.1.1:53", + "2.2.2.2:53", + }, + Domains: []string{ + "example.com", + "example.net", + }, + Enabled: false, + Error: "timeout", + }, + }, + }, + DaemonVersion: "0.14.1", +} + +var overview = OutputOverview{ + Peers: PeersStateOutput{ + Total: 2, + Connected: 2, + Details: []PeerStateDetailOutput{ + { + IP: "192.168.178.101", + PubKey: "Pubkey1", + FQDN: "peer-1.awesome-domain.com", + Status: "Connected", + LastStatusUpdate: time.Date(2001, 1, 1, 1, 1, 1, 0, time.UTC), + ConnType: "P2P", + IceCandidateType: IceCandidateType{ + Local: "", + Remote: "", + }, + IceCandidateEndpoint: IceCandidateType{ + Local: "", + Remote: "", + }, + LastWireguardHandshake: time.Date(2001, 1, 1, 1, 1, 2, 0, time.UTC), + TransferReceived: 200, + TransferSent: 100, + Networks: []string{ + "10.1.0.0/24", + }, + Latency: time.Duration(10000000), + }, + { + IP: "192.168.178.102", + PubKey: "Pubkey2", + FQDN: "peer-2.awesome-domain.com", + Status: "Connected", + LastStatusUpdate: time.Date(2002, 2, 2, 2, 2, 2, 0, time.UTC), + ConnType: "Relayed", + IceCandidateType: IceCandidateType{ + Local: "relay", + Remote: "prflx", + }, + IceCandidateEndpoint: IceCandidateType{ + Local: "10.0.0.1:10001", + Remote: "10.0.10.1:10002", + }, + LastWireguardHandshake: time.Date(2002, 2, 2, 2, 2, 3, 0, time.UTC), + TransferReceived: 2000, + TransferSent: 1000, + Latency: time.Duration(10000000), + }, + }, + }, + Events: []SystemEventOutput{}, + CliVersion: version.NetbirdVersion(), + DaemonVersion: "0.14.1", + ManagementState: ManagementStateOutput{ + URL: "my-awesome-management.com:443", + Connected: true, + Error: "", + }, + SignalState: SignalStateOutput{ + URL: "my-awesome-signal.com:443", + Connected: true, + Error: "", + }, + Relays: RelayStateOutput{ + Total: 2, + Available: 1, + Details: []RelayStateOutputDetail{ + { + URI: "stun:my-awesome-stun.com:3478", + Available: true, + Error: "", + }, + { + URI: "turns:my-awesome-turn.com:443?transport=tcp", + Available: false, + Error: "context: deadline exceeded", + }, + }, + }, + IP: "192.168.178.100/16", + PubKey: "Some-Pub-Key", + KernelInterface: true, + FQDN: "some-localhost.awesome-domain.com", + NSServerGroups: []NsServerGroupStateOutput{ + { + Servers: []string{ + "8.8.8.8:53", + }, + Domains: nil, + Enabled: true, + Error: "", + }, + { + Servers: []string{ + "1.1.1.1:53", + "2.2.2.2:53", + }, + Domains: []string{ + "example.com", + "example.net", + }, + Enabled: false, + Error: "timeout", + }, + }, + Networks: []string{ + "10.10.0.0/24", + }, +} + +func TestConversionFromFullStatusToOutputOverview(t *testing.T) { + convertedResult := ConvertToStatusOutputOverview(resp, false, "", nil, nil, nil) + + assert.Equal(t, overview, convertedResult) +} + +func TestSortingOfPeers(t *testing.T) { + peers := []PeerStateDetailOutput{ + { + IP: "192.168.178.104", + }, + { + IP: "192.168.178.102", + }, + { + IP: "192.168.178.101", + }, + { + IP: "192.168.178.105", + }, + { + IP: "192.168.178.103", + }, + } + + sortPeersByIP(peers) + + assert.Equal(t, peers[3].IP, "192.168.178.104") +} + +func TestParsingToJSON(t *testing.T) { + jsonString, _ := ParseToJSON(overview) + + //@formatter:off + expectedJSONString := ` + { + "peers": { + "total": 2, + "connected": 2, + "details": [ + { + "fqdn": "peer-1.awesome-domain.com", + "netbirdIp": "192.168.178.101", + "publicKey": "Pubkey1", + "status": "Connected", + "lastStatusUpdate": "2001-01-01T01:01:01Z", + "connectionType": "P2P", + "iceCandidateType": { + "local": "", + "remote": "" + }, + "iceCandidateEndpoint": { + "local": "", + "remote": "" + }, + "relayAddress": "", + "lastWireguardHandshake": "2001-01-01T01:01:02Z", + "transferReceived": 200, + "transferSent": 100, + "latency": 10000000, + "quantumResistance": false, + "networks": [ + "10.1.0.0/24" + ] + }, + { + "fqdn": "peer-2.awesome-domain.com", + "netbirdIp": "192.168.178.102", + "publicKey": "Pubkey2", + "status": "Connected", + "lastStatusUpdate": "2002-02-02T02:02:02Z", + "connectionType": "Relayed", + "iceCandidateType": { + "local": "relay", + "remote": "prflx" + }, + "iceCandidateEndpoint": { + "local": "10.0.0.1:10001", + "remote": "10.0.10.1:10002" + }, + "relayAddress": "", + "lastWireguardHandshake": "2002-02-02T02:02:03Z", + "transferReceived": 2000, + "transferSent": 1000, + "latency": 10000000, + "quantumResistance": false, + "networks": null + } + ] + }, + "cliVersion": "development", + "daemonVersion": "0.14.1", + "management": { + "url": "my-awesome-management.com:443", + "connected": true, + "error": "" + }, + "signal": { + "url": "my-awesome-signal.com:443", + "connected": true, + "error": "" + }, + "relays": { + "total": 2, + "available": 1, + "details": [ + { + "uri": "stun:my-awesome-stun.com:3478", + "available": true, + "error": "" + }, + { + "uri": "turns:my-awesome-turn.com:443?transport=tcp", + "available": false, + "error": "context: deadline exceeded" + } + ] + }, + "netbirdIp": "192.168.178.100/16", + "publicKey": "Some-Pub-Key", + "usesKernelInterface": true, + "fqdn": "some-localhost.awesome-domain.com", + "quantumResistance": false, + "quantumResistancePermissive": false, + "networks": [ + "10.10.0.0/24" + ], + "dnsServers": [ + { + "servers": [ + "8.8.8.8:53" + ], + "domains": null, + "enabled": true, + "error": "" + }, + { + "servers": [ + "1.1.1.1:53", + "2.2.2.2:53" + ], + "domains": [ + "example.com", + "example.net" + ], + "enabled": false, + "error": "timeout" + } + ], + "events": [] + }` + // @formatter:on + + var expectedJSON bytes.Buffer + require.NoError(t, json.Compact(&expectedJSON, []byte(expectedJSONString))) + + assert.Equal(t, expectedJSON.String(), jsonString) +} + +func TestParsingToYAML(t *testing.T) { + yaml, _ := ParseToYAML(overview) + + expectedYAML := + `peers: + total: 2 + connected: 2 + details: + - fqdn: peer-1.awesome-domain.com + netbirdIp: 192.168.178.101 + publicKey: Pubkey1 + status: Connected + lastStatusUpdate: 2001-01-01T01:01:01Z + connectionType: P2P + iceCandidateType: + local: "" + remote: "" + iceCandidateEndpoint: + local: "" + remote: "" + relayAddress: "" + lastWireguardHandshake: 2001-01-01T01:01:02Z + transferReceived: 200 + transferSent: 100 + latency: 10ms + quantumResistance: false + networks: + - 10.1.0.0/24 + - fqdn: peer-2.awesome-domain.com + netbirdIp: 192.168.178.102 + publicKey: Pubkey2 + status: Connected + lastStatusUpdate: 2002-02-02T02:02:02Z + connectionType: Relayed + iceCandidateType: + local: relay + remote: prflx + iceCandidateEndpoint: + local: 10.0.0.1:10001 + remote: 10.0.10.1:10002 + relayAddress: "" + lastWireguardHandshake: 2002-02-02T02:02:03Z + transferReceived: 2000 + transferSent: 1000 + latency: 10ms + quantumResistance: false + networks: [] +cliVersion: development +daemonVersion: 0.14.1 +management: + url: my-awesome-management.com:443 + connected: true + error: "" +signal: + url: my-awesome-signal.com:443 + connected: true + error: "" +relays: + total: 2 + available: 1 + details: + - uri: stun:my-awesome-stun.com:3478 + available: true + error: "" + - uri: turns:my-awesome-turn.com:443?transport=tcp + available: false + error: 'context: deadline exceeded' +netbirdIp: 192.168.178.100/16 +publicKey: Some-Pub-Key +usesKernelInterface: true +fqdn: some-localhost.awesome-domain.com +quantumResistance: false +quantumResistancePermissive: false +networks: + - 10.10.0.0/24 +dnsServers: + - servers: + - 8.8.8.8:53 + domains: [] + enabled: true + error: "" + - servers: + - 1.1.1.1:53 + - 2.2.2.2:53 + domains: + - example.com + - example.net + enabled: false + error: timeout +events: [] +` + + assert.Equal(t, expectedYAML, yaml) +} + +func TestParsingToDetail(t *testing.T) { + // Calculate time ago based on the fixture dates + lastConnectionUpdate1 := timeAgo(overview.Peers.Details[0].LastStatusUpdate) + lastHandshake1 := timeAgo(overview.Peers.Details[0].LastWireguardHandshake) + lastConnectionUpdate2 := timeAgo(overview.Peers.Details[1].LastStatusUpdate) + lastHandshake2 := timeAgo(overview.Peers.Details[1].LastWireguardHandshake) + + detail := ParseToFullDetailSummary(overview) + + expectedDetail := fmt.Sprintf( + `Peers detail: + peer-1.awesome-domain.com: + NetBird IP: 192.168.178.101 + Public key: Pubkey1 + Status: Connected + -- detail -- + Connection type: P2P + ICE candidate (Local/Remote): -/- + ICE candidate endpoints (Local/Remote): -/- + Relay server address: + Last connection update: %s + Last WireGuard handshake: %s + Transfer status (received/sent) 200 B/100 B + Quantum resistance: false + Networks: 10.1.0.0/24 + Latency: 10ms + + peer-2.awesome-domain.com: + NetBird IP: 192.168.178.102 + Public key: Pubkey2 + Status: Connected + -- detail -- + Connection type: Relayed + ICE candidate (Local/Remote): relay/prflx + ICE candidate endpoints (Local/Remote): 10.0.0.1:10001/10.0.10.1:10002 + Relay server address: + Last connection update: %s + Last WireGuard handshake: %s + Transfer status (received/sent) 2.0 KiB/1000 B + Quantum resistance: false + Networks: - + Latency: 10ms + +Events: No events recorded +OS: %s/%s +Daemon version: 0.14.1 +CLI version: %s +Management: Connected to my-awesome-management.com:443 +Signal: Connected to my-awesome-signal.com:443 +Relays: + [stun:my-awesome-stun.com:3478] is Available + [turns:my-awesome-turn.com:443?transport=tcp] is Unavailable, reason: context: deadline exceeded +Nameservers: + [8.8.8.8:53] for [.] is Available + [1.1.1.1:53, 2.2.2.2:53] for [example.com, example.net] is Unavailable, reason: timeout +FQDN: some-localhost.awesome-domain.com +NetBird IP: 192.168.178.100/16 +Interface type: Kernel +Quantum resistance: false +Networks: 10.10.0.0/24 +Peers count: 2/2 Connected +`, lastConnectionUpdate1, lastHandshake1, lastConnectionUpdate2, lastHandshake2, runtime.GOOS, runtime.GOARCH, overview.CliVersion) + + assert.Equal(t, expectedDetail, detail) +} + +func TestParsingToShortVersion(t *testing.T) { + shortVersion := ParseGeneralSummary(overview, false, false, false) + + expectedString := fmt.Sprintf("OS: %s/%s", runtime.GOOS, runtime.GOARCH) + ` +Daemon version: 0.14.1 +CLI version: development +Management: Connected +Signal: Connected +Relays: 1/2 Available +Nameservers: 1/2 Available +FQDN: some-localhost.awesome-domain.com +NetBird IP: 192.168.178.100/16 +Interface type: Kernel +Quantum resistance: false +Networks: 10.10.0.0/24 +Peers count: 2/2 Connected +` + + assert.Equal(t, expectedString, shortVersion) +} + +func TestTimeAgo(t *testing.T) { + now := time.Now() + + cases := []struct { + name string + input time.Time + expected string + }{ + {"Now", now, "Now"}, + {"Seconds ago", now.Add(-10 * time.Second), "10 seconds ago"}, + {"One minute ago", now.Add(-1 * time.Minute), "1 minute ago"}, + {"Minutes and seconds ago", now.Add(-(1*time.Minute + 30*time.Second)), "1 minute, 30 seconds ago"}, + {"One hour ago", now.Add(-1 * time.Hour), "1 hour ago"}, + {"Hours and minutes ago", now.Add(-(2*time.Hour + 15*time.Minute)), "2 hours, 15 minutes ago"}, + {"One day ago", now.Add(-24 * time.Hour), "1 day ago"}, + {"Multiple days ago", now.Add(-(72*time.Hour + 20*time.Minute)), "3 days ago"}, + {"Zero time", time.Time{}, "-"}, + {"Unix zero time", time.Unix(0, 0), "-"}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + result := timeAgo(tc.input) + assert.Equal(t, tc.expected, result, "Failed %s", tc.name) + }) + } +} diff --git a/client/ui/bundled.go b/client/ui/bundled.go deleted file mode 100644 index e2c138b14..000000000 --- a/client/ui/bundled.go +++ /dev/null @@ -1,12 +0,0 @@ -// auto-generated -// Code generated by '$ fyne bundle'. DO NOT EDIT. - -package main - -import "fyne.io/fyne/v2" - -var resourceNetbirdSystemtrayConnectedPng = &fyne.StaticResource{ - StaticName: "netbird-systemtray-connected.png", - StaticContent: []byte( - "\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x01\x00\x00\x00\x01\x00\b\x06\x00\x00\x00\\r\xa8f\x00\x00\x00\xc3zTXtRaw profile type exif\x00\x00x\xdamP\xdb\r\xc3 \f\xfc\xf7\x14\x1d\xc1\xaf\x80\x19\x874\xa9\xd4\r:~\r8Q\x88r\x92χ\x9d\x1cư\xff\xbe\x1fx50)\xe8\x92-\x95\x94СE\vW\x17\x86\x03\xb53\xa1v>@\xc1S\x1dNɞų\x8c\x86\xa5\xf8\xeb\xa8\xd3d\x83T]-\x17#{Gc\x9d\x1bEGf\xbb\x19\xc5E\xd2&b\x17[\x18\x950\x12\x1e\r\n\x83:\x9e\x85\xa9X\xbe>a\xddq\x86\x8d\x80F\x92\xbb\xf7ir?k\xf6\xedm\x8b\x17\x85y\x17\x12t\x16\xd11\x80\xb4P\x90\xdaE\xf5\xf0\xa1\xfc#u-\x92:[L\xe2\vy\xda\xd3\x01\xf8\x03\xda\xd4Y\x17ݮ\xb7\xee\x00\x00\x01\x84iCCPICC profile\x00\x00x\x9c}\x91=H\xc3@\x1c\xc5_S\xa5\"-\x0e\x16\x14\x11\xccP\x9d\xec\xa2\"\xe2T\xabP\x84\n\xa5Vh\xd5\xc1\xe4\xd2/hҐ\xa4\xb88\n\xae\x05\a?\x16\xab\x0e.κ:\xb8\n\x82\xe0\a\x88\xb3\x83\x93\xa2\x8b\x94\xf8\xbf\xa4\xd0\"ƃ\xe3~\xbc\xbb\xf7\xb8{\a\b\x8d\nSͮ\x18\xa0j\x96\x91N\xc4\xc5lnU\f\xbcB\xc0\x00B\x18\xc1\xac\xc4L}.\x95J\xc2s|\xdd\xc3\xc7\u05fb(\xcf\xf2>\xf7\xe7\b)y\x93\x01>\x918\xc6t\xc3\"\xde \x9e\u07b4t\xce\xfb\xc4aV\x92\x14\xe2s\xe2q\x83.H\xfc\xc8u\xd9\xe57\xceE\x87\x05\x9e\x1962\xe9y\xe20\xb1X\xec`\xb9\x83Y\xc9P\x89\xa7\x88#\x8a\xaaQ\xbe\x90uY\xe1\xbc\xc5Y\xad\xd4X\xeb\x9e\xfc\x85\xc1\xbc\xb6\xb2\xccu\x9a\xc3H`\x11KHA\x84\x8c\x1aʨ\xc0B\x94V\x8d\x14\x13iڏ{\xf8\x87\x1c\x7f\x8a\\2\xb9\xca`\xe4X@\x15*$\xc7\x0f\xfe\a\xbf\xbb5\v\x93\x13nR0\x0et\xbf\xd8\xf6\xc7(\x10\xd8\x05\x9au\xdb\xfe>\xb6\xed\xe6\t\xe0\x7f\x06\xae\xb4\xb6\xbf\xda\x00f>I\xaf\xb7\xb5\xc8\x11з\r\\\\\xb75y\x0f\xb8\xdc\x01\x06\x9ftɐ\x1c\xc9OS(\x14\x80\xf73\xfa\xa6\x1c\xd0\x7f\v\xf4\xae\xb9\xbd\xb5\xf6q\xfa\x00d\xa8\xab\xe4\rpp\b\x8c\x15){\xdd\xe3\xdd=\x9d\xbd\xfd{\xa6\xd5\xdf\x0fںr\xd0VwQ\xba\x00\x00\rxiTXtXML:com.adobe.xmp\x00\x00\x00\x00\x00\n\n \n \n \n \n \n \n \n \n \n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\xf0C\xff\xd9\x00\x00\x00\x06bKGD\x00\xff\x00\xff\x00\xff\xa0\xbd\xa7\x93\x00\x00\x00\tpHYs\x00\x00\v\x13\x00\x00\v\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\atIME\a\xe8\x02\x17\r$'\xdd\xf7ȗ\x00\x00\x13;IDATx\xda\xed\x9d]o\x14W\x9a\xc7\xff\xa7\xaamh\xbf\xc46,I`\x99\xa1\xc3\ni\xb5{1\x95O0\xe4\x1b\xc0'X\xf2\t`.W`hp\xa2\xb9\fH{O\xa3\xcc\xc5\xecJ3q\xa4\x1d\xed\xcdJx>Aj/\"EBJګL \xb1\x00g\xf1\v\xb6\xbb\xeb\xec\x85mb\f\xb6\xfb\xa5^Ω\xfa\xfd\xee\x928v\xf7\xa9z\xfe\xcfs\x9e\xa7ο$\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\u0603a\t\xc0g\xd6\x7f\x1f5\x92\x8e\"k\xd4\b\xa4s\xb2jH\x9afez\n\xfe\xdb\b\x00x\x81mF\xd3/CE]\xa3(\x94~c\xa5\x8b;\xc1\x0e\x83E\x7f{\xecF\xfcA\x8d\x95\x00g\xb3\xfb\\tQ\xd2o\xadtq]\xba(I\x81\x95,K345\xa3˒\x84\x00\x80SY~5Х0\xd0o\x13\xabK\x96R>\x9b\xe4o\xd4\x1a\xbd\x1e\xc7\b\x008\x93\xe9\xadtkM\x8a\x02i\xdaZ\x9aS\x99\x12\xea\xf6\xabJ\x80Հ\x02\xf7\xf4W\x13\xe9\xdan\xa6'\xe8sXw\xe9\xf6ؿ\xc6\xed_Z\x01\x00\x05d{\xed\xec\xe9!\xcf\xda\x7f\xbb\xf1\xf7Z/\x80U\x81<\x03\xdf\x12\xf8\xc5\xc5\x7f\xf2K\xe9O\x05\x00d\xfcje\xffx\xecF\xfc\xe1\xfe\x7fM\x05\x00\xd9\x04~3j$5ݲVWX\r\a\xe2?\xdc\x1e\xfb!\x00\x909ks\xd1\xd5Dj\x1a\xcb\x18ω\xe07j\xd5\xf74\xfe\x10\x00Ȅ\x95O\xa3(Ht_R\xc4\xdeҙҿ\xbdw췟\x80\x15\x82\xb4\xb2~\x90\xe8+I\x11\xab\xe1\x0e\xd6\xea\xc1A\xd9\x7f[\x1f\x00\x86\xdc\xeb\xdbP\xf7E\x93\xcf\xc9\xec\xbf\x7f\xec\xc7\x16\x00\xd2\v\xfe\xb9\xe8b\"}axd\xd7\xcd\xf8O\x0e.\xfd\xd9\x02\xc0\xb0\xc1\x7f\xcbJ\x0f\t~G\t4_\xbf\x19\xb7\x8e\xfa1*\x00\xe8\x9b\xd5O\xa2\xfb\x8c\xf7\x1c\xcf\xfe\x81~\xd7\xcb\xcf!\x00\xd03\xb6\x19M\xaf\x87\xfaB\x96\xfd\xbe\xd3\xc1\x7f\xc8؏-\x00\fV\xf27\xa3\xc6z\xa8\x87\xa2\xd9\xe7x\xf4\x1f>\xf6\xa3\x02\x80\x81\x82\xdf\xd6\xf4\x10\a\x1e\x0f\xe2?\xd1\xed\xfa\x8d\u07b2\xff\xb6^\x00\x10\xfc\xa5\xc9\xfeG\x8d\xfdJW\x01\xd8f4\xfd\xf2ؾNt\xe7\xe8\x9b5\f\xb4\xdc\r\xb4\xbc\xfb\xcf\xc77\xb4l\x9a\xf12wѾ=?\xc1\xef\xcf\xf5\xb2\xbd5\xfe\x9c\xac\x00\xd6\x7f\x1f5\xc2D\xd3[\x89\x1a\xd6jZ\x81\xa6\x8d\xd5t`tn\xe7\xcb5$M\xcb\xec\x04{\x867\xa5\x95\x96\x8dѲ\xacvK\xa9ec\xb4\x9cX-Z\xa3ec\xd5\x0e\xa4e\xd5\xd4\xee\xb5\xd9\xe2#ks\x11O\xf6\xf9\x92\xfc\x8dZ\xf5\x1b\xf1\xc7N\n\x80mF\xd3[#jlv\x15)\xd0\xf4\x1e\xfb憌\xa6\xbd\xcf0F\xed\x1d\xb1X\xb6\xd2\xffX\xabvh\xd4>\xdeU\xeckU\xb1v'\xfaLF\xd7\b-On\xc1\x9a>\x18$\x19\x99,\x82<\b\xf4\x1b\xb3\xed\xed\x16Y\xa9Q\xe5\x87E\xac\xb4l\xa4XFq\"-\x86V\xb1\xeb°\xf3\x90O\x93\xb0\xf2\xe6\x1e\xbb=>\x1b\x0ft\xbd\xcc\xc0\x81nuq'\x93Gv\xfb\xf4\x17O\x84\xf5G,\xa9\x9d\x18\xfd5\xb4\x8a\xeb\xb3\xf1\x82\v\x1fju.\xbad\xa4/\xb8<\xfeT\x9f\xf5\x8e>\x1c4\xa1\x98\xa3\x82}-\xd4Ek\xd4\xe0e\f\xb9\xb0 \xa3\xd8Z\xfdu\xac\xab\x85\xbc\xab\x04:\xfe\x1eƿ\xd5ǽ<\xf2ۓ\x00l~\x1aE\x9bV\x17\tv\x87\xaa\x04\xa3\x05c\xf5e\x1e\x15\xc2\xda'\xd1w\\s\xbf\xb2\x7f\xbfc\xbf7~\xc5\xda\\tU\xd2%\xcax/z\t\v\x89\u0557\xe1\x88\x16Ҟ>\xb0\xef\xf70\xfe\al\xfc\xbd\xf6;V>\x8d\"\x93p\xaa\xcb\xc7\xedBb\xf5 \r1\xd89\xd3\xff\x1dK\xeaQ\xf0\x0f8\xf6{\xeb\x16`ǹ\xf5!\xcbZM1X\x9d\x8b\xbe2\xcc\xfb\xbd\xaa\x06\x83\x9a>L\xa3\n|\xd5\x03X\xbf\x13]\xb1F\xf7Y^\uf677҃\xf1\xd9x\xbe\x97\x1f^\xb9\x13]\t\xb8\xee\xbe\t\xc0\xc0c\xbf\x03\x05\x00\x11([\x8d\xa8\xb6\x12͛\x11\xdd;,S\xd0\xf8\xf3\xef\xba\x0e\xdb\xf8\xdb\xcbkǁ\xeb7\xe3\x96U\xefG\t\xc1\xe94ѐ\xd15\xdb\xd1w\xab\x9fD\xf7w^\xb5\xfd\xfa\xde\x7f.\xbaE\xf0{\x16\xffI\xba\xf1i\x0e\xd8\x136\xcd\xf6\xdb\\\xa0d\xd9#It{\xe2fܢ\xf1\xe7\xe5\xf5[\x18\xbb\x11\x7f\x94\xb9\x00 \x02\x15\x10\x82\xe7\x13\xed`z\xe5\"\x8b\xe1\xd1eKa\xec׳\x00 \x02%\xde\x1dlִ\xf5\xf5\xafeF;\nO?Wp\xe2\x05\x8b\xe2z\xf0\xa74\xf6;\xb4\a\xb0\x9f\xf1ٸ\x99H\x0fX\xfer\xd1yt\xe6\x95\x10t\x16Oi\xeb\xeb_+y6\xc9\xc28\\\xb1\xf5c\xf3\x95\x9a\x00H\xd2\xc4l|\x05\x11(\x0fɳI\xd9\xcd\xda\x1b\x15\xc1\xae\x10ؕ:\x8b\xe4\x1e\xf7\xb2\xf2\x9d\xe8\xf94\xe0\xfa\\\xf4\x90w\xbb{^\xfaw\x03u\xbe9\xfb\x86\x00\xbc\x91\x15N\xac(<\xfd\\ft\x8bEs \xfb\xa79\xf6\xeb\xbb\x02\xd8\xe5eW\x97\xed\xf6\x11V\xf05\xfb\xff4ud\xf0oW\t\x13\xda\xfa\xfaW\xea>\x9eaъ\x8e\xff$۱|_~\x00ϛ\xd1\xf4h\xa8\x87<6\xeaa\xf6\xdfi\xfc\xf5}\x83\x8cvT\xbb\xf0\x98j\xa0\x88\xe0Ϩ\xf17P\x05 I3\xcdx9\xe8게\xda\\\x1e\xbf\x184\x9bo\vǯ\xd4\xfd\xfe\xa4l\x97\xd7H\xe4J\x98\xfdCy}_\xd1z3n\x9b\x8e>B\x04<\xca\xfe+\xf5\xa1\xbb\xfcݥ\xa9\x9d\xfe\xc1\b\v\x9a\xc75\x93n\xe7a8;\x90\xa4#\x02~\xd1Y<\x95\xe26\x82\xde@\xf6\xb5\xbf\xdaAM\xad<\xfe\xd4\xc05\x1d\"\xe0\ao\x1b\xfb\r\xbd\x9dx2\xa3-\xaa\x81\xec\xe2?\xc9'\xfbok͐`(\xe2p\x19\xb9YS\xe7љ\xd4\x05\xe0\xd5\xcd3\xdaQx\xf6\xa9\x82\xa9U\x16;\xc5\xec\x9f\xe5\xd8/\xb5\n`\x97\x89\xebql\x03}d%ު\xe3\x18\xdd\xc73\x99\x05\xff+\x81\xf9\xf6=\xb6\x04)R3\xba\x9c\xe7\xdfK\xa5\xad;q=\x8e%}\xcc\xe5s+\xfb\xe7\xf5xo\xf7Ɍ:\x8b\xef2%\x186\xf9\x1b\xb5F\xb7c\xc9/\x01\x90\xa4\xf1\xd9x\xdeXD\xc0\xa5\xec\x9fo\xafa\x82)\xc1\xb0\x84\xf9{q\xa4*\xd9\xf5\x9bqK\xa6\xff\x17\x14B\xda\xc18Y\xc8\xe1\x9e\xed\x9e\xc3iD`\x90\xb5S~\x8d\xbf\xd7[\x0e\x19\xc01\xe2b\xd9\xfa\xfaי\xee\xfd\x8f\xced\x89F.<\x96\xa9op1z\x8b\xc2\\\x1b\x7f\x99U\x00{\xb6\x03M\xac\xc5\n*\xfd{|\xde?\xdb\x0f\x11h\xeb\xd1i%?\x8fsAz\x89\xff\xa4\xb8X\xc9\xf4\xed\xc0T\x02E\x94\xe0g\x8a\x17\x80\xbd\xc5\xc0\xb9%\x85\x18\x8e\x1c\x16\x81\xf1؍\xf8â\xfe|\xa6m\xdb\x1dC\x91{\\\xe5\x9c\x12o\xc6c\xbf\x81>\xd3\xe2)u1\x1b98\xfe\xc3|\xc7~\xb9\n\x80$M\xcc\xc6\xd70\x14\xc9'\xfb\xbb\xea\xea\x83\b\x1c\x10\xfcF\xad\"\x1a\x7f\xb9\n\xc0\x8e\b\xe0*\x941\x9do\xdfw\xbb:A\x04\xf6\x97\xfe\xed\"\xc6~\x85\b\x80$muu\rC\x91lH\x9eMʮ\x8f\xba\xbfE\xf9\xfe\xa4\xec\xfa1.\x98$k\xf5\xa0\xe8쿭C9\x82\xa1HF\xe2Z\xf4د\x1f\xc2D#\xff\xf8\xb7j\x1b\x8c\x148\xf6+\xac\x02\x90\xb6\rE6\xbb\x9c L5\xab:\xd8\xf8;\xfc\x03\a\x95\x7fX\xa8ȱ_\xa1\x02\xb0+\x02\x1c#N\xa9\x8cܬ\xa9\xfbd\xc6\xcb\xcf\xdd\xf9\xf6\xbdJ\x9e\x1d0F\xad\xfa\u0378UY\x01\x90\xf0\x12H3\xfb{+^\xeb\xa3\xea~\xffwջh\xa1[\x0f\xc8\x15&\xc1\x88\xc0\xb0\x01t\xcc\xfb\x97y$\xcf&*u\x94\u0605\xb1\x9f3\x02\xb0W\x04\xf0\x12\xe8\x9fη\uf563\x8ay2S\x8d\x97\x9182\xf6sJ\x00vE\x00C\x91~3\xe7\xa4_\x8d\xbf#\xd8\xfa\xf6\xbd\xd27\x05\xf3\xb4\xf9\xeaO\x97\x1c\x01k\xb1\x1eK\x7f\a\x9f\xf7O\xe5F\x9cx\xa9\x91\v?\x946\xfb\xbb2\xf6s\xae\x02\xd8e\xe2z\x1c\a\x16/\x81#\xb3\xff\xd3\xc9\xd2\x05\xbf$ٕ\xe3\xea.M\x953\xfe\x1d6\xcaqj\x0eS\xbf\x19\xb7p\x15:<\xfb\xfb8\xf6\xeb\xb9\x1f\xf0\xfd\xc9\xd2\xf5\x03\x8cQ\xab>\x1b/ \x00}\x88\x00\xaeB\a\x04H\x05:\xe6\x9d\xc5S\xe5z> t\xdb\x17\xc3ɕ\x1e\xbb\x11\xdf\xc5Pd_\xf6\xffy\xdc\xfb\xb1_\xafUNR\x12\xa1+\xca\xe6\xcb{\x01\x90p\x15z#3~\x7f\xb2:\x95\xceҔ\xff[\x01\xa3\xf6XWw]\xff\x98N\xd7Z\x88\xc06e\x1b\xfbUA\xf0L\xa2ۦ\x19;?\xda6>,\xe6\xca\\t7\x90\xaeV\xb2\xf4/\xe9د\xa7\xed\xf3٧\nO\xfd\xecg\xf6wt\xec\xe7U\x05\xb0K\x95]\x85\xbc;\xed\x97\xf6w\xf7\xb0!hB}\xe4\xcbg\xf5fu\xab\xe8*\xe4\xb2\xcdW>\n\x10xw`\xc8\xc5\xe7\xfdK!\x00R\xf5\\\x85*yZn\x1fɳ\to\x1a\x82VZv}\xec\xe7\xb5\x00\xec\x1a\x8aTA\x04\x92g\x93J~\x1e\x13H\x1d\x7fƂ\xf7|\xca\xfe\x92'M\xc0\xfd\xac7\xa3\x86\xad顬\x1ae\xbd齲\xf9ʁ\x91\v\x8fe&\xd6]\x8e$o\x1a\x7f\xdeV\x00\xbb\x94\xddK\xa0ʍ?_\xab\x00\x97l\xbeJ/\x00e\x16\x01\xbbYSR\xd2C1C\xad\xcb\xcaqw{\x01\x81\xe6]\xb2\xf9\xaa\x84\x00\x94U\x04|\x1d}U\xb9\n0\x81\xbfgW\xbc\xbf\xd3\xca\xe4*T\xf9\xb1\x9f\x87U\x80oc\xbf\xd2\t\xc0\xae\b\x94\xc1U\xa8\xf3\xe8\fQ\xeeS\x15\xe0\xa8\xcdW\xe5\x04@\xda1\x14Q\xb1/Z\x1c\x86*>\xef\xef{\x15\xe0\xaa\xcdW%\x05@\x92\xea\xb3\U000423c6\"\xb6\x1bT\xca\x1d\xb7\x14U\x80Q\xdb\xd7\xc6_i\x05@\xf2\xd3U(\xf9i\x8a\xec\xdfo\x15P\xb0\x89\xa8-\x89}])\xdb\xcd\xf5\x9bq˗c\xc4e\xb7\xf9\xcan\xcb4Q\\\xf27j\x8d\xcf\xc6\xf3\b\x80\xc3\xf8\xe2%@\xe9?\xe0\xba\xfd4Uܸ4,\x8fGE\xa9\aή\x8b\x80]\xa93\xf6\x1bX\x01\x82B\xd6\xce\a\x9b/\x04\xc0\x13\x11\xe8,\x9e\"\x90\x87\xd9\x06,\x8f\xe7\\\xfb\xab\x1d\xd4\xd4*\xd3\x1aV⑳\xf1ٸ隗\x00c\xbf4*\xa8\xe3\xb2\xeb\xc7\xf2\x8b\xff\xa4\\ٿ2\x02 \xb9e(b7k\xec\xfd\xd3\x12Ҽ\x8eL\x97d\xecWY\x01\xd8\x15\x01#-\x14~\xd32\xf6K\xaf\x15\xf0S>\a\xa7j\xc6߇\xcc\x10\x80=\xbc\xec\xear\x91\x86\"v\xb3V\xdaW`\x15\xa3\x00A\xe6O\x06\x1a\xa3\xd6\xe8\xf58F\x00J@ѮB\x94\xfe\x19\xaci\xd6ۀ\xb0\xbc\xd6\xf4\x95@X\xbd\xec\x8f\x00d \x02\x8c\xfd\x1c\xe8\x03\xf4)\x02I\xc5\x1a\x7f\b\xc0\x80\"Ћ\xa1\bo\xf7q\xa1\x0f\xd0\xc76\xc0\xa8\x1d\xd6t\xb7\xaak\x85\x00\xf4\xc1Q\xaeB\xd8|\xb9\xb2\r\xe8\xdd&\xac\x8c6_\xfd`\xb8]\xfagu.\xfa\xcaH\xd1k7]7P盳\b\x80\v7\xf5hG#\xff\xfc\xbf=e\xff\xb1\x1b\xf1\aU^+*\x80\x01x\x9b\xa1\b6_\x0eU\x00\x9b\xb5\x9e\x9a\xb0>\xbeF\x0e\x01p\x80]W\xa1\xdd\x13\x84\xbc\xdd\xc7A\x8e\xd8\x06\x18\xa3V}6^@\x00``\x11\xd8=F\xcc\x13\x7f\xee\x91\x1c5\t\xa8\xe8\xd8\x0f\x01H\x91z3n'\x1b\xb5{\xae\xbc\xae\x1a\xf6l\x03\x0e\xa9\x00\xcan\xf3\x85\x00乀\xc7:Wk\x17~\x90\u0084\xc5pI\x00\xd6\x0e\xa8\x00\x8c\xdac\xdd\xea\x8e\xfd\x10\x80\x14Y\x9b\x8b\xaeʪaF;\x1a\xb9\xf0\x18\x11pI\x00\x0ehȚD\xb7M3^f\x85\x10\x80\xa1XoF\r\x19]{uc\xd574r\xfeG\x16\xc6\x15\xba\xc1\x9b\x0eA%}\xbb\x0f\x02P\x00IM\xb7d\xd5x\xed\xfe\x9aXWxn\x89\xc5q\xa6\x0f\xf0\xfa6\xc0\x84\xfa\x88UA\x00R\xc9\xfe\xc6\xea\xca\xdb\xfe[x\xe2\x05\"\xe0\xe06\xa0J6_\b@\xd6\xd9?\xd4\x17\x87\xfd\xf7\xf0\xc4\v\x85\xa7\x9f\xb3P\x8e\b\x80\x95\x96\x19\xfb!\x00\xa9\xb0r'\xba\xb2\xff1්\xc0\xfb\xcf\x11\x01w*\x80{d\x7f\x04 \x9d\x05\vt\xabןE\x04\nf\xedX%\xde\xee\x83\x00\xe4\xb5\xf7\x9f\x8b\xdeh\xfc!\x02\x0eW\x00ݠ\x926_\xfd\xc0i\xc0^\x83\xbf\x195l\xa8\xef\x06\xfd\xff;\x8b\xa70\n\xc9\xfb\xe6~g\xbd=\xd5\xfa\xaf\x0fX\t*\x80\xa1Ij\xbd\x97\xfeo\xa3vnI\xc1\x89\x17,d\x8e\x84'V\xc8\xfeT\x00\xc5g\xff\xbdl=:û\x02\xf2\xc8lc\x9b\xf3\xef\xfc\xe1?/\xb3\x12T\x00\xc3\xef%kz\x98\xd6\xef\x1a9\xffD\xa6\xbeɢf\x99\xd5F;\xeav&~\xc7J \x00C\xb3r'\xba\xd2o\xe3\xef\xf0\xba4\xd1ȅ\x1f\x10\x81,K\xff\xa9\xf5\xd6\xcc\x1f\xff\xd8f%\x10\x80\xa1K\xff~\xc6~\xfd\x88@\xed\xfc\x13\x99\xd1\x0e\x8b\x9c>\xed\xb0\xb1\xc4\xde\x1f\x01H#P\xf5/\xa9f\xff}ej\xed\xc2\x0f\x88@\xda7\xf4\xf4\x1ag\xfd\xfb\xb9\x0fY\x82\x83\xb3\x7fZ\x8d\xbfC\xfb\v\x9b5u\x1e\x9d\xc1O0\x8d\x9b\x99\xb1\x1f\x15@Z\f;\xf6\xa3\x12(\xe0f\x9e\xd8\xfa\x98U@\x00\x86fu.\xbat\xd0i\xbf\xccD\xe0\xfc\x8f\x18\x8a\fs#\x8fo\xb4&\xff\xed\xbf\x17X\t\x04`\xf8\x804\xfa,\xf7\xbfY\xdf\xc0Uh\b\x01\x1d9\xb9J\xe3\x0f\x01\x18\x9e\xd4\xc7~}\x8a@\r/\x81\xfe\x19\xedܮ\xdf]h\xb3\x10\b\xc0Pd6\xf6\xeb\xe7\x82L\xadb(\xd2\x1f\xedw\xce\xff\x80\xc9'\x020\xff\v\x8d?*\x80\xe1qe\xec\xd7\x0f\xb5\xb3O+\xed%\x10\x9e\xfa?J\x7f*\x80\x94\xb2\xbfcc\xbf\x9e\xe9\x06\xdb\xd6b\xeb\xa3\xd5\xcaV#\xc9\xfc;\xff>\x8f\xcd\x17\x15@\n\xd9\xff\x88\xb7\xfb\xb8\x9d\x06w\\\x85*t\x82Ќv\xd45DZ\xf9B\x00\x86\xa7\u05f7\xfb\xb8.\x02U:F\x8c\xcd\x17\x02\x90ޗv|\xec\xd7OV\xac\x88\b`\xf3\x85\x00\xa4\xb4\xf7\xf7d\xec\x87\b\xfc\x82\x95\xb0\xf9\xca\xea\xfe\xa9T\xf0\xfb\xdc\xf8;*H6k\xda\xfa\xe6\xac\xd4-\x97\xa6\xf3\xbc?\x15@j\xe4e\xf3UT%PFC\x11l\xbe\xa8\x00Ra\xe5\xd3(\n\x12}U\xf6\xefi\u05cfi\xeb\xd1\xe9RT\x02\xc1\xf8F\xeb\x9d\xcf\xff\x82\x00P\x01\xa4\xf0E\xad\xc7c\xbf~\x14\xbd\xbeQ\n/\x013\xdaQwk\x92\xc6\x1f\x02\x90B\xf6/\xd0\xe6\xab\b\xc2\x13/\xfcw\x15\x1a\xed\xdcf\xec\x87\x00\f\x8d\v6_\x85\x89\x80\xbf^\x02\xd8|!\x00)\xed\x89\x03]\xadR\xf6\x7fM\x04<5\x14\t\xde_\xc6\xe6+\xaf\xadVٳ\x7fY\xc7~\xfd\xe0\x93\xb5\x18c?*\x80\xd4(\xf3د\xac\x95\x006_\b@*\xac܉\xae\xe4\xf9v\x1f/D\xc0qW\xa1`|\x83\xe7\xfd\x11\x80\x94\xbeX@\xf6\x7fC\x04\xdcv\x15\xc2\xe6\v\x01H\x87\xb5\xb9\xa8\xb2\x8d\xbf\xa3\xa8\x9d[\x92\x99x\xe9\xde\xde\x1f\x9b\xafbֽl_\xc8G\x9b\xaf\xdcq\xccP\xc4Ԓ\xf6\xd4\x7f\xcc\xd3\xf8\xa3\x02\x18\x1e\x1fm\xbe\xf2\xdf\v\xec\x18\x8a8b-\x16\x9e}J\xe9O\x05\x90R\xf6g\xec\xd73v\xb3\xa6Σ3\xb2\x9bŽ\x1e\x02\x9b/*\x80\x14S\x89\xeesI\xfbP\xff\x82\xbd\x04\xb0\xf9B\x00Rc\xe5Nt\xc5J\x17\xb9\xa4\xfe\x88\x80\x19\xe92\xf6C\x00R\xfa\"\x8c\xfd|\x13\x01\xc6~\b@J{\xff\x92\xd9|\x15&\x02\xe7\x7f\xcc\xcdP\x04\x9b/G\xae\xbb\xf7\xc1ߌ\x1aI\xa8\xaf\x8c4\xcd\xe5L!0s0\x14\xe1\xed>T\x00\xa9\x91\xd4t\x8b\xe0O18\xeb\x1b\x1a9\xffc\xb67\xdd\xd4\x06.?T\x00\xe9d\x7f\xc6~\xd9\xd0}6\xa9\xee\xe2\xa9\xf4o8cZS\x7f\xfa\x13\x02@\x05\x90B\xb9Z\xd3C.a6d\xe1*dF;JFFh\xfc!\x00\xc3S5\x9b\xaf\xc2D \xcdc\xc4\xd8|!\x00\xa9d\xfef4\xcd\xd8/'\x11H\xcfK\x00\x9b/\x04 \x1d^\x86ⴟg\"\x10\x1c\xdf\xc2\xe6\xcbA\xbck\x02\xd2\xf8+\x8eA\xadŰ\xf9\xa2\x02H\rl\xbe\x8a\xad\x04\x061\x14\xc1\xe6\v\x01H\x85չ\xe8\x126_\xc5R;\xb7ԗ\b`\xf3\x85\x00\xa4\xb7_1\xfa\x8cK\xe6\x86\b\xf4\xe4%\x10&\xcb#'W\x19\xfb!\x00\xc3\xc3\xd8\xcf-z1\x141\xf5\xcd{\xf5\xbb\vd\x7f\x97\x93\xaa\x0f\x1f\x12\x9b/G\xe9\x06\xda\xfa\xe6\xecA\x86\"\xed\xe9?\xff\x99\xc6\x1f\x15\xc0\xf0`\xf3\xe5(ar\xe01\xe2Zc\x89ҟ\n \xa5\xec\xcf\xd8\xcfi\xf6[\x8ba\xf3E\x05\x90\xde\xcd\x15\xd2\xf8s>\x8b\xec1\x141a\x82͗G\xd4\\\xfep\xabs\xd1%\x19E\x92\xda\\*\xc7E\xe0XG#\xff\xf0X\x1b\x8b\xa7\xbe\x9c\xf9\xc3<\xd7\v\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00|\xe4\xff\x01\xf6P(\xf3)+S\x1f\x00\x00\x00\x00IEND\xaeB`\x82"), -} diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go index 9ed40b0be..100076806 100644 --- a/client/ui/client_ui.go +++ b/client/ui/client_ui.go @@ -148,22 +148,24 @@ type serviceClient struct { icError []byte // systray menu items - 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 - mRoutes *systray.MenuItem - mAllowSSH *systray.MenuItem - mAutoConnect *systray.MenuItem - mEnableRosenpass *systray.MenuItem - mNotifications *systray.MenuItem - mAdvancedSettings *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 + mNetworks *systray.MenuItem + mAllowSSH *systray.MenuItem + mAutoConnect *systray.MenuItem + mEnableRosenpass *systray.MenuItem + mNotifications *systray.MenuItem + mAdvancedSettings *systray.MenuItem + mCreateDebugBundle *systray.MenuItem + mExitNode *systray.MenuItem // application with main windows. app fyne.App @@ -200,6 +202,14 @@ type serviceClient struct { wRoutes fyne.Window eventManager *event.Manager + + exitNodeMu sync.Mutex + mExitNodeItems []menuHandler +} + +type menuHandler struct { + *systray.MenuItem + cancel context.CancelFunc } // newServiceClient instance constructor @@ -473,6 +483,9 @@ func (s *serviceClient) updateStatus() error { status, err := conn.Status(s.ctx, &proto.StatusRequest{}) if err != nil { log.Errorf("get service status: %v", err) + if s.connected { + s.app.SendNotification(fyne.NewNotification("Error", "Connection to service lost")) + } s.setDisconnectedStatus() return err } @@ -498,7 +511,8 @@ func (s *serviceClient) updateStatus() error { s.mStatus.SetTitle("Connected") s.mUp.Disable() s.mDown.Enable() - s.mRoutes.Enable() + s.mNetworks.Enable() + go s.updateExitNodes() systrayIconState = true } else if status.Status != string(internal.StatusConnected) && s.mUp.Disabled() { s.setDisconnectedStatus() @@ -554,7 +568,9 @@ func (s *serviceClient) setDisconnectedStatus() { s.mStatus.SetTitle("Disconnected") s.mDown.Disable() s.mUp.Enable() - s.mRoutes.Disable() + s.mNetworks.Disable() + s.mExitNode.Disable() + go s.updateExitNodes() } func (s *serviceClient) onTrayReady() { @@ -577,10 +593,16 @@ func (s *serviceClient) onTrayReady() { s.mEnableRosenpass = s.mSettings.AddSubMenuItemCheckbox("Enable Quantum-Resistance", "Enable post-quantum security via Rosenpass", false) s.mNotifications = s.mSettings.AddSubMenuItemCheckbox("Notifications", "Enable notifications", true) s.mAdvancedSettings = s.mSettings.AddSubMenuItem("Advanced Settings", "Advanced settings of the application") + s.mCreateDebugBundle = s.mSettings.AddSubMenuItem("Create Debug Bundle", "Create and open debug information bundle") s.loadSettings() - s.mRoutes = systray.AddMenuItem("Networks", "Open the networks management window") - s.mRoutes.Disable() + s.exitNodeMu.Lock() + s.mExitNode = systray.AddMenuItem("Exit Node", "Select exit node for routing traffic") + s.mExitNode.Disable() + s.exitNodeMu.Unlock() + + s.mNetworks = systray.AddMenuItem("Networks", "Open the networks management window") + s.mNetworks.Disable() systray.AddSeparator() s.mAbout = systray.AddMenuItem("About", "About") @@ -599,6 +621,9 @@ func (s *serviceClient) onTrayReady() { systray.AddSeparator() s.mQuit = systray.AddMenuItem("Quit", "Quit the client app") + // update exit node menu in case service is already connected + go s.updateExitNodes() + s.update.SetOnUpdateListener(s.onUpdateAvailable) go func() { s.getSrvConfig() @@ -614,6 +639,12 @@ func (s *serviceClient) onTrayReady() { s.eventManager = event.NewManager(s.app, s.addr) s.eventManager.SetNotificationsEnabled(s.mNotifications.Checked()) + s.eventManager.AddHandler(func(event *proto.SystemEvent) { + if event.Category == proto.SystemEvent_SYSTEM { + s.updateExitNodes() + } + }) + go s.eventManager.Start(s.ctx) go func() { @@ -628,7 +659,7 @@ func (s *serviceClient) onTrayReady() { defer s.mUp.Enable() err := s.menuUpClick() if err != nil { - s.runSelfCommand("error-msg", err.Error()) + s.app.SendNotification(fyne.NewNotification("Error", "Failed to connect to NetBird service")) return } }() @@ -638,7 +669,7 @@ func (s *serviceClient) onTrayReady() { defer s.mDown.Enable() err := s.menuDownClick() if err != nil { - s.runSelfCommand("error-msg", err.Error()) + s.app.SendNotification(fyne.NewNotification("Error", "Failed to connect to NetBird service")) return } }() @@ -676,6 +707,13 @@ func (s *serviceClient) onTrayReady() { defer s.getSrvConfig() s.runSelfCommand("settings", "true") }() + case <-s.mCreateDebugBundle.ClickedCh: + go func() { + if err := s.createAndOpenDebugBundle(); err != nil { + log.Errorf("Failed to create debug bundle: %v", err) + s.app.SendNotification(fyne.NewNotification("Error", "Failed to create debug bundle")) + } + }() case <-s.mQuit.ClickedCh: systray.Quit() return @@ -684,10 +722,10 @@ func (s *serviceClient) onTrayReady() { if err != nil { log.Errorf("%s", err) } - case <-s.mRoutes.ClickedCh: - s.mRoutes.Disable() + case <-s.mNetworks.ClickedCh: + s.mNetworks.Disable() go func() { - defer s.mRoutes.Enable() + defer s.mNetworks.Enable() s.runSelfCommand("networks", "true") }() case <-s.mNotifications.ClickedCh: @@ -718,7 +756,11 @@ func (s *serviceClient) runSelfCommand(command, arg string) { return } - cmd := exec.Command(proc, fmt.Sprintf("--%s=%s", command, arg)) + cmd := exec.Command(proc, + fmt.Sprintf("--%s=%s", command, arg), + fmt.Sprintf("--daemon-addr=%s", s.addr), + ) + out, err := cmd.CombinedOutput() if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 { log.Errorf("start %s UI: %v, %s", command, err, string(out)) @@ -737,7 +779,11 @@ func normalizedVersion(version string) string { return versionString } -func (s *serviceClient) onTrayExit() {} +func (s *serviceClient) onTrayExit() { + for _, item := range s.mExitNodeItems { + item.cancel() + } +} // getSrvClient connection to the service. func (s *serviceClient) getSrvClient(timeout time.Duration) (proto.DaemonServiceClient, error) { diff --git a/client/ui/config/config.go b/client/ui/config/config.go deleted file mode 100644 index fc3361b61..000000000 --- a/client/ui/config/config.go +++ /dev/null @@ -1,46 +0,0 @@ -package config - -import ( - "os" - "runtime" -) - -// ClientConfig basic settings for the UI application. -type ClientConfig struct { - configPath string - logFile string - daemonAddr string -} - -// Config object with default settings. -// -// We are creating this package to extract utility functions from the cmd package -// reading and parsing the configurations for the client should be done here -func Config() *ClientConfig { - defaultConfigPath := "/etc/wiretrustee/config.json" - defaultLogFile := "/var/log/wiretrustee/client.log" - if runtime.GOOS == "windows" { - defaultConfigPath = os.Getenv("PROGRAMDATA") + "\\Wiretrustee\\" + "config.json" - defaultLogFile = os.Getenv("PROGRAMDATA") + "\\Wiretrustee\\" + "client.log" - } - - defaultDaemonAddr := "unix:///var/run/wiretrustee.sock" - if runtime.GOOS == "windows" { - defaultDaemonAddr = "tcp://127.0.0.1:41731" - } - return &ClientConfig{ - configPath: defaultConfigPath, - logFile: defaultLogFile, - daemonAddr: defaultDaemonAddr, - } -} - -// DaemonAddr of the gRPC API. -func (c *ClientConfig) DaemonAddr() string { - return c.daemonAddr -} - -// LogFile path. -func (c *ClientConfig) LogFile() string { - return c.logFile -} diff --git a/client/ui/debug.go b/client/ui/debug.go new file mode 100644 index 000000000..845ea284c --- /dev/null +++ b/client/ui/debug.go @@ -0,0 +1,50 @@ +//go:build !(linux && 386) + +package main + +import ( + "fmt" + "path/filepath" + + "fyne.io/fyne/v2" + "github.com/skratchdot/open-golang/open" + + "github.com/netbirdio/netbird/client/proto" + nbstatus "github.com/netbirdio/netbird/client/status" +) + +func (s *serviceClient) createAndOpenDebugBundle() error { + conn, err := s.getSrvClient(failFastTimeout) + if err != nil { + return fmt.Errorf("get client: %v", err) + } + + statusResp, err := conn.Status(s.ctx, &proto.StatusRequest{GetFullPeerStatus: true}) + if err != nil { + return fmt.Errorf("failed to get status: %v", err) + } + + overview := nbstatus.ConvertToStatusOutputOverview(statusResp, true, "", nil, nil, nil) + statusOutput := nbstatus.ParseToFullDetailSummary(overview) + + resp, err := conn.DebugBundle(s.ctx, &proto.DebugBundleRequest{ + Anonymize: true, + Status: statusOutput, + SystemInfo: true, + }) + if err != nil { + return fmt.Errorf("failed to create debug bundle: %v", err) + } + + bundleDir := filepath.Dir(resp.GetPath()) + if err := open.Start(bundleDir); err != nil { + return fmt.Errorf("failed to open debug bundle directory: %v", err) + } + + s.app.SendNotification(fyne.NewNotification( + "Debug Bundle", + fmt.Sprintf("Debug bundle created at %s. Administrator privileges are required to access it.", resp.GetPath()), + )) + + return nil +} diff --git a/client/ui/event/event.go b/client/ui/event/event.go index 7925ee4d3..62a3c7c6a 100644 --- a/client/ui/event/event.go +++ b/client/ui/event/event.go @@ -3,6 +3,7 @@ package event import ( "context" "fmt" + "slices" "strings" "sync" "time" @@ -17,14 +18,17 @@ import ( "github.com/netbirdio/netbird/client/system" ) +type Handler func(*proto.SystemEvent) + type Manager struct { app fyne.App addr string - mu sync.Mutex - ctx context.Context - cancel context.CancelFunc - enabled bool + mu sync.Mutex + ctx context.Context + cancel context.CancelFunc + enabled bool + handlers []Handler } func NewManager(app fyne.App, addr string) *Manager { @@ -100,20 +104,41 @@ func (e *Manager) SetNotificationsEnabled(enabled bool) { func (e *Manager) handleEvent(event *proto.SystemEvent) { e.mu.Lock() enabled := e.enabled + handlers := slices.Clone(e.handlers) e.mu.Unlock() - if !enabled { + // critical events are always shown + if !enabled && event.Severity != proto.SystemEvent_CRITICAL { return } - title := e.getEventTitle(event) - e.app.SendNotification(fyne.NewNotification(title, event.UserMessage)) + if event.UserMessage != "" { + title := e.getEventTitle(event) + body := event.UserMessage + id := event.Metadata["id"] + if id != "" { + body += fmt.Sprintf(" ID: %s", id) + } + e.app.SendNotification(fyne.NewNotification(title, body)) + } + + for _, handler := range handlers { + go handler(event) + } +} + +func (e *Manager) AddHandler(handler Handler) { + e.mu.Lock() + defer e.mu.Unlock() + e.handlers = append(e.handlers, handler) } func (e *Manager) getEventTitle(event *proto.SystemEvent) string { var prefix string switch event.Severity { - case proto.SystemEvent_ERROR, proto.SystemEvent_CRITICAL: + case proto.SystemEvent_CRITICAL: + prefix = "Critical" + case proto.SystemEvent_ERROR: prefix = "Error" case proto.SystemEvent_WARNING: prefix = "Warning" diff --git a/client/ui/network.go b/client/ui/network.go index 852c4765b..750788cf3 100644 --- a/client/ui/network.go +++ b/client/ui/network.go @@ -3,7 +3,9 @@ package main import ( + "context" "fmt" + "runtime" "sort" "strings" "time" @@ -13,6 +15,7 @@ import ( "fyne.io/fyne/v2/dialog" "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/widget" + "fyne.io/systray" log "github.com/sirupsen/logrus" "github.com/netbirdio/netbird/client/proto" @@ -237,14 +240,14 @@ func (s *serviceClient) selectNetwork(id string, checked bool) { s.showError(fmt.Errorf("failed to select network: %v", err)) return } - log.Infof("Route %s selected", id) + log.Infof("Network '%s' selected", id) } else { if _, err := conn.DeselectNetworks(s.ctx, req); err != nil { log.Errorf("failed to deselect network: %v", err) s.showError(fmt.Errorf("failed to deselect network: %v", err)) return } - log.Infof("Network %s deselected", id) + log.Infof("Network '%s' deselected", id) } } @@ -324,6 +327,201 @@ func (s *serviceClient) updateNetworksBasedOnDisplayTab(tabs *container.AppTabs, s.updateNetworks(grid, f) } +func (s *serviceClient) updateExitNodes() { + conn, err := s.getSrvClient(defaultFailTimeout) + if err != nil { + log.Errorf("get client: %v", err) + return + } + + exitNodes, err := s.getExitNodes(conn) + if err != nil { + log.Errorf("get exit nodes: %v", err) + return + } + + s.exitNodeMu.Lock() + defer s.exitNodeMu.Unlock() + + s.recreateExitNodeMenu(exitNodes) + + if len(s.mExitNodeItems) > 0 { + s.mExitNode.Enable() + } else { + s.mExitNode.Disable() + } + + log.Debugf("Exit nodes updated: %d", len(s.mExitNodeItems)) +} + +func (s *serviceClient) recreateExitNodeMenu(exitNodes []*proto.Network) { + for _, node := range s.mExitNodeItems { + node.cancel() + node.Remove() + } + s.mExitNodeItems = nil + + if runtime.GOOS == "linux" || runtime.GOOS == "freebsd" { + s.mExitNode.Remove() + s.mExitNode = systray.AddMenuItem("Exit Node", "Select exit node for routing traffic") + } + + for _, node := range exitNodes { + menuItem := s.mExitNode.AddSubMenuItemCheckbox( + node.ID, + fmt.Sprintf("Use exit node %s", node.ID), + node.Selected, + ) + + ctx, cancel := context.WithCancel(context.Background()) + s.mExitNodeItems = append(s.mExitNodeItems, menuHandler{ + MenuItem: menuItem, + cancel: cancel, + }) + go s.handleChecked(ctx, node.ID, menuItem) + } + +} + +func (s *serviceClient) getExitNodes(conn proto.DaemonServiceClient) ([]*proto.Network, error) { + ctx, cancel := context.WithTimeout(s.ctx, defaultFailTimeout) + defer cancel() + + resp, err := conn.ListNetworks(ctx, &proto.ListNetworksRequest{}) + if err != nil { + return nil, fmt.Errorf("list networks: %v", err) + } + + var exitNodes []*proto.Network + for _, network := range resp.Routes { + if network.Range == "0.0.0.0/0" { + exitNodes = append(exitNodes, network) + } + } + return exitNodes, nil +} + +func (s *serviceClient) handleChecked(ctx context.Context, id string, item *systray.MenuItem) { + for { + select { + case <-ctx.Done(): + return + case _, ok := <-item.ClickedCh: + if !ok { + return + } + if err := s.toggleExitNode(id, item); err != nil { + log.Errorf("failed to toggle exit node: %v", err) + continue + } + } + } +} + +// Add function to toggle exit node selection +func (s *serviceClient) toggleExitNode(nodeID string, item *systray.MenuItem) error { + conn, err := s.getSrvClient(defaultFailTimeout) + if err != nil { + return fmt.Errorf("get client: %v", err) + } + + log.Infof("Toggling exit node '%s'", nodeID) + + s.exitNodeMu.Lock() + defer s.exitNodeMu.Unlock() + + exitNodes, err := s.getExitNodes(conn) + if err != nil { + return fmt.Errorf("get exit nodes: %v", err) + } + + var exitNode *proto.Network + // find other selected nodes and ours + ids := make([]string, 0, len(exitNodes)) + for _, node := range exitNodes { + if node.ID == nodeID { + // preserve original state + cp := *node //nolint:govet + exitNode = &cp + + // set desired state for recreation + node.Selected = true + continue + } + if node.Selected { + ids = append(ids, node.ID) + + // set desired state for recreation + node.Selected = false + } + } + + if item.Checked() && len(ids) == 0 { + // exit node is the only selected node, deselect it + ids = append(ids, nodeID) + exitNode = nil + } + + // deselect all other selected exit nodes + if err := s.deselectOtherExitNodes(conn, ids, item); err != nil { + return err + } + + if err := s.selectNewExitNode(conn, exitNode, nodeID, item); err != nil { + return err + } + + // linux/bsd doesn't handle Check/Uncheck well, so we recreate the menu + if runtime.GOOS == "linux" || runtime.GOOS == "freebsd" { + s.recreateExitNodeMenu(exitNodes) + } + + return nil +} + +func (s *serviceClient) deselectOtherExitNodes(conn proto.DaemonServiceClient, ids []string, currentItem *systray.MenuItem) error { + // deselect all other selected exit nodes + if len(ids) > 0 { + deselectReq := &proto.SelectNetworksRequest{ + NetworkIDs: ids, + } + if _, err := conn.DeselectNetworks(s.ctx, deselectReq); err != nil { + return fmt.Errorf("deselect networks: %v", err) + } + + log.Infof("Deselected exit nodes: %v", ids) + } + + // uncheck all other exit node menu items + for _, i := range s.mExitNodeItems { + if i.MenuItem == currentItem { + continue + } + i.Uncheck() + log.Infof("Unchecked exit node %v", i) + } + + return nil +} + +func (s *serviceClient) selectNewExitNode(conn proto.DaemonServiceClient, exitNode *proto.Network, nodeID string, item *systray.MenuItem) error { + if exitNode != nil && !exitNode.Selected { + selectReq := &proto.SelectNetworksRequest{ + NetworkIDs: []string{exitNode.ID}, + Append: true, + } + if _, err := conn.SelectNetworks(s.ctx, selectReq); err != nil { + return fmt.Errorf("select network: %v", err) + } + + log.Infof("Selected exit node '%s'", nodeID) + } + + item.Check() + + return nil +} + func getGridAndFilterFromTab(tabs *container.AppTabs, allGrid, overlappingGrid, exitNodesGrid *fyne.Container) (*fyne.Container, filter) { switch tabs.Selected().Text { case overlappingNetworksText: From a74208abac509aed52ea4fe170cb7892ca4a642b Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Fri, 21 Feb 2025 18:51:52 +0100 Subject: [PATCH 2/8] [client] Fix udp forwarder deadline (#3364) --- client/firewall/uspfilter/forwarder/udp.go | 44 ++++++++++------------ 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/client/firewall/uspfilter/forwarder/udp.go b/client/firewall/uspfilter/forwarder/udp.go index 97e4662fd..c37740587 100644 --- a/client/firewall/uspfilter/forwarder/udp.go +++ b/client/firewall/uspfilter/forwarder/udp.go @@ -245,33 +245,29 @@ func (c *udpPacketConn) copy(ctx context.Context, dst net.Conn, src net.Conn, bu defer bufPool.Put(bufp) buffer := *bufp - if err := src.SetReadDeadline(time.Now().Add(udpTimeout)); err != nil { - return fmt.Errorf("set read deadline: %w", err) - } - if err := src.SetWriteDeadline(time.Now().Add(udpTimeout)); err != nil { - return fmt.Errorf("set write deadline: %w", err) - } - for { - select { - case <-ctx.Done(): + if ctx.Err() != nil { return ctx.Err() - default: - n, err := src.Read(buffer) - if err != nil { - if isTimeout(err) { - continue - } - return fmt.Errorf("read from %s: %w", direction, err) - } - - _, err = dst.Write(buffer[:n]) - if err != nil { - return fmt.Errorf("write to %s: %w", direction, err) - } - - c.updateLastSeen() } + + if err := src.SetDeadline(time.Now().Add(udpTimeout)); err != nil { + return fmt.Errorf("set read deadline: %w", err) + } + + n, err := src.Read(buffer) + if err != nil { + if isTimeout(err) { + continue + } + return fmt.Errorf("read from %s: %w", direction, err) + } + + _, err = dst.Write(buffer[:n]) + if err != nil { + return fmt.Errorf("write to %s: %w", direction, err) + } + + c.updateLastSeen() } } From 73ce746ba7bcfbfe31ad4e2d6710236f1d663e62 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Fri, 21 Feb 2025 19:07:43 +0100 Subject: [PATCH 3/8] [misc] Rename CI client tests (#3366) --- .github/workflows/golang-test-darwin.yml | 6 ++---- .github/workflows/golang-test-freebsd.yml | 4 ++-- .github/workflows/golang-test-windows.yml | 3 ++- .github/workflows/golangci-lint.yml | 11 +++++++++-- .github/workflows/mobile-build-validation.yml | 4 +++- 5 files changed, 18 insertions(+), 10 deletions(-) diff --git a/.github/workflows/golang-test-darwin.yml b/.github/workflows/golang-test-darwin.yml index 664e8be18..4571ce753 100644 --- a/.github/workflows/golang-test-darwin.yml +++ b/.github/workflows/golang-test-darwin.yml @@ -1,4 +1,4 @@ -name: Test Code Darwin +name: "Darwin" on: push: @@ -12,9 +12,7 @@ concurrency: jobs: test: - strategy: - matrix: - store: ['sqlite'] + name: "Client / Unit" runs-on: macos-latest steps: - name: Install Go diff --git a/.github/workflows/golang-test-freebsd.yml b/.github/workflows/golang-test-freebsd.yml index 0f510cb3a..e1c688b1b 100644 --- a/.github/workflows/golang-test-freebsd.yml +++ b/.github/workflows/golang-test-freebsd.yml @@ -1,5 +1,4 @@ - -name: Test Code FreeBSD +name: "FreeBSD" on: push: @@ -13,6 +12,7 @@ concurrency: jobs: test: + name: "Client / Unit" runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/golang-test-windows.yml b/.github/workflows/golang-test-windows.yml index 782e4c30a..d9ff0a84b 100644 --- a/.github/workflows/golang-test-windows.yml +++ b/.github/workflows/golang-test-windows.yml @@ -1,4 +1,4 @@ -name: Test Code Windows +name: "Windows" on: push: @@ -14,6 +14,7 @@ concurrency: jobs: test: + name: "Client / Unit" runs-on: windows-latest steps: - name: Checkout code diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 6705a34ec..ca075d30f 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -1,4 +1,4 @@ -name: golangci-lint +name: Lint on: [pull_request] permissions: @@ -27,7 +27,14 @@ jobs: fail-fast: false matrix: os: [macos-latest, windows-latest, ubuntu-latest] - name: lint + include: + - os: macos-latest + display_name: Darwin + - os: windows-latest + display_name: Windows + - os: ubuntu-latest + display_name: Linux + name: ${{ matrix.display_name }} runs-on: ${{ matrix.os }} timeout-minutes: 15 steps: diff --git a/.github/workflows/mobile-build-validation.yml b/.github/workflows/mobile-build-validation.yml index dcf461a34..569956a54 100644 --- a/.github/workflows/mobile-build-validation.yml +++ b/.github/workflows/mobile-build-validation.yml @@ -1,4 +1,4 @@ -name: Mobile build validation +name: Mobile on: push: @@ -12,6 +12,7 @@ concurrency: jobs: android_build: + name: "Android / Build" runs-on: ubuntu-latest steps: - name: Checkout repository @@ -47,6 +48,7 @@ jobs: CGO_ENABLED: 0 ANDROID_NDK_HOME: /usr/local/lib/android/sdk/ndk/23.1.7779620 ios_build: + name: "iOS / Build" runs-on: macos-latest steps: - name: Checkout repository From 73101c897757c9f7ae7ed653f42dd91f0112a20b Mon Sep 17 00:00:00 2001 From: "M. Essam" Date: Fri, 21 Feb 2025 20:39:12 +0200 Subject: [PATCH 4/8] [client] Restart netbird-ui post-install in linux deb&rpm (#2992) --- .goreleaser_ui.yaml | 4 ++++ release_files/ui-post-install.sh | 10 ++++++++++ 2 files changed, 14 insertions(+) create mode 100644 release_files/ui-post-install.sh diff --git a/.goreleaser_ui.yaml b/.goreleaser_ui.yaml index 983aa0e78..1dd649d1b 100644 --- a/.goreleaser_ui.yaml +++ b/.goreleaser_ui.yaml @@ -50,6 +50,8 @@ nfpms: - netbird-ui formats: - deb + scripts: + postinstall: "release_files/ui-post-install.sh" contents: - src: client/ui/netbird.desktop dst: /usr/share/applications/netbird.desktop @@ -67,6 +69,8 @@ nfpms: - netbird-ui formats: - rpm + scripts: + postinstall: "release_files/ui-post-install.sh" contents: - src: client/ui/netbird.desktop dst: /usr/share/applications/netbird.desktop diff --git a/release_files/ui-post-install.sh b/release_files/ui-post-install.sh new file mode 100644 index 000000000..f6e8ddf92 --- /dev/null +++ b/release_files/ui-post-install.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +# Check if netbird-ui is running +if pgrep -x -f /usr/bin/netbird-ui >/dev/null 2>&1; +then + runner=$(ps --no-headers -o '%U' -p $(pgrep -x -f /usr/bin/netbird-ui) | sed 's/^[ \t]*//;s/[ \t]*$//') + # Only re-run if it was already running + pkill -x -f /usr/bin/netbird-ui >/dev/null 2>&1 + su -l - "$runner" -c 'nohup /usr/bin/netbird-ui > /dev/null 2>&1 &' +fi From 9a0354b681fbb8e73be265252b99eb09242d0ab3 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Fri, 21 Feb 2025 19:44:50 +0100 Subject: [PATCH 5/8] [client] Update local interface addresses when gathering candidates (#3324) --- client/iface/bind/udp_mux.go | 102 +++++++++++++++++------------ client/internal/dns/server_test.go | 4 +- client/internal/stdnet/filter.go | 1 - client/internal/stdnet/stdnet.go | 39 ++++++++++- 4 files changed, 98 insertions(+), 48 deletions(-) diff --git a/client/iface/bind/udp_mux.go b/client/iface/bind/udp_mux.go index 00a91f0ec..4c827de95 100644 --- a/client/iface/bind/udp_mux.go +++ b/client/iface/bind/udp_mux.go @@ -4,6 +4,7 @@ import ( "fmt" "io" "net" + "slices" "strings" "sync" @@ -152,46 +153,7 @@ func NewUDPMuxDefault(params UDPMuxParams) *UDPMuxDefault { params.Logger = logging.NewDefaultLoggerFactory().NewLogger("ice") } - var localAddrsForUnspecified []net.Addr - if addr, ok := params.UDPConn.LocalAddr().(*net.UDPAddr); !ok { - params.Logger.Errorf("LocalAddr is not a net.UDPAddr, got %T", params.UDPConn.LocalAddr()) - } else if ok && addr.IP.IsUnspecified() { - // For unspecified addresses, the correct behavior is to return errListenUnspecified, but - // it will break the applications that are already using unspecified UDP connection - // with UDPMuxDefault, so print a warn log and create a local address list for mux. - params.Logger.Warn("UDPMuxDefault should not listening on unspecified address, use NewMultiUDPMuxFromPort instead") - var networks []ice.NetworkType - switch { - - case addr.IP.To16() != nil: - networks = []ice.NetworkType{ice.NetworkTypeUDP4, ice.NetworkTypeUDP6} - - case addr.IP.To4() != nil: - networks = []ice.NetworkType{ice.NetworkTypeUDP4} - - default: - params.Logger.Errorf("LocalAddr expected IPV4 or IPV6, got %T", params.UDPConn.LocalAddr()) - } - if len(networks) > 0 { - if params.Net == nil { - var err error - if params.Net, err = stdnet.NewNet(); err != nil { - params.Logger.Errorf("failed to get create network: %v", err) - } - } - - ips, err := localInterfaces(params.Net, params.InterfaceFilter, nil, networks, true) - if err == nil { - for _, ip := range ips { - localAddrsForUnspecified = append(localAddrsForUnspecified, &net.UDPAddr{IP: ip, Port: addr.Port}) - } - } else { - params.Logger.Errorf("failed to get local interfaces for unspecified addr: %v", err) - } - } - } - - return &UDPMuxDefault{ + mux := &UDPMuxDefault{ addressMap: map[string][]*udpMuxedConn{}, params: params, connsIPv4: make(map[string]*udpMuxedConn), @@ -203,8 +165,55 @@ func NewUDPMuxDefault(params UDPMuxParams) *UDPMuxDefault { return newBufferHolder(receiveMTU + maxAddrSize) }, }, - localAddrsForUnspecified: localAddrsForUnspecified, } + + mux.updateLocalAddresses() + return mux +} + +func (m *UDPMuxDefault) updateLocalAddresses() { + var localAddrsForUnspecified []net.Addr + if addr, ok := m.params.UDPConn.LocalAddr().(*net.UDPAddr); !ok { + m.params.Logger.Errorf("LocalAddr is not a net.UDPAddr, got %T", m.params.UDPConn.LocalAddr()) + } else if ok && addr.IP.IsUnspecified() { + // For unspecified addresses, the correct behavior is to return errListenUnspecified, but + // it will break the applications that are already using unspecified UDP connection + // with UDPMuxDefault, so print a warn log and create a local address list for mux. + m.params.Logger.Warn("UDPMuxDefault should not listening on unspecified address, use NewMultiUDPMuxFromPort instead") + var networks []ice.NetworkType + switch { + + case addr.IP.To16() != nil: + networks = []ice.NetworkType{ice.NetworkTypeUDP4, ice.NetworkTypeUDP6} + + case addr.IP.To4() != nil: + networks = []ice.NetworkType{ice.NetworkTypeUDP4} + + default: + m.params.Logger.Errorf("LocalAddr expected IPV4 or IPV6, got %T", m.params.UDPConn.LocalAddr()) + } + if len(networks) > 0 { + if m.params.Net == nil { + var err error + if m.params.Net, err = stdnet.NewNet(); err != nil { + m.params.Logger.Errorf("failed to get create network: %v", err) + } + } + + ips, err := localInterfaces(m.params.Net, m.params.InterfaceFilter, nil, networks, true) + if err == nil { + for _, ip := range ips { + localAddrsForUnspecified = append(localAddrsForUnspecified, &net.UDPAddr{IP: ip, Port: addr.Port}) + } + } else { + m.params.Logger.Errorf("failed to get local interfaces for unspecified addr: %v", err) + } + } + } + + m.mu.Lock() + m.localAddrsForUnspecified = localAddrsForUnspecified + m.mu.Unlock() } // LocalAddr returns the listening address of this UDPMuxDefault @@ -214,8 +223,12 @@ func (m *UDPMuxDefault) LocalAddr() net.Addr { // GetListenAddresses returns the list of addresses that this mux is listening on func (m *UDPMuxDefault) GetListenAddresses() []net.Addr { + m.updateLocalAddresses() + + m.mu.Lock() + defer m.mu.Unlock() if len(m.localAddrsForUnspecified) > 0 { - return m.localAddrsForUnspecified + return slices.Clone(m.localAddrsForUnspecified) } return []net.Addr{m.LocalAddr()} @@ -225,7 +238,10 @@ func (m *UDPMuxDefault) GetListenAddresses() []net.Addr { // creates the connection if an existing one can't be found func (m *UDPMuxDefault) GetConn(ufrag string, addr net.Addr) (net.PacketConn, error) { // don't check addr for mux using unspecified address - if len(m.localAddrsForUnspecified) == 0 && m.params.UDPConn.LocalAddr().String() != addr.String() { + m.mu.Lock() + lenLocalAddrs := len(m.localAddrsForUnspecified) + m.mu.Unlock() + if lenLocalAddrs == 0 && m.params.UDPConn.LocalAddr().String() != addr.String() { return nil, fmt.Errorf("invalid address %s", addr.String()) } diff --git a/client/internal/dns/server_test.go b/client/internal/dns/server_test.go index 84779256f..94b87124b 100644 --- a/client/internal/dns/server_test.go +++ b/client/internal/dns/server_test.go @@ -413,7 +413,7 @@ func TestDNSFakeResolverHandleUpdates(t *testing.T) { defer t.Setenv("NB_WG_KERNEL_DISABLED", ov) t.Setenv("NB_WG_KERNEL_DISABLED", "true") - newNet, err := stdnet.NewNet(nil) + newNet, err := stdnet.NewNet([]string{"utun2301"}) if err != nil { t.Errorf("create stdnet: %v", err) return @@ -887,7 +887,7 @@ func createWgInterfaceWithBind(t *testing.T) (*iface.WGIface, error) { defer t.Setenv("NB_WG_KERNEL_DISABLED", ov) t.Setenv("NB_WG_KERNEL_DISABLED", "true") - newNet, err := stdnet.NewNet(nil) + newNet, err := stdnet.NewNet([]string{"utun2301"}) if err != nil { t.Fatalf("create stdnet: %v", err) return nil, err diff --git a/client/internal/stdnet/filter.go b/client/internal/stdnet/filter.go index c04250b2d..e45714001 100644 --- a/client/internal/stdnet/filter.go +++ b/client/internal/stdnet/filter.go @@ -21,7 +21,6 @@ func InterfaceFilter(disallowList []string) func(string) bool { for _, s := range disallowList { if strings.HasPrefix(iFace, s) && runtime.GOOS != "ios" { - log.Tracef("ignoring interface %s - it is not allowed", iFace) return false } } diff --git a/client/internal/stdnet/stdnet.go b/client/internal/stdnet/stdnet.go index 2e87475a5..aa9fdd045 100644 --- a/client/internal/stdnet/stdnet.go +++ b/client/internal/stdnet/stdnet.go @@ -5,11 +5,16 @@ package stdnet import ( "fmt" + "slices" + "sync" + "time" "github.com/pion/transport/v3" "github.com/pion/transport/v3/stdnet" ) +const updateInterval = 30 * time.Second + // Net is an implementation of the net.Net interface // based on functions of the standard net package. type Net struct { @@ -18,6 +23,10 @@ type Net struct { iFaceDiscover iFaceDiscover // interfaceFilter should return true if the given interfaceName is allowed interfaceFilter func(interfaceName string) bool + lastUpdate time.Time + + // mu is shared between interfaces and lastUpdate + mu sync.Mutex } // NewNetWithDiscover creates a new StdNet instance. @@ -43,18 +52,40 @@ func NewNet(disallowList []string) (*Net, error) { // The interfaces are discovered by an external iFaceDiscover function or by a default discoverer if the external one // wasn't specified. func (n *Net) UpdateInterfaces() (err error) { + n.mu.Lock() + defer n.mu.Unlock() + + return n.updateInterfaces() +} + +func (n *Net) updateInterfaces() (err error) { allIfaces, err := n.iFaceDiscover.iFaces() if err != nil { return err } + n.interfaces = n.filterInterfaces(allIfaces) + + n.lastUpdate = time.Now() + return nil } // Interfaces returns a slice of interfaces which are available on the // system func (n *Net) Interfaces() ([]*transport.Interface, error) { - return n.interfaces, nil + n.mu.Lock() + defer n.mu.Unlock() + + if time.Since(n.lastUpdate) < updateInterval { + return slices.Clone(n.interfaces), nil + } + + if err := n.updateInterfaces(); err != nil { + return nil, fmt.Errorf("update interfaces: %w", err) + } + + return slices.Clone(n.interfaces), nil } // InterfaceByIndex returns the interface specified by index. @@ -63,6 +94,8 @@ func (n *Net) Interfaces() ([]*transport.Interface, error) { // sharing the logical data link; for more precision use // InterfaceByName. func (n *Net) InterfaceByIndex(index int) (*transport.Interface, error) { + n.mu.Lock() + defer n.mu.Unlock() for _, ifc := range n.interfaces { if ifc.Index == index { return ifc, nil @@ -74,6 +107,8 @@ func (n *Net) InterfaceByIndex(index int) (*transport.Interface, error) { // InterfaceByName returns the interface specified by name. func (n *Net) InterfaceByName(name string) (*transport.Interface, error) { + n.mu.Lock() + defer n.mu.Unlock() for _, ifc := range n.interfaces { if ifc.Name == name { return ifc, nil @@ -87,7 +122,7 @@ func (n *Net) filterInterfaces(interfaces []*transport.Interface) []*transport.I if n.interfaceFilter == nil { return interfaces } - result := []*transport.Interface{} + var result []*transport.Interface for _, iface := range interfaces { if n.interfaceFilter(iface.Name) { result = append(result, iface) From b64bee35fa4cbe1b3b03620bc5ad84768652f920 Mon Sep 17 00:00:00 2001 From: Pedro Maia Costa <550684+pnmcosta@users.noreply.github.com> Date: Sat, 22 Feb 2025 10:31:39 +0000 Subject: [PATCH 6/8] [management] faster server bootstrap (#3365) Faster server bootstrap by counting accounts rather than fetching all from storage in the account manager instantiation. This change moved the deprecated need to ensure accounts have an All group to tests instead. --- management/server/account.go | 81 ++++------------------- management/server/store/sql_store.go | 20 +++++- management/server/store/sql_store_test.go | 43 +----------- management/server/store/store.go | 29 ++++++++ management/server/testdata/storev1.sql | 1 - management/server/types/account.go | 41 ++++++++++++ 6 files changed, 104 insertions(+), 111 deletions(-) diff --git a/management/server/account.go b/management/server/account.go index 76c984286..332d356e2 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -246,6 +246,11 @@ func BuildManager( integratedPeerValidator integrated_validator.IntegratedValidator, metrics telemetry.AppMetrics, ) (*DefaultAccountManager, error) { + start := time.Now() + defer func() { + log.WithContext(ctx).Debugf("took %v to instantiate account manager", time.Since(start)) + }() + am := &DefaultAccountManager{ Store: store, geo: geo, @@ -263,39 +268,21 @@ func BuildManager( metrics: metrics, requestBuffer: NewAccountRequestBuffer(ctx, store), } - allAccounts := store.GetAllAccounts(ctx) + accountsCounter, err := store.GetAccountsCounter(ctx) + if err != nil { + log.WithContext(ctx).Error(err) + } + // enable single account mode only if configured by user and number of existing accounts is not grater than 1 - am.singleAccountMode = singleAccountModeDomain != "" && len(allAccounts) <= 1 + am.singleAccountMode = singleAccountModeDomain != "" && accountsCounter <= 1 if am.singleAccountMode { if !isDomainValid(singleAccountModeDomain) { return nil, status.Errorf(status.InvalidArgument, "invalid domain \"%s\" provided for a single account mode. Please review your input for --single-account-mode-domain", singleAccountModeDomain) } am.singleAccountModeDomain = singleAccountModeDomain - log.WithContext(ctx).Infof("single account mode enabled, accounts number %d", len(allAccounts)) + log.WithContext(ctx).Infof("single account mode enabled, accounts number %d", accountsCounter) } else { - log.WithContext(ctx).Infof("single account mode disabled, accounts number %d", len(allAccounts)) - } - - // if account doesn't have a default group - // we create 'all' group and add all peers into it - // also we create default rule with source as destination - for _, account := range allAccounts { - shouldSave := false - - _, err := account.GetGroupAll() - if err != nil { - if err := addAllGroup(account); err != nil { - return nil, err - } - shouldSave = true - } - - if shouldSave { - err = store.SaveAccount(ctx, account) - if err != nil { - return nil, err - } - } + log.WithContext(ctx).Infof("single account mode disabled, accounts number %d", accountsCounter) } goCacheClient := gocache.New(CacheExpirationMax, 30*time.Minute) @@ -1619,46 +1606,6 @@ func (am *DefaultAccountManager) GetAccountSettings(ctx context.Context, account return am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID) } -// addAllGroup to account object if it doesn't exist -func addAllGroup(account *types.Account) error { - if len(account.Groups) == 0 { - allGroup := &types.Group{ - ID: xid.New().String(), - Name: "All", - Issued: types.GroupIssuedAPI, - } - for _, peer := range account.Peers { - allGroup.Peers = append(allGroup.Peers, peer.ID) - } - account.Groups = map[string]*types.Group{allGroup.ID: allGroup} - - id := xid.New().String() - - defaultPolicy := &types.Policy{ - ID: id, - Name: types.DefaultRuleName, - Description: types.DefaultRuleDescription, - Enabled: true, - Rules: []*types.PolicyRule{ - { - ID: id, - Name: types.DefaultRuleName, - Description: types.DefaultRuleDescription, - Enabled: true, - Sources: []string{allGroup.ID}, - Destinations: []string{allGroup.ID}, - Bidirectional: true, - Protocol: types.PolicyRuleProtocolALL, - Action: types.PolicyTrafficActionAccept, - }, - }, - } - - account.Policies = []*types.Policy{defaultPolicy} - } - return nil -} - // newAccountWithId creates a new Account with a default SetupKey (doesn't store in a Store) and provided id func newAccountWithId(ctx context.Context, accountID, userID, domain string) *types.Account { log.WithContext(ctx).Debugf("creating new account") @@ -1703,7 +1650,7 @@ func newAccountWithId(ctx context.Context, accountID, userID, domain string) *ty }, } - if err := addAllGroup(acc); err != nil { + if err := acc.AddAllGroup(); err != nil { log.WithContext(ctx).Errorf("error adding all group to account %s: %v", acc.Id, err) } return acc diff --git a/management/server/store/sql_store.go b/management/server/store/sql_store.go index 947694420..efc2539ff 100644 --- a/management/server/store/sql_store.go +++ b/management/server/store/sql_store.go @@ -15,7 +15,6 @@ import ( "sync" "time" - "github.com/netbirdio/netbird/management/server/util" log "github.com/sirupsen/logrus" "gorm.io/driver/mysql" "gorm.io/driver/postgres" @@ -24,6 +23,8 @@ import ( "gorm.io/gorm/clause" "gorm.io/gorm/logger" + "github.com/netbirdio/netbird/management/server/util" + nbdns "github.com/netbirdio/netbird/dns" "github.com/netbirdio/netbird/management/server/account" resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types" @@ -615,6 +616,16 @@ func (s *SqlStore) GetResourceGroups(ctx context.Context, lockStrength LockingSt return groups, nil } +func (s *SqlStore) GetAccountsCounter(ctx context.Context) (int64, error) { + var count int64 + result := s.db.Model(&types.Account{}).Count(&count) + if result.Error != nil { + return 0, fmt.Errorf("failed to get all accounts counter: %w", result.Error) + } + + return count, nil +} + func (s *SqlStore) GetAllAccounts(ctx context.Context) (all []*types.Account) { var accounts []types.Account result := s.db.Find(&accounts) @@ -1035,6 +1046,13 @@ func NewSqliteStoreFromFileStore(ctx context.Context, fileStore *FileStore, data } for _, account := range fileStore.GetAllAccounts(ctx) { + _, err = account.GetGroupAll() + if err != nil { + if err := account.AddAllGroup(); err != nil { + return nil, err + } + } + err := store.SaveAccount(ctx, account) if err != nil { return nil, err diff --git a/management/server/store/sql_store_test.go b/management/server/store/sql_store_test.go index dd240ce6c..5cb092190 100644 --- a/management/server/store/sql_store_test.go +++ b/management/server/store/sql_store_test.go @@ -15,7 +15,6 @@ import ( "time" "github.com/google/uuid" - "github.com/rs/xid" log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -2045,52 +2044,12 @@ func newAccountWithId(ctx context.Context, accountID, userID, domain string) *ty }, } - if err := addAllGroup(acc); err != nil { + if err := acc.AddAllGroup(); err != nil { log.WithContext(ctx).Errorf("error adding all group to account %s: %v", acc.Id, err) } return acc } -// addAllGroup to account object if it doesn't exist -func addAllGroup(account *types.Account) error { - if len(account.Groups) == 0 { - allGroup := &types.Group{ - ID: xid.New().String(), - Name: "All", - Issued: types.GroupIssuedAPI, - } - for _, peer := range account.Peers { - allGroup.Peers = append(allGroup.Peers, peer.ID) - } - account.Groups = map[string]*types.Group{allGroup.ID: allGroup} - - id := xid.New().String() - - defaultPolicy := &types.Policy{ - ID: id, - Name: types.DefaultRuleName, - Description: types.DefaultRuleDescription, - Enabled: true, - Rules: []*types.PolicyRule{ - { - ID: id, - Name: types.DefaultRuleName, - Description: types.DefaultRuleDescription, - Enabled: true, - Sources: []string{allGroup.ID}, - Destinations: []string{allGroup.ID}, - Bidirectional: true, - Protocol: types.PolicyRuleProtocolALL, - Action: types.PolicyTrafficActionAccept, - }, - }, - } - - account.Policies = []*types.Policy{defaultPolicy} - } - return nil -} - func TestSqlStore_GetAccountNetworks(t *testing.T) { store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir()) t.Cleanup(cleanup) diff --git a/management/server/store/store.go b/management/server/store/store.go index e074c4c60..2686c3597 100644 --- a/management/server/store/store.go +++ b/management/server/store/store.go @@ -48,6 +48,7 @@ const ( ) type Store interface { + GetAccountsCounter(ctx context.Context) (int64, error) GetAllAccounts(ctx context.Context) []*types.Account GetAccount(ctx context.Context, accountID string) (*types.Account, error) AccountExists(ctx context.Context, lockStrength LockingStrength, id string) (bool, error) @@ -352,9 +353,37 @@ func NewTestStoreFromSQL(ctx context.Context, filename string, dataDir string) ( return nil, nil, fmt.Errorf("failed to create test store: %v", err) } + err = addAllGroupToAccount(ctx, store) + if err != nil { + return nil, nil, fmt.Errorf("failed to add all group to account: %v", err) + } + return getSqlStoreEngine(ctx, store, kind) } +func addAllGroupToAccount(ctx context.Context, store Store) error { + allAccounts := store.GetAllAccounts(ctx) + for _, account := range allAccounts { + shouldSave := false + + _, err := account.GetGroupAll() + if err != nil { + if err := account.AddAllGroup(); err != nil { + return err + } + shouldSave = true + } + + if shouldSave { + err = store.SaveAccount(ctx, account) + if err != nil { + return err + } + } + } + return nil +} + func getSqlStoreEngine(ctx context.Context, store *SqlStore, kind Engine) (Store, func(), error) { var cleanup func() var err error diff --git a/management/server/testdata/storev1.sql b/management/server/testdata/storev1.sql index cda333d4f..8b09ec2be 100644 --- a/management/server/testdata/storev1.sql +++ b/management/server/testdata/storev1.sql @@ -36,4 +36,3 @@ INSERT INTO peers VALUES('xlx9/9D8+ibnRiIIB8nHGMxGOzxV17r8ShPHgi4aYSM=','auth0|6 INSERT INTO peers VALUES('6kjbmVq1hmucVzvBXo5OucY5OYv+jSsB1jUTLq291Dw=','google-oauth2|103201118415301331038','6kjbmVq1hmucVzvBXo5OucY5OYv+jSsB1jUTLq291Dw=','5AFB60DB-61F2-4251-8E11-494847EE88E9','"100.64.0.2"','braginini','linux','Linux','21.04','x86_64','Ubuntu','','','','',NULL,'','','','{"Cloud":"","Platform":""}',NULL,'braginini','braginini','2021-12-24 16:12:05.994305438+01:00',0,0,0,'','',0,0,NULL,'2024-10-02 17:00:54.228182+02:00',0,'""','','',0); INSERT INTO peers VALUES('Ok+5QMdt/UjoktNOvicGYj+IX2g98p+0N2PJ3vJ45RI=','google-oauth2|103201118415301331038','Ok+5QMdt/UjoktNOvicGYj+IX2g98p+0N2PJ3vJ45RI=','A72E4DC2-00DE-4542-8A24-62945438104E','"100.64.0.1"','braginini','linux','Linux','21.04','x86_64','Ubuntu','','','','',NULL,'','','','{"Cloud":"","Platform":""}',NULL,'braginini','braginini-1','2021-12-24 16:11:27.015739803+01:00',0,0,0,'','',0,0,NULL,'2024-10-02 17:00:54.228182+02:00',1,'""','','',0); INSERT INTO installations VALUES(1,''); - diff --git a/management/server/types/account.go b/management/server/types/account.go index 4c68b9523..c890a7730 100644 --- a/management/server/types/account.go +++ b/management/server/types/account.go @@ -12,6 +12,7 @@ import ( "github.com/hashicorp/go-multierror" "github.com/miekg/dns" + "github.com/rs/xid" log "github.com/sirupsen/logrus" nbdns "github.com/netbirdio/netbird/dns" @@ -1525,3 +1526,43 @@ func getPoliciesSourcePeers(policies []*Policy, groups map[string]*Group) map[st return sourcePeers } + +// AddAllGroup to account object if it doesn't exist +func (a *Account) AddAllGroup() error { + if len(a.Groups) == 0 { + allGroup := &Group{ + ID: xid.New().String(), + Name: "All", + Issued: GroupIssuedAPI, + } + for _, peer := range a.Peers { + allGroup.Peers = append(allGroup.Peers, peer.ID) + } + a.Groups = map[string]*Group{allGroup.ID: allGroup} + + id := xid.New().String() + + defaultPolicy := &Policy{ + ID: id, + Name: DefaultRuleName, + Description: DefaultRuleDescription, + Enabled: true, + Rules: []*PolicyRule{ + { + ID: id, + Name: DefaultRuleName, + Description: DefaultRuleDescription, + Enabled: true, + Sources: []string{allGroup.ID}, + Destinations: []string{allGroup.ID}, + Bidirectional: true, + Protocol: PolicyRuleProtocolALL, + Action: PolicyTrafficActionAccept, + }, + }, + } + + a.Policies = []*Policy{defaultPolicy} + } + return nil +} From 559e6731079ed1c555984389f1a5c6cf485392c8 Mon Sep 17 00:00:00 2001 From: Carlos Hernandez Date: Sat, 22 Feb 2025 04:41:24 -0700 Subject: [PATCH 7/8] [client] fix privacy warning on macOS (#3350) * fix: macos privacy warning Move GetDesktopUIUserAgent to its own package so UI does not have to import client/system package that reaches out to broadcasts address. Thus, fixing the network privacy warnings. --- client/system/info.go | 6 ------ client/ui/client_ui.go | 5 +++-- client/ui/desktop/desktop.go | 8 ++++++++ client/ui/event/event.go | 4 ++-- 4 files changed, 13 insertions(+), 10 deletions(-) create mode 100644 client/ui/desktop/desktop.go diff --git a/client/system/info.go b/client/system/info.go index d83e9509a..2a0343ca6 100644 --- a/client/system/info.go +++ b/client/system/info.go @@ -9,7 +9,6 @@ import ( "google.golang.org/grpc/metadata" "github.com/netbirdio/netbird/management/proto" - "github.com/netbirdio/netbird/version" ) // DeviceNameCtxKey context key for device name @@ -119,11 +118,6 @@ func extractDeviceName(ctx context.Context, defaultName string) string { return v } -// GetDesktopUIUserAgent returns the Desktop ui user agent -func GetDesktopUIUserAgent() string { - return "netbird-desktop-ui/" + version.NetbirdVersion() -} - func networkAddresses() ([]NetworkAddress, error) { interfaces, err := net.Interfaces() if err != nil { diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go index 100076806..bfc4cde16 100644 --- a/client/ui/client_ui.go +++ b/client/ui/client_ui.go @@ -33,7 +33,7 @@ import ( "github.com/netbirdio/netbird/client/internal" "github.com/netbirdio/netbird/client/proto" - "github.com/netbirdio/netbird/client/system" + "github.com/netbirdio/netbird/client/ui/desktop" "github.com/netbirdio/netbird/client/ui/event" "github.com/netbirdio/netbird/util" "github.com/netbirdio/netbird/version" @@ -779,6 +779,7 @@ func normalizedVersion(version string) string { return versionString } +// onTrayExit is called when the tray icon is closed. func (s *serviceClient) onTrayExit() { for _, item := range s.mExitNodeItems { item.cancel() @@ -799,7 +800,7 @@ func (s *serviceClient) getSrvClient(timeout time.Duration) (proto.DaemonService strings.TrimPrefix(s.addr, "tcp://"), grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithBlock(), - grpc.WithUserAgent(system.GetDesktopUIUserAgent()), + grpc.WithUserAgent(desktop.GetUIUserAgent()), ) if err != nil { return nil, fmt.Errorf("dial service: %w", err) diff --git a/client/ui/desktop/desktop.go b/client/ui/desktop/desktop.go new file mode 100644 index 000000000..0c99e2f38 --- /dev/null +++ b/client/ui/desktop/desktop.go @@ -0,0 +1,8 @@ +package desktop + +import "github.com/netbirdio/netbird/version" + +// GetUIUserAgent returns the Desktop ui user agent +func GetUIUserAgent() string { + return "netbird-desktop-ui/" + version.NetbirdVersion() +} diff --git a/client/ui/event/event.go b/client/ui/event/event.go index 62a3c7c6a..4d949416d 100644 --- a/client/ui/event/event.go +++ b/client/ui/event/event.go @@ -15,7 +15,7 @@ import ( "google.golang.org/grpc/credentials/insecure" "github.com/netbirdio/netbird/client/proto" - "github.com/netbirdio/netbird/client/system" + "github.com/netbirdio/netbird/client/ui/desktop" ) type Handler func(*proto.SystemEvent) @@ -167,7 +167,7 @@ func getClient(addr string) (proto.DaemonServiceClient, error) { conn, err := grpc.NewClient( strings.TrimPrefix(addr, "tcp://"), grpc.WithTransportCredentials(insecure.NewCredentials()), - grpc.WithUserAgent(system.GetDesktopUIUserAgent()), + grpc.WithUserAgent(desktop.GetUIUserAgent()), ) if err != nil { return nil, err From cc48594b0b15d3f3961a27729293f72eaa6bcdc7 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Mon, 24 Feb 2025 01:14:31 +0100 Subject: [PATCH 8/8] [client][ui] Disable notifications by default (#3375) --- client/internal/config.go | 13 ++++++++++--- client/server/server.go | 9 +++++++-- client/ui/client_ui.go | 2 +- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/client/internal/config.go b/client/internal/config.go index b269a3854..b2f96cbdc 100644 --- a/client/internal/config.go +++ b/client/internal/config.go @@ -99,7 +99,7 @@ type Config struct { BlockLANAccess bool - DisableNotifications bool + DisableNotifications *bool DNSLabels domain.List @@ -479,13 +479,20 @@ func (config *Config) apply(input ConfigInput) (updated bool, err error) { updated = true } - if input.DisableNotifications != nil && *input.DisableNotifications != config.DisableNotifications { + if input.DisableNotifications != nil && input.DisableNotifications != config.DisableNotifications { if *input.DisableNotifications { log.Infof("disabling notifications") } else { log.Infof("enabling notifications") } - config.DisableNotifications = *input.DisableNotifications + config.DisableNotifications = input.DisableNotifications + updated = true + } + + if config.DisableNotifications == nil { + disabled := true + config.DisableNotifications = &disabled + log.Infof("setting notifications to disabled by default") updated = true } diff --git a/client/server/server.go b/client/server/server.go index 2efbb94ff..348fb9872 100644 --- a/client/server/server.go +++ b/client/server/server.go @@ -751,6 +751,11 @@ func (s *Server) GetConfig(_ context.Context, _ *proto.GetConfigRequest) (*proto } + disableNotifications := true + if s.config.DisableNotifications != nil { + disableNotifications = *s.config.DisableNotifications + } + return &proto.GetConfigResponse{ ManagementUrl: managementURL, ConfigFile: s.latestConfigInput.ConfigPath, @@ -763,14 +768,14 @@ func (s *Server) GetConfig(_ context.Context, _ *proto.GetConfigRequest) (*proto ServerSSHAllowed: *s.config.ServerSSHAllowed, RosenpassEnabled: s.config.RosenpassEnabled, RosenpassPermissive: s.config.RosenpassPermissive, - DisableNotifications: s.config.DisableNotifications, + DisableNotifications: disableNotifications, }, nil } func (s *Server) onSessionExpire() { if runtime.GOOS != "windows" { isUIActive := internal.CheckUIApp() - if !isUIActive && !s.config.DisableNotifications { + if !isUIActive && s.config.DisableNotifications != nil && !*s.config.DisableNotifications { if err := sendTerminalNotification(); err != nil { log.Errorf("send session expire terminal notification: %v", err) } diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go index bfc4cde16..51eec59a5 100644 --- a/client/ui/client_ui.go +++ b/client/ui/client_ui.go @@ -591,7 +591,7 @@ func (s *serviceClient) onTrayReady() { s.mAllowSSH = s.mSettings.AddSubMenuItemCheckbox("Allow SSH", "Allow SSH connections", false) s.mAutoConnect = s.mSettings.AddSubMenuItemCheckbox("Connect on Startup", "Connect automatically when the service starts", false) s.mEnableRosenpass = s.mSettings.AddSubMenuItemCheckbox("Enable Quantum-Resistance", "Enable post-quantum security via Rosenpass", false) - s.mNotifications = s.mSettings.AddSubMenuItemCheckbox("Notifications", "Enable notifications", true) + s.mNotifications = s.mSettings.AddSubMenuItemCheckbox("Notifications", "Enable notifications", false) s.mAdvancedSettings = s.mSettings.AddSubMenuItem("Advanced Settings", "Advanced settings of the application") s.mCreateDebugBundle = s.mSettings.AddSubMenuItem("Create Debug Bundle", "Create and open debug information bundle") s.loadSettings()