diff --git a/client/cmd/debug.go b/client/cmd/debug.go index c7ab87b4766..c02f60aed4a 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 bf4588ce4ea..0ddba8b2f3f 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 1e240d1924c..03608eab0b9 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 a1e8f0f8c04..8ca701adf5a 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" @@ -108,6 +109,16 @@ func (c *ConnectClient) RunOniOS( func (c *ConnectClient) run(mobileDependency MobileDependency, probes *ProbeHolder, 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 02ae26e10e3..1f4ddb67cef 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 fbe8c4dbbb1..c06de4d2572 100644 --- a/client/internal/dns/host.go +++ b/client/internal/dns/host.go @@ -9,10 +9,13 @@ 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") + type hostManager interface { applyDNSConfig(config HostDNSConfig, stateManager *statemanager.Manager) error restoreHostDNS() error supportCustomPort() bool + string() string } type SystemDNSSettings struct { @@ -39,6 +42,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 { @@ -62,6 +66,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 }, @@ -116,3 +127,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 5653710d705..dfa3e5712d5 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 2f92dd36758..f727f68b5b9 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 4a0acf57241..1c0ac63e9b4 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 7ecca8a41f4..6f9a170c7b0 100644 --- a/client/internal/dns/host_windows.go +++ b/client/internal/dns/host_windows.go @@ -163,6 +163,10 @@ func (r *registryConfigurator) restoreHostDNS() error { return nil } +func (r *registryConfigurator) string() string { + return "registry" +} + func (r *registryConfigurator) updateSearchDomains(domains []string) error { err := r.setInterfaceRegistryKeyStringValue(interfaceConfigSearchListKey, strings.Join(domains, ",")) if err != nil { diff --git a/client/internal/dns/network_manager_unix.go b/client/internal/dns/network_manager_unix.go index 63bbead7728..10b4e6a6e44 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 6b5fdaf8698..54c4c75bfca 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 1fe913fd9c1..588ce755a1d 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" @@ -16,6 +17,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" ) @@ -401,6 +403,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() { @@ -419,6 +422,23 @@ 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) ([]muxUpdate, map[string]nbdns.SimpleRecord, error) { var muxUpdates []muxUpdate localRecords := make(map[string]nbdns.SimpleRecord, 0) @@ -621,6 +641,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) } @@ -659,6 +680,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 c166820c457..4a79d2f79cf 100644 --- a/client/internal/dns/server_test.go +++ b/client/internal/dns/server_test.go @@ -294,7 +294,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) } @@ -403,7 +403,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 @@ -498,7 +498,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) } @@ -572,7 +572,7 @@ func TestDNSServerUpstreamDeactivateCallback(t *testing.T) { {false, "domain2", false}, }, }, - statusRecorder: &peer.Status{}, + statusRecorder: peer.NewRecorder("mgm"), } var domainsUpdate string @@ -633,7 +633,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) @@ -657,7 +657,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) @@ -749,7 +749,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 a031be5823d..a87cc73e5c7 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 682f4eb57bc..4ba770e828a 100644 --- a/client/internal/dns/upstream.go +++ b/client/internal/dns/upstream.go @@ -171,6 +171,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 @@ -220,10 +233,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 b3689c91153..1b60a1662e8 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" semaphoregroup "github.com/netbirdio/netbird/util/semaphore-group" nbssh "github.com/netbirdio/netbird/client/ssh" @@ -700,6 +701,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 e3b234b0b24..35627a4746b 100644 --- a/client/internal/routemanager/client.go +++ b/client/internal/routemanager/client.go @@ -305,11 +305,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 { @@ -323,6 +325,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) @@ -330,6 +336,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 { @@ -348,29 +383,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 e2d982fefa9..e8a00e92b2f 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, } ) @@ -3278,7 +3281,7 @@ var file_daemon_proto_rawDesc = []byte{ 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x22, 0x0a, 0x20, 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, 0x12, 0x0a, 0x10, 0x53, 0x75, 0x62, - 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x87, 0x04, + 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 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, @@ -3307,106 +3310,106 @@ var file_daemon_proto_rawDesc = []byte{ 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, 0x0b, 0x0a, 0x07, 0x4e, + 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, 0x9d, 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, + 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, 0x9d, 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, + 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, 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, 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, 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, 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, + 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, 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, 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, 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 0858dd6347f..4f7db25090b 100644 --- a/client/proto/daemon.proto +++ b/client/proto/daemon.proto @@ -382,6 +382,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 aaf361524dd..d310f4da12d 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 9331570e637..2b65c9fa364 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 00000000000..2d11ee3ba57 --- /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 00000000000..24c4827d3d6 --- /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 e2c138b145e..00000000000 --- 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 23756e1b759..057a165319e 100644 --- a/client/ui/client_ui.go +++ b/client/ui/client_ui.go @@ -83,7 +83,7 @@ func main() { } a := app.NewWithID("NetBird") - a.SetIcon(fyne.NewStaticResource("netbird", iconDisconnectedPNG)) + a.SetIcon(fyne.NewStaticResource("netbird", iconConnectedPNG)) if errorMSG != "" { showErrorMSG(errorMSG) @@ -149,22 +149,24 @@ type serviceClient struct { icUpdateCloud []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 @@ -201,6 +203,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 @@ -446,6 +456,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 } @@ -467,11 +480,13 @@ func (s *serviceClient) updateStatus() error { } else { systray.SetIcon(s.icConnected) } + s.mAbout.SetIcon(s.icConnected) systray.SetTooltip("NetBird (Connected)") 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() @@ -526,11 +541,13 @@ func (s *serviceClient) setDisconnectedStatus() { } else { systray.SetIcon(s.icDisconnected) } + s.mAbout.SetIcon(s.icDisconnected) systray.SetTooltip("NetBird (Disconnected)") s.mStatus.SetTitle("Disconnected") s.mDown.Disable() s.mUp.Enable() - s.mRoutes.Disable() + s.mNetworks.Disable() + go s.updateExitNodes() } func (s *serviceClient) onTrayReady() { @@ -553,10 +570,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") @@ -575,6 +598,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() @@ -590,6 +616,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() { @@ -604,7 +636,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 } }() @@ -614,7 +646,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 } }() @@ -648,6 +680,19 @@ func (s *serviceClient) onTrayReady() { log.Errorf("failed to update config: %v", err) return } + case <-s.mNotifications.ClickedCh: + if s.mNotifications.Checked() { + s.mNotifications.Uncheck() + } else { + s.mNotifications.Check() + } + if s.eventManager != nil { + s.eventManager.SetNotificationsEnabled(s.mNotifications.Checked()) + } + if err := s.updateConfig(); err != nil { + log.Errorf("failed to update config: %v", err) + return + } case <-s.mAdvancedSettings.ClickedCh: s.mAdvancedSettings.Disable() go func() { @@ -655,6 +700,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 @@ -663,25 +715,12 @@ 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: - if s.mNotifications.Checked() { - s.mNotifications.Uncheck() - } else { - s.mNotifications.Check() - } - if s.eventManager != nil { - s.eventManager.SetNotificationsEnabled(s.mNotifications.Checked()) - } - if err := s.updateConfig(); err != nil { - log.Errorf("failed to update config: %v", err) - return - } } if err != nil { @@ -698,7 +737,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)) @@ -717,7 +760,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 fc3361b6161..00000000000 --- 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 00000000000..845ea284c61 --- /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 7925ee4d357..62a3c7c6a8c 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 852c4765b27..05e06c75e03 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,198 @@ 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) { + resp, err := conn.ListNetworks(s.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: