2022-03-08 14:47:55 +01:00
package cmd
import (
"context"
2023-02-23 20:13:19 +01:00
"encoding/json"
2022-05-12 11:17:24 +02:00
"fmt"
2022-12-01 11:48:13 +01:00
"net"
"net/netip"
"sort"
"strings"
2023-02-24 19:01:54 +01:00
"time"
"github.com/spf13/cobra"
"google.golang.org/grpc/status"
2023-02-24 19:14:22 +01:00
"gopkg.in/yaml.v3"
2022-12-01 11:48:13 +01:00
2022-07-05 19:47:50 +02:00
"github.com/netbirdio/netbird/client/internal"
"github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/client/proto"
2022-03-26 12:08:54 +01:00
"github.com/netbirdio/netbird/util"
2023-03-15 07:54:51 +01:00
"github.com/netbirdio/netbird/version"
2022-07-05 19:47:50 +02:00
)
2022-03-08 14:47:55 +01:00
2023-02-24 19:01:54 +01:00
type peerStateDetailOutput struct {
2023-02-27 17:06:20 +01:00
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" `
Direct bool ` json:"direct" yaml:"direct" `
IceCandidateType iceCandidateType ` json:"iceCandidateType" yaml:"iceCandidateType" `
2023-02-24 19:01:54 +01:00
}
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" `
}
type managementStateOutput struct {
URL string ` json:"url" yaml:"url" `
Connected bool ` json:"connected" yaml:"connected" `
}
2023-02-27 17:06:20 +01:00
type iceCandidateType struct {
Local string ` json:"local" yaml:"local" `
Remote string ` json:"remote" yaml:"remote" `
}
2023-02-24 19:01:54 +01:00
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" `
2023-02-27 17:06:20 +01:00
IP string ` json:"netbirdIp" yaml:"netbirdIp" `
2023-02-24 19:01:54 +01:00
PubKey string ` json:"publicKey" yaml:"publicKey" `
2023-02-27 15:14:41 +01:00
KernelInterface bool ` json:"usesKernelInterface" yaml:"usesKernelInterface" `
2023-02-27 17:06:20 +01:00
FQDN string ` json:"fqdn" yaml:"fqdn" `
2023-02-24 19:01:54 +01:00
}
2022-07-05 19:47:50 +02:00
var (
detailFlag bool
2022-09-22 09:25:52 +02:00
ipv4Flag bool
2023-02-23 20:13:19 +01:00
jsonFlag bool
yamlFlag bool
2022-07-05 19:47:50 +02:00
ipsFilter [ ] string
statusFilter string
ipsFilterMap map [ string ] struct { }
2022-03-08 14:47:55 +01:00
)
var statusCmd = & cobra . Command {
Use : "status" ,
2022-05-22 18:53:47 +02:00
Short : "status of the Netbird Service" ,
2023-02-23 20:13:19 +01:00
RunE : statusFunc ,
}
2022-03-10 18:14:07 +01:00
2023-02-23 20:13:19 +01:00
func init ( ) {
ipsFilterMap = make ( map [ string ] struct { } )
statusCmd . PersistentFlags ( ) . BoolVarP ( & detailFlag , "detail" , "d" , false , "display detailed status information in human-readable format" )
statusCmd . PersistentFlags ( ) . BoolVar ( & jsonFlag , "json" , false , "display detailed status information in json format" )
statusCmd . PersistentFlags ( ) . BoolVar ( & yamlFlag , "yaml" , false , "display detailed status information in yaml format" )
statusCmd . PersistentFlags ( ) . BoolVar ( & ipv4Flag , "ipv4" , false , "display only NetBird IPv4 of this peer, e.g., --ipv4 will output 100.64.0.33" )
statusCmd . MarkFlagsMutuallyExclusive ( "detail" , "json" , "yaml" , "ipv4" )
statusCmd . PersistentFlags ( ) . StringSliceVar ( & ipsFilter , "filter-by-ips" , [ ] string { } , "filters the detailed output by a list of one or more IPs, e.g., --filter-by-ips 100.64.0.100,100.64.0.200" )
statusCmd . PersistentFlags ( ) . StringVar ( & statusFilter , "filter-by-status" , "" , "filters the detailed output by connection status(connected|disconnected), e.g., --filter-by-status connected" )
}
2022-05-22 18:53:47 +02:00
2023-02-23 20:13:19 +01:00
func statusFunc ( cmd * cobra . Command , args [ ] string ) error {
SetFlagsFromEnvVars ( rootCmd )
2022-07-05 19:47:50 +02:00
2023-02-23 20:13:19 +01:00
cmd . SetOut ( cmd . OutOrStdout ( ) )
2022-03-10 18:14:07 +01:00
2023-02-23 20:13:19 +01:00
err := parseFilters ( )
if err != nil {
return err
}
2022-03-08 14:47:55 +01:00
2023-02-23 20:13:19 +01:00
err = util . InitLog ( logLevel , "console" )
if err != nil {
return fmt . Errorf ( "failed initializing log %v" , err )
}
2022-03-08 14:47:55 +01:00
2023-02-23 20:13:19 +01:00
ctx := internal . CtxInitState ( context . Background ( ) )
2022-05-12 11:17:24 +02:00
2023-02-24 19:01:54 +01:00
resp , _ := getStatus ( ctx , cmd )
2023-02-23 20:13:19 +01:00
if err != nil {
2023-02-24 19:01:54 +01:00
return nil
2023-02-23 20:13:19 +01:00
}
2022-07-05 19:47:50 +02:00
2023-02-23 20:13:19 +01:00
if resp . GetStatus ( ) == string ( internal . StatusNeedsLogin ) || resp . GetStatus ( ) == string ( internal . StatusLoginFailed ) {
2023-02-24 19:01:54 +01:00
cmd . Printf ( "Daemon status: %s\n\n" +
2023-02-23 20:13:19 +01:00
"Run UP command to log in with SSO (interactive login):\n\n" +
" netbird up \n\n" +
"If you are running a self-hosted version and no SSO provider has been configured in your Management Server,\n" +
"you can use a setup-key:\n\n netbird up --management-url <YOUR_MANAGEMENT_URL> --setup-key <YOUR_SETUP_KEY>\n\n" +
"More info: https://www.netbird.io/docs/overview/setup-keys\n\n" ,
2023-02-24 19:01:54 +01:00
resp . GetStatus ( ) ,
2023-02-23 20:13:19 +01:00
)
2022-03-08 14:47:55 +01:00
return nil
2023-02-23 20:13:19 +01:00
}
2022-07-05 19:47:50 +02:00
2023-02-24 19:01:54 +01:00
if ipv4Flag {
cmd . Print ( parseInterfaceIP ( resp . GetFullStatus ( ) . GetLocalPeerState ( ) . GetIP ( ) ) )
return nil
}
2023-02-27 17:06:20 +01:00
outputInformationHolder := convertToStatusOutputOverview ( resp )
2023-02-23 20:13:19 +01:00
statusOutputString := ""
2023-02-27 17:06:20 +01:00
switch {
case detailFlag :
statusOutputString = parseToFullDetailSummary ( outputInformationHolder )
case jsonFlag :
statusOutputString , err = parseToJSON ( outputInformationHolder )
case yamlFlag :
statusOutputString , err = parseToYAML ( outputInformationHolder )
default :
statusOutputString = parseGeneralSummary ( outputInformationHolder , false )
}
if err != nil {
return err
2023-02-23 20:13:19 +01:00
}
cmd . Print ( statusOutputString )
return nil
2022-07-05 19:47:50 +02:00
}
2023-02-24 19:01:54 +01:00
func getStatus ( ctx context . Context , cmd * cobra . Command ) ( * proto . StatusResponse , error ) {
conn , err := DialClientGRPCServer ( ctx , daemonAddr )
if err != nil {
return nil , fmt . Errorf ( "failed to connect to daemon error: %v\n" +
"If the daemon is not running please run: " +
"\nnetbird service install \nnetbird service start\n" , err )
}
defer conn . Close ( )
resp , err := proto . NewDaemonServiceClient ( conn ) . Status ( cmd . Context ( ) , & proto . StatusRequest { GetFullPeerStatus : true } )
if err != nil {
return nil , fmt . Errorf ( "status failed: %v" , status . Convert ( err ) . Message ( ) )
}
return resp , nil
}
2022-07-05 19:47:50 +02:00
func parseFilters ( ) error {
switch strings . ToLower ( statusFilter ) {
case "" , "disconnected" , "connected" :
default :
return fmt . Errorf ( "wrong status filter, should be one of connected|disconnected, got: %s" , statusFilter )
}
if len ( ipsFilter ) > 0 {
for _ , addr := range ipsFilter {
_ , err := netip . ParseAddr ( addr )
if err != nil {
return fmt . Errorf ( "got an invalid IP address in the filter: address %s, error %s" , addr , err )
}
ipsFilterMap [ addr ] = struct { } { }
}
}
return nil
}
2023-02-24 19:01:54 +01:00
func convertToStatusOutputOverview ( resp * proto . StatusResponse ) statusOutputOverview {
pbFullStatus := resp . GetFullStatus ( )
2022-07-07 13:54:47 +02:00
managementState := pbFullStatus . GetManagementState ( )
2023-02-24 19:01:54 +01:00
managementOverview := managementStateOutput {
URL : managementState . GetURL ( ) ,
Connected : managementState . GetConnected ( ) ,
}
2022-07-05 19:47:50 +02:00
2022-07-07 13:54:47 +02:00
signalState := pbFullStatus . GetSignalState ( )
2023-02-24 19:01:54 +01:00
signalOverview := signalStateOutput {
URL : signalState . GetURL ( ) ,
Connected : signalState . GetConnected ( ) ,
}
2022-07-05 19:47:50 +02:00
2023-02-24 19:01:54 +01:00
peersOverview := mapPeers ( resp . GetFullStatus ( ) . GetPeers ( ) )
2022-07-05 19:47:50 +02:00
2023-02-24 19:01:54 +01:00
overview := statusOutputOverview {
Peers : peersOverview ,
2023-03-15 07:54:51 +01:00
CliVersion : version . NetbirdVersion ( ) ,
2023-02-24 19:01:54 +01:00
DaemonVersion : resp . GetDaemonVersion ( ) ,
ManagementState : managementOverview ,
SignalState : signalOverview ,
2023-02-27 15:14:41 +01:00
IP : pbFullStatus . GetLocalPeerState ( ) . GetIP ( ) ,
2023-02-24 19:01:54 +01:00
PubKey : pbFullStatus . GetLocalPeerState ( ) . GetPubKey ( ) ,
2023-02-27 15:14:41 +01:00
KernelInterface : pbFullStatus . GetLocalPeerState ( ) . GetKernelInterface ( ) ,
2023-02-24 19:01:54 +01:00
FQDN : pbFullStatus . GetLocalPeerState ( ) . GetFqdn ( ) ,
}
return overview
}
func mapPeers ( peers [ ] * proto . PeerState ) peersStateOutput {
var peersStateDetail [ ] peerStateDetailOutput
2023-02-27 15:14:41 +01:00
localICE := ""
remoteICE := ""
connType := ""
2023-02-24 19:01:54 +01:00
peersConnected := 0
for _ , pbPeerState := range peers {
isPeerConnected := pbPeerState . ConnStatus == peer . StatusConnected . String ( )
if skipDetailByFilters ( pbPeerState , isPeerConnected ) {
continue
}
if isPeerConnected {
peersConnected = peersConnected + 1
localICE = pbPeerState . GetLocalIceCandidateType ( )
remoteICE = pbPeerState . GetRemoteIceCandidateType ( )
connType = "P2P"
if pbPeerState . Relayed {
connType = "Relayed"
}
}
2022-07-05 19:47:50 +02:00
2022-07-07 13:54:47 +02:00
timeLocal := pbPeerState . GetConnStatusUpdate ( ) . AsTime ( ) . Local ( )
2023-02-24 19:01:54 +01:00
peerState := peerStateDetailOutput {
2023-02-27 17:06:20 +01:00
IP : pbPeerState . GetIP ( ) ,
PubKey : pbPeerState . GetPubKey ( ) ,
Status : pbPeerState . GetConnStatus ( ) ,
LastStatusUpdate : timeLocal . UTC ( ) ,
ConnType : connType ,
Direct : pbPeerState . GetDirect ( ) ,
IceCandidateType : iceCandidateType {
Local : localICE ,
Remote : remoteICE ,
} ,
FQDN : pbPeerState . GetFqdn ( ) ,
2022-07-05 19:47:50 +02:00
}
2023-02-24 19:01:54 +01:00
peersStateDetail = append ( peersStateDetail , peerState )
2022-07-05 19:47:50 +02:00
}
2023-02-27 15:34:17 +01:00
sortPeersByIP ( peersStateDetail )
2022-07-05 19:47:50 +02:00
2023-02-24 19:01:54 +01:00
peersOverview := peersStateOutput {
Total : len ( peersStateDetail ) ,
Connected : peersConnected ,
Details : peersStateDetail ,
}
return peersOverview
}
2023-02-27 15:34:17 +01:00
func sortPeersByIP ( peersStateDetail [ ] peerStateDetailOutput ) {
2023-02-24 19:01:54 +01:00
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
} )
}
2022-07-05 19:47:50 +02:00
}
2023-02-23 20:13:19 +01:00
func parseInterfaceIP ( interfaceIP string ) string {
2022-09-22 09:25:52 +02:00
ip , _ , err := net . ParseCIDR ( interfaceIP )
if err != nil {
return ""
}
2023-02-23 20:13:19 +01:00
return fmt . Sprintf ( "%s\n" , ip )
}
2022-09-22 09:25:52 +02:00
2023-02-27 15:34:17 +01:00
func parseToJSON ( overview statusOutputOverview ) ( string , error ) {
2023-02-24 19:01:54 +01:00
jsonBytes , err := json . Marshal ( overview )
if err != nil {
return "" , fmt . Errorf ( "json marshal failed" )
}
2023-02-23 20:13:19 +01:00
return string ( jsonBytes ) , err
}
2022-09-22 09:25:52 +02:00
2023-02-27 15:34:17 +01:00
func parseToYAML ( overview statusOutputOverview ) ( string , error ) {
2023-02-24 19:01:54 +01:00
yamlBytes , err := yaml . Marshal ( overview )
if err != nil {
return "" , fmt . Errorf ( "yaml marshal failed" )
2022-07-05 19:47:50 +02:00
}
2023-02-24 19:01:54 +01:00
return string ( yamlBytes ) , nil
2023-02-23 20:13:19 +01:00
}
2023-02-27 15:34:17 +01:00
func parseGeneralSummary ( overview statusOutputOverview , showURL bool ) string {
2023-02-23 20:13:19 +01:00
managementConnString := "Disconnected"
2023-02-24 19:01:54 +01:00
if overview . ManagementState . Connected {
2022-07-05 19:47:50 +02:00
managementConnString = "Connected"
2023-02-27 15:34:17 +01:00
if showURL {
2023-02-24 19:01:54 +01:00
managementConnString = fmt . Sprintf ( "%s to %s" , managementConnString , overview . ManagementState . URL )
}
2022-07-05 19:47:50 +02:00
}
2023-02-23 20:13:19 +01:00
signalConnString := "Disconnected"
2023-02-24 19:01:54 +01:00
if overview . SignalState . Connected {
2022-07-05 19:47:50 +02:00
signalConnString = "Connected"
2023-02-27 15:34:17 +01:00
if showURL {
2023-02-24 19:01:54 +01:00
signalConnString = fmt . Sprintf ( "%s to %s" , signalConnString , overview . SignalState . URL )
}
2022-07-05 19:47:50 +02:00
}
2023-02-27 15:14:41 +01:00
interfaceTypeString := "Userspace"
interfaceIP := overview . IP
if overview . KernelInterface {
interfaceTypeString = "Kernel"
} else if overview . IP == "" {
interfaceTypeString = "N/A"
interfaceIP = "N/A"
}
2023-02-24 19:01:54 +01:00
peersCountString := fmt . Sprintf ( "%d/%d Connected" , overview . Peers . Connected , overview . Peers . Total )
2022-07-05 19:47:50 +02:00
summary := fmt . Sprintf (
2022-08-01 12:42:45 +02:00
"Daemon version: %s\n" +
"CLI version: %s\n" +
2023-02-24 19:01:54 +01:00
"Management: %s\n" +
"Signal: %s\n" +
2023-02-27 17:06:20 +01:00
"FQDN: %s\n" +
2022-07-05 19:47:50 +02:00
"NetBird IP: %s\n" +
"Interface type: %s\n" +
"Peers count: %s\n" ,
2023-02-24 19:01:54 +01:00
overview . DaemonVersion ,
2023-03-15 07:54:51 +01:00
version . NetbirdVersion ( ) ,
2022-07-05 19:47:50 +02:00
managementConnString ,
signalConnString ,
2023-02-24 19:01:54 +01:00
overview . FQDN ,
2023-02-27 15:14:41 +01:00
interfaceIP ,
interfaceTypeString ,
2022-07-05 19:47:50 +02:00
peersCountString ,
)
return summary
}
2023-02-24 19:01:54 +01:00
func parseToFullDetailSummary ( overview statusOutputOverview ) string {
parsedPeersString := parsePeers ( overview . Peers )
summary := parseGeneralSummary ( overview , true )
2023-02-23 20:13:19 +01:00
return fmt . Sprintf (
"Peers detail:" +
"%s\n" +
"%s" ,
parsedPeersString ,
summary ,
)
}
2023-02-24 19:01:54 +01:00
func parsePeers ( peers peersStateOutput ) string {
2022-07-05 19:47:50 +02:00
var (
2023-02-23 20:13:19 +01:00
peersString = ""
2022-07-05 19:47:50 +02:00
)
2023-02-24 19:01:54 +01:00
for _ , peerState := range peers . Details {
2023-02-27 15:14:41 +01:00
localICE := "-"
2023-02-27 17:06:20 +01:00
if peerState . IceCandidateType . Local != "" {
localICE = peerState . IceCandidateType . Local
2023-02-27 15:14:41 +01:00
}
remoteICE := "-"
2023-02-27 17:06:20 +01:00
if peerState . IceCandidateType . Remote != "" {
remoteICE = peerState . IceCandidateType . Remote
2023-02-27 15:14:41 +01:00
}
2023-02-23 20:13:19 +01:00
peerString := fmt . Sprintf (
"\n %s:\n" +
" NetBird IP: %s\n" +
" Public key: %s\n" +
" Status: %s\n" +
" -- detail --\n" +
" Connection type: %s\n" +
" Direct: %t\n" +
" ICE candidate (Local/Remote): %s/%s\n" +
" Last connection update: %s\n" ,
peerState . FQDN ,
peerState . IP ,
peerState . PubKey ,
2023-02-27 17:06:20 +01:00
peerState . Status ,
2023-02-24 19:01:54 +01:00
peerState . ConnType ,
2023-02-23 20:13:19 +01:00
peerState . Direct ,
2023-02-27 15:14:41 +01:00
localICE ,
remoteICE ,
2023-02-27 17:06:20 +01:00
peerState . LastStatusUpdate . Format ( "2006-01-02 15:04:05" ) ,
2023-02-23 20:13:19 +01:00
)
2022-07-05 19:47:50 +02:00
2023-02-23 20:13:19 +01:00
peersString = peersString + peerString
2022-07-05 19:47:50 +02:00
}
2023-02-23 20:13:19 +01:00
return peersString
2022-07-05 19:47:50 +02:00
}
2023-02-24 19:01:54 +01:00
func skipDetailByFilters ( peerState * proto . PeerState , isConnected bool ) bool {
2022-07-05 19:47:50 +02:00
statusEval := false
ipEval := false
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
}
}
return statusEval || ipEval
}