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: