diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9e658b51b..492aa5c2e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -274,6 +274,8 @@ go test -exec sudo ./... ``` > On Windows use a powershell with administrator privileges +> Non-GTK environments will need the `libayatana-appindicator3-dev` (debian/ubuntu) package installed + ## Checklist before submitting a PR As a critical network service and open-source project, we must enforce a few things before submitting the pull-requests: - Keep functions as simple as possible, with a single purpose diff --git a/client/cmd/status.go b/client/cmd/status.go index 1c5dfab26..68d743eb2 100644 --- a/client/cmd/status.go +++ b/client/cmd/status.go @@ -22,14 +22,18 @@ import ( ) 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"` - Direct bool `json:"direct" yaml:"direct"` - IceCandidateType iceCandidateType `json:"iceCandidateType" yaml:"iceCandidateType"` + 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"` + IceCandidateEndpoint iceCandidateType `json:"iceCandidateEndpoint" yaml:"iceCandidateEndpoint"` + LastWireguardHandshake time.Time `json:"lastWireguardHandshake" yaml:"lastWireguardHandshake"` + TransferReceived int64 `json:"transferReceived" yaml:"transferReceived"` + TransferSent int64 `json:"transferSent" yaml:"transferSent"` } type peersStateOutput struct { @@ -41,11 +45,25 @@ type peersStateOutput struct { 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 { @@ -59,6 +77,7 @@ type statusOutputOverview struct { 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"` @@ -146,7 +165,7 @@ func statusFunc(cmd *cobra.Command, args []string) error { case yamlFlag: statusOutputString, err = parseToYAML(outputInformationHolder) default: - statusOutputString = parseGeneralSummary(outputInformationHolder, false) + statusOutputString = parseGeneralSummary(outputInformationHolder, false, false) } if err != nil { @@ -220,14 +239,17 @@ func convertToStatusOutputOverview(resp *proto.StatusResponse) statusOutputOverv 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{ @@ -236,6 +258,7 @@ func convertToStatusOutputOverview(resp *proto.StatusResponse) statusOutputOverv DaemonVersion: resp.GetDaemonVersion(), ManagementState: managementOverview, SignalState: signalOverview, + Relays: relayOverview, IP: pbFullStatus.GetLocalPeerState().GetIP(), PubKey: pbFullStatus.GetLocalPeerState().GetPubKey(), KernelInterface: pbFullStatus.GetLocalPeerState().GetKernelInterface(), @@ -245,12 +268,43 @@ func convertToStatusOutputOverview(resp *proto.StatusResponse) statusOutputOverv 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 mapPeers(peers []*proto.PeerState) peersStateOutput { var peersStateDetail []peerStateDetailOutput localICE := "" remoteICE := "" + localICEEndpoint := "" + remoteICEEndpoint := "" connType := "" peersConnected := 0 + lastHandshake := time.Time{} + transferReceived := int64(0) + transferSent := int64(0) for _, pbPeerState := range peers { isPeerConnected := pbPeerState.ConnStatus == peer.StatusConnected.String() if skipDetailByFilters(pbPeerState, isPeerConnected) { @@ -261,10 +315,15 @@ func mapPeers(peers []*proto.PeerState) peersStateOutput { localICE = pbPeerState.GetLocalIceCandidateType() remoteICE = pbPeerState.GetRemoteIceCandidateType() + localICEEndpoint = pbPeerState.GetLocalIceCandidateEndpoint() + remoteICEEndpoint = pbPeerState.GetRemoteIceCandidateEndpoint() connType = "P2P" if pbPeerState.Relayed { connType = "Relayed" } + lastHandshake = pbPeerState.GetLastWireguardHandshake().AsTime().Local() + transferReceived = pbPeerState.GetBytesRx() + transferSent = pbPeerState.GetBytesTx() } timeLocal := pbPeerState.GetConnStatusUpdate().AsTime().Local() @@ -279,7 +338,14 @@ func mapPeers(peers []*proto.PeerState) peersStateOutput { Local: localICE, Remote: remoteICE, }, - FQDN: pbPeerState.GetFqdn(), + IceCandidateEndpoint: iceCandidateType{ + Local: localICEEndpoint, + Remote: remoteICEEndpoint, + }, + FQDN: pbPeerState.GetFqdn(), + LastWireguardHandshake: lastHandshake, + TransferReceived: transferReceived, + TransferSent: transferSent, } peersStateDetail = append(peersStateDetail, peerState) @@ -329,22 +395,32 @@ func parseToYAML(overview statusOutputOverview) (string, error) { return string(yamlBytes), nil } -func parseGeneralSummary(overview statusOutputOverview, showURL bool) string { +func parseGeneralSummary(overview statusOutputOverview, showURL bool, showRelays bool) string { - managementConnString := "Disconnected" + 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) + } } - signalConnString := "Disconnected" + 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" @@ -356,6 +432,23 @@ func parseGeneralSummary(overview statusOutputOverview, showURL bool) string { interfaceIP = "N/A" } + var relayAvailableString string + if showRelays { + for _, relay := range overview.Relays.Details { + available := "Available" + reason := "" + if !relay.Available { + available = "Unavailable" + reason = fmt.Sprintf(", reason: %s", relay.Error) + } + relayAvailableString += fmt.Sprintf("\n [%s] is %s%s", relay.URI, available, reason) + + } + } else { + + relayAvailableString = fmt.Sprintf("%d/%d Available", overview.Relays.Available, overview.Relays.Total) + } + peersCountString := fmt.Sprintf("%d/%d Connected", overview.Peers.Connected, overview.Peers.Total) summary := fmt.Sprintf( @@ -363,6 +456,7 @@ func parseGeneralSummary(overview statusOutputOverview, showURL bool) string { "CLI version: %s\n"+ "Management: %s\n"+ "Signal: %s\n"+ + "Relays: %s\n"+ "FQDN: %s\n"+ "NetBird IP: %s\n"+ "Interface type: %s\n"+ @@ -371,6 +465,7 @@ func parseGeneralSummary(overview statusOutputOverview, showURL bool) string { version.NetbirdVersion(), managementConnString, signalConnString, + relayAvailableString, overview.FQDN, interfaceIP, interfaceTypeString, @@ -381,7 +476,7 @@ func parseGeneralSummary(overview statusOutputOverview, showURL bool) string { func parseToFullDetailSummary(overview statusOutputOverview) string { parsedPeersString := parsePeers(overview.Peers) - summary := parseGeneralSummary(overview, true) + summary := parseGeneralSummary(overview, true, true) return fmt.Sprintf( "Peers detail:"+ @@ -409,6 +504,25 @@ func parsePeers(peers peersStateOutput) string { remoteICE = peerState.IceCandidateType.Remote } + localICEEndpoint := "-" + if peerState.IceCandidateEndpoint.Local != "" { + localICEEndpoint = peerState.IceCandidateEndpoint.Local + } + + remoteICEEndpoint := "-" + if peerState.IceCandidateEndpoint.Remote != "" { + remoteICEEndpoint = peerState.IceCandidateEndpoint.Remote + } + lastStatusUpdate := "-" + if !peerState.LastStatusUpdate.IsZero() { + lastStatusUpdate = peerState.LastStatusUpdate.Format("2006-01-02 15:04:05") + } + + lastWireguardHandshake := "-" + if !peerState.LastWireguardHandshake.IsZero() && peerState.LastWireguardHandshake != time.Unix(0, 0) { + lastWireguardHandshake = peerState.LastWireguardHandshake.Format("2006-01-02 15:04:05") + } + peerString := fmt.Sprintf( "\n %s:\n"+ " NetBird IP: %s\n"+ @@ -418,7 +532,10 @@ func parsePeers(peers peersStateOutput) string { " Connection type: %s\n"+ " Direct: %t\n"+ " ICE candidate (Local/Remote): %s/%s\n"+ - " Last connection update: %s\n", + " ICE candidate endpoints (Local/Remote): %s/%s\n"+ + " Last connection update: %s\n"+ + " Last Wireguard handshake: %s\n"+ + " Transfer status (received/sent) %s/%s\n", peerState.FQDN, peerState.IP, peerState.PubKey, @@ -427,7 +544,12 @@ func parsePeers(peers peersStateOutput) string { peerState.Direct, localICE, remoteICE, - peerState.LastStatusUpdate.Format("2006-01-02 15:04:05"), + localICEEndpoint, + remoteICEEndpoint, + lastStatusUpdate, + lastWireguardHandshake, + toIEC(peerState.TransferReceived), + toIEC(peerState.TransferSent), ) peersString += peerString @@ -467,3 +589,17 @@ func skipDetailByFilters(peerState *proto.PeerState, isConnected bool) bool { 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]) +} diff --git a/client/cmd/status_test.go b/client/cmd/status_test.go index 6201c1590..39886e15f 100644 --- a/client/cmd/status_test.go +++ b/client/cmd/status_test.go @@ -1,10 +1,13 @@ package cmd import ( + "bytes" + "encoding/json" "testing" "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "google.golang.org/protobuf/types/known/timestamppb" "github.com/netbirdio/netbird/client/proto" @@ -25,35 +28,59 @@ var resp = &proto.StatusResponse{ 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, - Direct: true, - LocalIceCandidateType: "", - RemoteIceCandidateType: "", + 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, + Direct: true, + LocalIceCandidateType: "", + RemoteIceCandidateType: "", + LocalIceCandidateEndpoint: "", + RemoteIceCandidateEndpoint: "", + LastWireguardHandshake: timestamppb.New(time.Date(2001, time.Month(1), 1, 1, 1, 2, 0, time.UTC)), + BytesRx: 200, + BytesTx: 100, }, { - 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, - Direct: false, - LocalIceCandidateType: "relay", - RemoteIceCandidateType: "prflx", + 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, + Direct: false, + 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, }, }, 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", @@ -82,6 +109,13 @@ var overview = statusOutputOverview{ Local: "", Remote: "", }, + IceCandidateEndpoint: iceCandidateType{ + Local: "", + Remote: "", + }, + LastWireguardHandshake: time.Date(2001, 1, 1, 1, 1, 2, 0, time.UTC), + TransferReceived: 200, + TransferSent: 100, }, { IP: "192.168.178.102", @@ -95,6 +129,13 @@ var overview = statusOutputOverview{ 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, }, }, }, @@ -103,10 +144,28 @@ var overview = statusOutputOverview{ 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", @@ -145,107 +204,163 @@ func TestSortingOfPeers(t *testing.T) { } func TestParsingToJSON(t *testing.T) { - json, _ := parseToJSON(overview) + jsonString, _ := parseToJSON(overview) //@formatter:off - expectedJSON := "{\"" + - "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\"," + - "\"direct\":true," + - "\"iceCandidateType\":" + - "{" + - "\"local\":\"\"," + - "\"remote\":\"\"" + - "}" + - "}," + - "{" + - "\"fqdn\":\"peer-2.awesome-domain.com\"," + - "\"netbirdIp\":\"192.168.178.102\"," + - "\"publicKey\":\"Pubkey2\"," + - "\"status\":\"Connected\"," + - "\"lastStatusUpdate\":\"2002-02-02T02:02:02Z\"," + - "\"connectionType\":\"Relayed\"," + - "\"direct\":false," + - "\"iceCandidateType\":" + - "{" + - "\"local\":\"relay\"," + - "\"remote\":\"prflx\"" + - "}" + - "}" + - "]" + - "}," + - "\"cliVersion\":\"development\"," + - "\"daemonVersion\":\"0.14.1\"," + - "\"management\":" + - "{" + - "\"url\":\"my-awesome-management.com:443\"," + - "\"connected\":true" + - "}," + - "\"signal\":" + - "{\"" + - "url\":\"my-awesome-signal.com:443\"," + - "\"connected\":true" + - "}," + - "\"netbirdIp\":\"192.168.178.100/16\"," + - "\"publicKey\":\"Some-Pub-Key\"," + - "\"usesKernelInterface\":true," + - "\"fqdn\":\"some-localhost.awesome-domain.com\"" + - "}" + 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", + "direct": true, + "iceCandidateType": { + "local": "", + "remote": "" + }, + "iceCandidateEndpoint": { + "local": "", + "remote": "" + }, + "lastWireguardHandshake": "2001-01-01T01:01:02Z", + "transferReceived": 200, + "transferSent": 100 + }, + { + "fqdn": "peer-2.awesome-domain.com", + "netbirdIp": "192.168.178.102", + "publicKey": "Pubkey2", + "status": "Connected", + "lastStatusUpdate": "2002-02-02T02:02:02Z", + "connectionType": "Relayed", + "direct": false, + "iceCandidateType": { + "local": "relay", + "remote": "prflx" + }, + "iceCandidateEndpoint": { + "local": "10.0.0.1:10001", + "remote": "10.0.10.1:10002" + }, + "lastWireguardHandshake": "2002-02-02T02:02:03Z", + "transferReceived": 2000, + "transferSent": 1000 + } + ] + }, + "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" + }` // @formatter:on - assert.Equal(t, expectedJSON, json) + 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:\n" + - " total: 2\n" + - " connected: 2\n" + - " details:\n" + - " - fqdn: peer-1.awesome-domain.com\n" + - " netbirdIp: 192.168.178.101\n" + - " publicKey: Pubkey1\n" + - " status: Connected\n" + - " lastStatusUpdate: 2001-01-01T01:01:01Z\n" + - " connectionType: P2P\n" + - " direct: true\n" + - " iceCandidateType:\n" + - " local: \"\"\n" + - " remote: \"\"\n" + - " - fqdn: peer-2.awesome-domain.com\n" + - " netbirdIp: 192.168.178.102\n" + - " publicKey: Pubkey2\n" + - " status: Connected\n" + - " lastStatusUpdate: 2002-02-02T02:02:02Z\n" + - " connectionType: Relayed\n" + - " direct: false\n" + - " iceCandidateType:\n" + - " local: relay\n" + - " remote: prflx\n" + - "cliVersion: development\n" + - "daemonVersion: 0.14.1\n" + - "management:\n" + - " url: my-awesome-management.com:443\n" + - " connected: true\n" + - "signal:\n" + - " url: my-awesome-signal.com:443\n" + - " connected: true\n" + - "netbirdIp: 192.168.178.100/16\n" + - "publicKey: Some-Pub-Key\n" + - "usesKernelInterface: true\n" + - "fqdn: some-localhost.awesome-domain.com\n" + 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 + direct: true + iceCandidateType: + local: "" + remote: "" + iceCandidateEndpoint: + local: "" + remote: "" + lastWireguardHandshake: 2001-01-01T01:01:02Z + transferReceived: 200 + transferSent: 100 + - fqdn: peer-2.awesome-domain.com + netbirdIp: 192.168.178.102 + publicKey: Pubkey2 + status: Connected + lastStatusUpdate: 2002-02-02T02:02:02Z + connectionType: Relayed + direct: false + iceCandidateType: + local: relay + remote: prflx + iceCandidateEndpoint: + local: 10.0.0.1:10001 + remote: 10.0.10.1:10002 + lastWireguardHandshake: 2002-02-02T02:02:03Z + transferReceived: 2000 + transferSent: 1000 +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 +` assert.Equal(t, expectedYAML, yaml) } @@ -253,50 +368,64 @@ func TestParsingToYAML(t *testing.T) { func TestParsingToDetail(t *testing.T) { detail := parseToFullDetailSummary(overview) - expectedDetail := "Peers detail:\n" + - " peer-1.awesome-domain.com:\n" + - " NetBird IP: 192.168.178.101\n" + - " Public key: Pubkey1\n" + - " Status: Connected\n" + - " -- detail --\n" + - " Connection type: P2P\n" + - " Direct: true\n" + - " ICE candidate (Local/Remote): -/-\n" + - " Last connection update: 2001-01-01 01:01:01\n" + - "\n" + - " peer-2.awesome-domain.com:\n" + - " NetBird IP: 192.168.178.102\n" + - " Public key: Pubkey2\n" + - " Status: Connected\n" + - " -- detail --\n" + - " Connection type: Relayed\n" + - " Direct: false\n" + - " ICE candidate (Local/Remote): relay/prflx\n" + - " Last connection update: 2002-02-02 02:02:02\n" + - "\n" + - "Daemon version: 0.14.1\n" + - "CLI version: development\n" + - "Management: Connected to my-awesome-management.com:443\n" + - "Signal: Connected to my-awesome-signal.com:443\n" + - "FQDN: some-localhost.awesome-domain.com\n" + - "NetBird IP: 192.168.178.100/16\n" + - "Interface type: Kernel\n" + - "Peers count: 2/2 Connected\n" + expectedDetail := + `Peers detail: + peer-1.awesome-domain.com: + NetBird IP: 192.168.178.101 + Public key: Pubkey1 + Status: Connected + -- detail -- + Connection type: P2P + Direct: true + ICE candidate (Local/Remote): -/- + ICE candidate endpoints (Local/Remote): -/- + Last connection update: 2001-01-01 01:01:01 + Last Wireguard handshake: 2001-01-01 01:01:02 + Transfer status (received/sent) 200 B/100 B + + peer-2.awesome-domain.com: + NetBird IP: 192.168.178.102 + Public key: Pubkey2 + Status: Connected + -- detail -- + Connection type: Relayed + Direct: false + ICE candidate (Local/Remote): relay/prflx + ICE candidate endpoints (Local/Remote): 10.0.0.1:10001/10.0.10.1:10002 + Last connection update: 2002-02-02 02:02:02 + Last Wireguard handshake: 2002-02-02 02:02:03 + Transfer status (received/sent) 2.0 KiB/1000 B + +Daemon version: 0.14.1 +CLI version: development +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 +FQDN: some-localhost.awesome-domain.com +NetBird IP: 192.168.178.100/16 +Interface type: Kernel +Peers count: 2/2 Connected +` assert.Equal(t, expectedDetail, detail) } func TestParsingToShortVersion(t *testing.T) { - shortVersion := parseGeneralSummary(overview, false) + shortVersion := parseGeneralSummary(overview, false, false) - expectedString := "Daemon version: 0.14.1\n" + - "CLI version: development\n" + - "Management: Connected\n" + - "Signal: Connected\n" + - "FQDN: some-localhost.awesome-domain.com\n" + - "NetBird IP: 192.168.178.100/16\n" + - "Interface type: Kernel\n" + - "Peers count: 2/2 Connected\n" + expectedString := + `Daemon version: 0.14.1 +CLI version: development +Management: Connected +Signal: Connected +Relays: 1/2 Available +FQDN: some-localhost.awesome-domain.com +NetBird IP: 192.168.178.100/16 +Interface type: Kernel +Peers count: 2/2 Connected +` assert.Equal(t, expectedString, shortVersion) } diff --git a/client/internal/connect.go b/client/internal/connect.go index 939c6483c..575269956 100644 --- a/client/internal/connect.go +++ b/client/internal/connect.go @@ -27,11 +27,33 @@ import ( // RunClient with main logic. func RunClient(ctx context.Context, config *Config, statusRecorder *peer.Status) error { - return runClient(ctx, config, statusRecorder, MobileDependency{}) + return runClient(ctx, config, statusRecorder, MobileDependency{}, nil, nil, nil, nil) +} + +// RunClientWithProbes runs the client's main logic with probes attached +func RunClientWithProbes( + ctx context.Context, + config *Config, + statusRecorder *peer.Status, + mgmProbe *Probe, + signalProbe *Probe, + relayProbe *Probe, + wgProbe *Probe, +) error { + return runClient(ctx, config, statusRecorder, MobileDependency{}, mgmProbe, signalProbe, relayProbe, wgProbe) } // RunClientMobile with main logic on mobile system -func RunClientMobile(ctx context.Context, config *Config, statusRecorder *peer.Status, tunAdapter iface.TunAdapter, iFaceDiscover stdnet.ExternalIFaceDiscover, networkChangeListener listener.NetworkChangeListener, dnsAddresses []string, dnsReadyListener dns.ReadyListener) error { +func RunClientMobile( + ctx context.Context, + config *Config, + statusRecorder *peer.Status, + tunAdapter iface.TunAdapter, + iFaceDiscover stdnet.ExternalIFaceDiscover, + networkChangeListener listener.NetworkChangeListener, + dnsAddresses []string, + dnsReadyListener dns.ReadyListener, +) error { // in case of non Android os these variables will be nil mobileDependency := MobileDependency{ TunAdapter: tunAdapter, @@ -40,19 +62,35 @@ func RunClientMobile(ctx context.Context, config *Config, statusRecorder *peer.S HostDNSAddresses: dnsAddresses, DnsReadyListener: dnsReadyListener, } - return runClient(ctx, config, statusRecorder, mobileDependency) + return runClient(ctx, config, statusRecorder, mobileDependency, nil, nil, nil, nil) } -func RunClientiOS(ctx context.Context, config *Config, statusRecorder *peer.Status, fileDescriptor int32, networkChangeListener listener.NetworkChangeListener, dnsManager dns.IosDnsManager) error { +func RunClientiOS( + ctx context.Context, + config *Config, + statusRecorder *peer.Status, + fileDescriptor int32, + networkChangeListener listener.NetworkChangeListener, + dnsManager dns.IosDnsManager, +) error { mobileDependency := MobileDependency{ FileDescriptor: fileDescriptor, NetworkChangeListener: networkChangeListener, DnsManager: dnsManager, } - return runClient(ctx, config, statusRecorder, mobileDependency) + return runClient(ctx, config, statusRecorder, mobileDependency, nil, nil, nil, nil) } -func runClient(ctx context.Context, config *Config, statusRecorder *peer.Status, mobileDependency MobileDependency) error { +func runClient( + ctx context.Context, + config *Config, + statusRecorder *peer.Status, + mobileDependency MobileDependency, + mgmProbe *Probe, + signalProbe *Probe, + relayProbe *Probe, + wgProbe *Probe, +) error { log.Infof("starting NetBird client version %s", version.NetbirdVersion()) backOff := &backoff.ExponentialBackOff{ @@ -103,7 +141,7 @@ func runClient(ctx context.Context, config *Config, statusRecorder *peer.Status, engineCtx, cancel := context.WithCancel(ctx) defer func() { - statusRecorder.MarkManagementDisconnected() + statusRecorder.MarkManagementDisconnected(state.err) statusRecorder.CleanLocalPeerState() cancel() }() @@ -152,8 +190,10 @@ func runClient(ctx context.Context, config *Config, statusRecorder *peer.Status, statusRecorder.UpdateSignalAddress(signalURL) - statusRecorder.MarkSignalDisconnected() - defer statusRecorder.MarkSignalDisconnected() + statusRecorder.MarkSignalDisconnected(nil) + defer func() { + statusRecorder.MarkSignalDisconnected(state.err) + }() // with the global Wiretrustee config in hand connect (just a connection, no stream yet) Signal signalClient, err := connectToSignal(engineCtx, loginResp.GetWiretrusteeConfig(), myPrivateKey) @@ -181,7 +221,7 @@ func runClient(ctx context.Context, config *Config, statusRecorder *peer.Status, return wrapErr(err) } - engine := NewEngine(engineCtx, cancel, signalClient, mgmClient, engineConfig, mobileDependency, statusRecorder) + engine := NewEngineWithProbes(engineCtx, cancel, signalClient, mgmClient, engineConfig, mobileDependency, statusRecorder, mgmProbe, signalProbe, relayProbe, wgProbe) err = engine.Start() if err != nil { log.Errorf("error while starting Netbird Connection Engine: %s", err) diff --git a/client/internal/dns/server_test.go b/client/internal/dns/server_test.go index 5f67a6ece..68c4992d8 100644 --- a/client/internal/dns/server_test.go +++ b/client/internal/dns/server_test.go @@ -59,6 +59,10 @@ func (w *mocWGIface) SetFilter(filter iface.PacketFilter) error { return nil } +func (w *mocWGIface) GetStats(_ string) (iface.WGStats, error) { + return iface.WGStats{}, nil +} + var zoneRecords = []nbdns.SimpleRecord{ { Name: "peera.netbird.cloud", diff --git a/client/internal/dns/wgiface.go b/client/internal/dns/wgiface.go index ee487867a..2c34f1c47 100644 --- a/client/internal/dns/wgiface.go +++ b/client/internal/dns/wgiface.go @@ -11,4 +11,5 @@ type WGIface interface { IsUserspaceBind() bool GetFilter() iface.PacketFilter GetDevice() *iface.DeviceWrapper + GetStats(peerKey string) (iface.WGStats, error) } diff --git a/client/internal/dns/wgiface_windows.go b/client/internal/dns/wgiface_windows.go index 80705a9c0..f8bb80fb9 100644 --- a/client/internal/dns/wgiface_windows.go +++ b/client/internal/dns/wgiface_windows.go @@ -9,5 +9,6 @@ type WGIface interface { IsUserspaceBind() bool GetFilter() iface.PacketFilter GetDevice() *iface.DeviceWrapper + GetStats(peerKey string) (iface.WGStats, error) GetInterfaceGUIDString() (string, error) } diff --git a/client/internal/engine.go b/client/internal/engine.go index 82405a5aa..bbce1dced 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -22,6 +22,7 @@ import ( "github.com/netbirdio/netbird/client/internal/acl" "github.com/netbirdio/netbird/client/internal/dns" "github.com/netbirdio/netbird/client/internal/peer" + "github.com/netbirdio/netbird/client/internal/relay" "github.com/netbirdio/netbird/client/internal/rosenpass" "github.com/netbirdio/netbird/client/internal/routemanager" "github.com/netbirdio/netbird/client/internal/wgproxy" @@ -125,6 +126,11 @@ type Engine struct { acl acl.Manager dnsServer dns.Server + + mgmProbe *Probe + signalProbe *Probe + relayProbe *Probe + wgProbe *Probe } // Peer is an instance of the Connection Peer @@ -135,11 +141,43 @@ type Peer struct { // NewEngine creates a new Connection Engine func NewEngine( - ctx context.Context, cancel context.CancelFunc, - signalClient signal.Client, mgmClient mgm.Client, - config *EngineConfig, mobileDep MobileDependency, statusRecorder *peer.Status, + ctx context.Context, + cancel context.CancelFunc, + signalClient signal.Client, + mgmClient mgm.Client, + config *EngineConfig, + mobileDep MobileDependency, + statusRecorder *peer.Status, ) *Engine { + return NewEngineWithProbes( + ctx, + cancel, + signalClient, + mgmClient, + config, + mobileDep, + statusRecorder, + nil, + nil, + nil, + nil, + ) +} +// NewEngineWithProbes creates a new Connection Engine with probes attached +func NewEngineWithProbes( + ctx context.Context, + cancel context.CancelFunc, + signalClient signal.Client, + mgmClient mgm.Client, + config *EngineConfig, + mobileDep MobileDependency, + statusRecorder *peer.Status, + mgmProbe *Probe, + signalProbe *Probe, + relayProbe *Probe, + wgProbe *Probe, +) *Engine { return &Engine{ ctx: ctx, cancel: cancel, @@ -155,6 +193,10 @@ func NewEngine( sshServerFunc: nbssh.DefaultSSHServer, statusRecorder: statusRecorder, wgProxyFactory: wgproxy.NewFactory(config.WgPort), + mgmProbe: mgmProbe, + signalProbe: signalProbe, + relayProbe: relayProbe, + wgProbe: wgProbe, } } @@ -251,6 +293,7 @@ func (e *Engine) Start() error { e.receiveSignalEvents() e.receiveManagementEvents() + e.receiveProbeEvents() return nil } @@ -512,9 +555,7 @@ func (e *Engine) updateConfig(conf *mgmProto.PeerConfig) error { // E.g. when a new peer has been registered and we are allowed to connect to it. func (e *Engine) receiveManagementEvents() { go func() { - err := e.mgmClient.Sync(func(update *mgmProto.SyncResponse) error { - return e.handleSync(update) - }) + err := e.mgmClient.Sync(e.handleSync) if err != nil { // happens if management is unavailable for a long time. // We want to cancel the operation of the whole client @@ -1175,3 +1216,69 @@ func (e *Engine) getRosenpassAddr() string { } return "" } + +func (e *Engine) receiveProbeEvents() { + if e.signalProbe != nil { + go e.signalProbe.Receive(e.ctx, func() bool { + healthy := e.signal.IsHealthy() + log.Debugf("received signal probe request, healthy: %t", healthy) + return healthy + }) + } + + if e.mgmProbe != nil { + go e.mgmProbe.Receive(e.ctx, func() bool { + healthy := e.mgmClient.IsHealthy() + log.Debugf("received management probe request, healthy: %t", healthy) + return healthy + }) + } + + if e.relayProbe != nil { + go e.relayProbe.Receive(e.ctx, func() bool { + healthy := true + + results := append(e.probeSTUNs(), e.probeTURNs()...) + e.statusRecorder.UpdateRelayStates(results) + + // A single failed server will result in a "failed" probe + for _, res := range results { + if res.Err != nil { + healthy = false + break + } + } + + log.Debugf("received relay probe request, healthy: %t", healthy) + return healthy + }) + } + + if e.wgProbe != nil { + go e.wgProbe.Receive(e.ctx, func() bool { + log.Debug("received wg probe request") + + for _, peer := range e.peerConns { + key := peer.GetKey() + wgStats, err := peer.GetConf().WgConfig.WgInterface.GetStats(key) + if err != nil { + log.Debugf("failed to get wg stats for peer %s: %s", key, err) + } + // wgStats could be zero value, in which case we just reset the stats + if err := e.statusRecorder.UpdateWireguardPeerState(key, wgStats); err != nil { + log.Debugf("failed to update wg stats for peer %s: %s", key, err) + } + } + + return true + }) + } +} + +func (e *Engine) probeSTUNs() []relay.ProbeResult { + return relay.ProbeAll(e.ctx, relay.ProbeSTUN, e.STUNs) +} + +func (e *Engine) probeTURNs() []relay.ProbeResult { + return relay.ProbeAll(e.ctx, relay.ProbeTURN, e.TURNs) +} diff --git a/client/internal/peer/conn.go b/client/internal/peer/conn.go index b4c969dfc..05cfbeec0 100644 --- a/client/internal/peer/conn.go +++ b/client/internal/peer/conn.go @@ -408,12 +408,14 @@ func (conn *Conn) configureConnection(remoteConn net.Conn, remoteWgPort int, rem conn.status = StatusConnected peerState := State{ - PubKey: conn.config.Key, - ConnStatus: conn.status, - ConnStatusUpdate: time.Now(), - LocalIceCandidateType: pair.Local.Type().String(), - RemoteIceCandidateType: pair.Remote.Type().String(), - Direct: !isRelayCandidate(pair.Local), + PubKey: conn.config.Key, + ConnStatus: conn.status, + ConnStatusUpdate: time.Now(), + LocalIceCandidateType: pair.Local.Type().String(), + RemoteIceCandidateType: pair.Remote.Type().String(), + LocalIceCandidateEndpoint: fmt.Sprintf("%s:%d", pair.Local.Address(), pair.Local.Port()), + RemoteIceCandidateEndpoint: fmt.Sprintf("%s:%d", pair.Remote.Address(), pair.Local.Port()), + Direct: !isRelayCandidate(pair.Local), } if pair.Local.Type() == ice.CandidateTypeRelay || pair.Remote.Type() == ice.CandidateTypeRelay { peerState.Relayed = true @@ -500,6 +502,9 @@ func (conn *Conn) cleanup() error { // todo rethink status updates log.Debugf("error while updating peer's %s state, err: %v", conn.config.Key, err) } + if err := conn.statusRecorder.UpdateWireguardPeerState(conn.config.Key, iface.WGStats{}); err != nil { + log.Debugf("failed to reset wireguard stats for peer %s: %s", conn.config.Key, err) + } log.Debugf("cleaned up connection to peer %s", conn.config.Key) if err1 != nil { diff --git a/client/internal/peer/status.go b/client/internal/peer/status.go index c69354eac..235e44184 100644 --- a/client/internal/peer/status.go +++ b/client/internal/peer/status.go @@ -4,19 +4,27 @@ import ( "errors" "sync" "time" + + "github.com/netbirdio/netbird/client/internal/relay" + "github.com/netbirdio/netbird/iface" ) // State contains the latest state of a peer type State struct { - IP string - PubKey string - FQDN string - ConnStatus ConnStatus - ConnStatusUpdate time.Time - Relayed bool - Direct bool - LocalIceCandidateType string - RemoteIceCandidateType string + IP string + PubKey string + FQDN string + ConnStatus ConnStatus + ConnStatusUpdate time.Time + Relayed bool + Direct bool + LocalIceCandidateType string + RemoteIceCandidateType string + LocalIceCandidateEndpoint string + RemoteIceCandidateEndpoint string + LastWireguardHandshake time.Time + BytesTx int64 + BytesRx int64 } // LocalPeerState contains the latest state of the local peer @@ -31,12 +39,14 @@ type LocalPeerState struct { type SignalState struct { URL string Connected bool + Error error } // ManagementState contains the latest state of a management connection type ManagementState struct { URL string Connected bool + Error error } // FullStatus contains the full state held by the Status instance @@ -45,15 +55,19 @@ type FullStatus struct { ManagementState ManagementState SignalState SignalState LocalPeerState LocalPeerState + Relays []relay.ProbeResult } -// Status holds a state of peers, signal and management connections +// Status holds a state of peers, signal, management connections and relays type Status struct { mux sync.Mutex peers map[string]State changeNotify map[string]chan struct{} signalState bool + signalError error managementState bool + managementError error + relayStates []relay.ProbeResult localPeer LocalPeerState offlinePeers []State mgmAddress string @@ -156,6 +170,8 @@ func (d *Status) UpdatePeerState(receivedState State) error { peerState.Relayed = receivedState.Relayed peerState.LocalIceCandidateType = receivedState.LocalIceCandidateType peerState.RemoteIceCandidateType = receivedState.RemoteIceCandidateType + peerState.LocalIceCandidateEndpoint = receivedState.LocalIceCandidateEndpoint + peerState.RemoteIceCandidateEndpoint = receivedState.RemoteIceCandidateEndpoint } d.peers[receivedState.PubKey] = peerState @@ -174,6 +190,25 @@ func (d *Status) UpdatePeerState(receivedState State) error { return nil } +// UpdateWireguardPeerState updates the wireguard bits of the peer state +func (d *Status) UpdateWireguardPeerState(pubKey string, wgStats iface.WGStats) error { + d.mux.Lock() + defer d.mux.Unlock() + + peerState, ok := d.peers[pubKey] + if !ok { + return errors.New("peer doesn't exist") + } + + peerState.LastWireguardHandshake = wgStats.LastHandshake + peerState.BytesRx = wgStats.RxBytes + peerState.BytesTx = wgStats.TxBytes + + d.peers[pubKey] = peerState + + return nil +} + func shouldSkipNotify(received, curr State) bool { switch { case received.ConnStatus == StatusConnecting: @@ -248,12 +283,13 @@ func (d *Status) CleanLocalPeerState() { } // MarkManagementDisconnected sets ManagementState to disconnected -func (d *Status) MarkManagementDisconnected() { +func (d *Status) MarkManagementDisconnected(err error) { d.mux.Lock() defer d.mux.Unlock() defer d.onConnectionChanged() d.managementState = false + d.managementError = err } // MarkManagementConnected sets ManagementState to connected @@ -263,6 +299,7 @@ func (d *Status) MarkManagementConnected() { defer d.onConnectionChanged() d.managementState = true + d.managementError = nil } // UpdateSignalAddress update the address of the signal server @@ -280,12 +317,13 @@ func (d *Status) UpdateManagementAddress(mgmAddress string) { } // MarkSignalDisconnected sets SignalState to disconnected -func (d *Status) MarkSignalDisconnected() { +func (d *Status) MarkSignalDisconnected(err error) { d.mux.Lock() defer d.mux.Unlock() defer d.onConnectionChanged() d.signalState = false + d.signalError = err } // MarkSignalConnected sets SignalState to connected @@ -295,6 +333,33 @@ func (d *Status) MarkSignalConnected() { defer d.onConnectionChanged() d.signalState = true + d.signalError = nil +} + +func (d *Status) UpdateRelayStates(relayResults []relay.ProbeResult) { + d.mux.Lock() + defer d.mux.Unlock() + d.relayStates = relayResults +} + +func (d *Status) GetManagementState() ManagementState { + return ManagementState{ + d.mgmAddress, + d.managementState, + d.managementError, + } +} + +func (d *Status) GetSignalState() SignalState { + return SignalState{ + d.signalAddress, + d.signalState, + d.signalError, + } +} + +func (d *Status) GetRelayStates() []relay.ProbeResult { + return d.relayStates } // GetFullStatus gets full status @@ -303,15 +368,10 @@ func (d *Status) GetFullStatus() FullStatus { defer d.mux.Unlock() fullStatus := FullStatus{ - ManagementState: ManagementState{ - d.mgmAddress, - d.managementState, - }, - SignalState: SignalState{ - d.signalAddress, - d.signalState, - }, - LocalPeerState: d.localPeer, + ManagementState: d.GetManagementState(), + SignalState: d.GetSignalState(), + LocalPeerState: d.localPeer, + Relays: d.GetRelayStates(), } for _, status := range d.peers { diff --git a/client/internal/peer/status_test.go b/client/internal/peer/status_test.go index 5730b1cf1..9038371bd 100644 --- a/client/internal/peer/status_test.go +++ b/client/internal/peer/status_test.go @@ -1,6 +1,7 @@ package peer import ( + "errors" "testing" "github.com/stretchr/testify/assert" @@ -152,9 +153,10 @@ func TestUpdateSignalState(t *testing.T) { name string connected bool want bool + err error }{ - {"should mark as connected", true, true}, - {"should mark as disconnected", false, false}, + {"should mark as connected", true, true, nil}, + {"should mark as disconnected", false, false, errors.New("test")}, } status := NewRecorder("https://mgm") @@ -165,9 +167,10 @@ func TestUpdateSignalState(t *testing.T) { if test.connected { status.MarkSignalConnected() } else { - status.MarkSignalDisconnected() + status.MarkSignalDisconnected(test.err) } assert.Equal(t, test.want, status.signalState, "signal status should be equal") + assert.Equal(t, test.err, status.signalError) }) } } @@ -178,9 +181,10 @@ func TestUpdateManagementState(t *testing.T) { name string connected bool want bool + err error }{ - {"should mark as connected", true, true}, - {"should mark as disconnected", false, false}, + {"should mark as connected", true, true, nil}, + {"should mark as disconnected", false, false, errors.New("test")}, } status := NewRecorder(url) @@ -190,9 +194,10 @@ func TestUpdateManagementState(t *testing.T) { if test.connected { status.MarkManagementConnected() } else { - status.MarkManagementDisconnected() + status.MarkManagementDisconnected(test.err) } assert.Equal(t, test.want, status.managementState, "signalState status should be equal") + assert.Equal(t, test.err, status.managementError) }) } } diff --git a/client/internal/probe.go b/client/internal/probe.go new file mode 100644 index 000000000..743b6b190 --- /dev/null +++ b/client/internal/probe.go @@ -0,0 +1,51 @@ +package internal + +import "context" + +// Probe allows to run on-demand callbacks from different code locations. +// Pass the probe to a receiving and a sending end. The receiving end starts listening +// to requests with Receive and executes a callback when the sending end requests it +// by calling Probe. +type Probe struct { + request chan struct{} + result chan bool + ready bool +} + +// NewProbe returns a new initialized probe. +func NewProbe() *Probe { + return &Probe{ + request: make(chan struct{}), + result: make(chan bool), + } +} + +// Probe requests the callback to be run and returns a bool indicating success. +// It always returns true as long as the receiver is not ready. +func (p *Probe) Probe() bool { + if !p.ready { + return true + } + + p.request <- struct{}{} + return <-p.result +} + +// Receive starts listening for probe requests. On such a request it runs the supplied +// callback func which must return a bool indicating success. +// Blocks until the passed context is cancelled. +func (p *Probe) Receive(ctx context.Context, callback func() bool) { + p.ready = true + defer func() { + p.ready = false + }() + + for { + select { + case <-ctx.Done(): + return + case <-p.request: + p.result <- callback() + } + } +} diff --git a/client/internal/relay/relay.go b/client/internal/relay/relay.go new file mode 100644 index 000000000..1d8e6846d --- /dev/null +++ b/client/internal/relay/relay.go @@ -0,0 +1,171 @@ +package relay + +import ( + "context" + "fmt" + "net" + "sync" + "time" + + "github.com/pion/stun/v2" + "github.com/pion/turn/v3" + log "github.com/sirupsen/logrus" +) + +// ProbeResult holds the info about the result of a relay probe request +type ProbeResult struct { + URI *stun.URI + Err error + Addr string +} + +// ProbeSTUN tries binding to the given STUN uri and acquiring an address +func ProbeSTUN(ctx context.Context, uri *stun.URI) (addr string, probeErr error) { + defer func() { + if probeErr != nil { + log.Debugf("stun probe error from %s: %s", uri, probeErr) + } + }() + + client, err := stun.DialURI(uri, &stun.DialConfig{}) + if err != nil { + probeErr = fmt.Errorf("dial: %w", err) + return + } + + defer func() { + if err := client.Close(); err != nil && probeErr == nil { + probeErr = fmt.Errorf("close: %w", err) + } + }() + + done := make(chan struct{}) + if err = client.Start(stun.MustBuild(stun.TransactionID, stun.BindingRequest), func(res stun.Event) { + if res.Error != nil { + probeErr = fmt.Errorf("request: %w", err) + return + } + + var xorAddr stun.XORMappedAddress + if getErr := xorAddr.GetFrom(res.Message); getErr != nil { + probeErr = fmt.Errorf("get xor addr: %w", err) + return + } + + log.Debugf("stun probe received address from %s: %s", uri, xorAddr) + addr = xorAddr.String() + + done <- struct{}{} + }); err != nil { + probeErr = fmt.Errorf("client: %w", err) + return + } + + select { + case <-ctx.Done(): + probeErr = fmt.Errorf("stun request: %w", ctx.Err()) + return + case <-done: + } + + return addr, nil +} + +// ProbeTURN tries allocating a session from the given TURN URI +func ProbeTURN(ctx context.Context, uri *stun.URI) (addr string, probeErr error) { + defer func() { + if probeErr != nil { + log.Debugf("turn probe error from %s: %s", uri, probeErr) + } + }() + + turnServerAddr := fmt.Sprintf("%s:%d", uri.Host, uri.Port) + + var conn net.PacketConn + switch uri.Proto { + case stun.ProtoTypeUDP: + var err error + conn, err = net.ListenPacket("udp", "") + if err != nil { + probeErr = fmt.Errorf("listen: %w", err) + return + } + case stun.ProtoTypeTCP: + dialer := net.Dialer{} + tcpConn, err := dialer.DialContext(ctx, "tcp", turnServerAddr) + if err != nil { + probeErr = fmt.Errorf("dial: %w", err) + return + } + conn = turn.NewSTUNConn(tcpConn) + default: + probeErr = fmt.Errorf("conn: unknown proto: %s", uri.Proto) + return + } + + defer func() { + if err := conn.Close(); err != nil && probeErr == nil { + probeErr = fmt.Errorf("conn close: %w", err) + } + }() + + cfg := &turn.ClientConfig{ + STUNServerAddr: turnServerAddr, + TURNServerAddr: turnServerAddr, + Conn: conn, + Username: uri.Username, + Password: uri.Password, + } + client, err := turn.NewClient(cfg) + if err != nil { + probeErr = fmt.Errorf("create client: %w", err) + return + } + defer client.Close() + + if err := client.Listen(); err != nil { + probeErr = fmt.Errorf("client listen: %w", err) + return + } + + relayConn, err := client.Allocate() + if err != nil { + probeErr = fmt.Errorf("allocate: %w", err) + return + } + defer func() { + if err := relayConn.Close(); err != nil && probeErr == nil { + probeErr = fmt.Errorf("close relay conn: %w", err) + } + }() + + log.Debugf("turn probe relay address from %s: %s", uri, relayConn.LocalAddr()) + + return relayConn.LocalAddr().String(), nil +} + +// ProbeAll probes all given servers asynchronously and returns the results +func ProbeAll( + ctx context.Context, + fn func(ctx context.Context, uri *stun.URI) (addr string, probeErr error), + relays []*stun.URI, +) []ProbeResult { + results := make([]ProbeResult, len(relays)) + + var wg sync.WaitGroup + for i, uri := range relays { + ctx, cancel := context.WithTimeout(ctx, 1*time.Second) + defer cancel() + + wg.Add(1) + go func(res *ProbeResult, stunURI *stun.URI) { + defer wg.Done() + res.URI = stunURI + res.Addr, res.Err = fn(ctx, stunURI) + }(&results[i], uri) + } + + wg.Wait() + + return results +} diff --git a/client/proto/daemon.pb.go b/client/proto/daemon.pb.go index cccefc91b..c994cad43 100644 --- a/client/proto/daemon.pb.go +++ b/client/proto/daemon.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.26.0 -// protoc v4.24.3 +// protoc v3.21.9 // source: daemon.proto package proto @@ -733,15 +733,20 @@ type PeerState struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - IP string `protobuf:"bytes,1,opt,name=IP,proto3" json:"IP,omitempty"` - PubKey string `protobuf:"bytes,2,opt,name=pubKey,proto3" json:"pubKey,omitempty"` - ConnStatus string `protobuf:"bytes,3,opt,name=connStatus,proto3" json:"connStatus,omitempty"` - ConnStatusUpdate *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=connStatusUpdate,proto3" json:"connStatusUpdate,omitempty"` - Relayed bool `protobuf:"varint,5,opt,name=relayed,proto3" json:"relayed,omitempty"` - Direct bool `protobuf:"varint,6,opt,name=direct,proto3" json:"direct,omitempty"` - LocalIceCandidateType string `protobuf:"bytes,7,opt,name=localIceCandidateType,proto3" json:"localIceCandidateType,omitempty"` - RemoteIceCandidateType string `protobuf:"bytes,8,opt,name=remoteIceCandidateType,proto3" json:"remoteIceCandidateType,omitempty"` - Fqdn string `protobuf:"bytes,9,opt,name=fqdn,proto3" json:"fqdn,omitempty"` + IP string `protobuf:"bytes,1,opt,name=IP,proto3" json:"IP,omitempty"` + PubKey string `protobuf:"bytes,2,opt,name=pubKey,proto3" json:"pubKey,omitempty"` + ConnStatus string `protobuf:"bytes,3,opt,name=connStatus,proto3" json:"connStatus,omitempty"` + ConnStatusUpdate *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=connStatusUpdate,proto3" json:"connStatusUpdate,omitempty"` + Relayed bool `protobuf:"varint,5,opt,name=relayed,proto3" json:"relayed,omitempty"` + Direct bool `protobuf:"varint,6,opt,name=direct,proto3" json:"direct,omitempty"` + LocalIceCandidateType string `protobuf:"bytes,7,opt,name=localIceCandidateType,proto3" json:"localIceCandidateType,omitempty"` + RemoteIceCandidateType string `protobuf:"bytes,8,opt,name=remoteIceCandidateType,proto3" json:"remoteIceCandidateType,omitempty"` + Fqdn string `protobuf:"bytes,9,opt,name=fqdn,proto3" json:"fqdn,omitempty"` + LocalIceCandidateEndpoint string `protobuf:"bytes,10,opt,name=localIceCandidateEndpoint,proto3" json:"localIceCandidateEndpoint,omitempty"` + RemoteIceCandidateEndpoint string `protobuf:"bytes,11,opt,name=remoteIceCandidateEndpoint,proto3" json:"remoteIceCandidateEndpoint,omitempty"` + LastWireguardHandshake *timestamppb.Timestamp `protobuf:"bytes,12,opt,name=lastWireguardHandshake,proto3" json:"lastWireguardHandshake,omitempty"` + BytesRx int64 `protobuf:"varint,13,opt,name=bytesRx,proto3" json:"bytesRx,omitempty"` + BytesTx int64 `protobuf:"varint,14,opt,name=bytesTx,proto3" json:"bytesTx,omitempty"` } func (x *PeerState) Reset() { @@ -839,6 +844,41 @@ func (x *PeerState) GetFqdn() string { return "" } +func (x *PeerState) GetLocalIceCandidateEndpoint() string { + if x != nil { + return x.LocalIceCandidateEndpoint + } + return "" +} + +func (x *PeerState) GetRemoteIceCandidateEndpoint() string { + if x != nil { + return x.RemoteIceCandidateEndpoint + } + return "" +} + +func (x *PeerState) GetLastWireguardHandshake() *timestamppb.Timestamp { + if x != nil { + return x.LastWireguardHandshake + } + return nil +} + +func (x *PeerState) GetBytesRx() int64 { + if x != nil { + return x.BytesRx + } + return 0 +} + +func (x *PeerState) GetBytesTx() int64 { + if x != nil { + return x.BytesTx + } + return 0 +} + // LocalPeerState contains the latest state of the local peer type LocalPeerState struct { state protoimpl.MessageState @@ -919,6 +959,7 @@ type SignalState struct { URL string `protobuf:"bytes,1,opt,name=URL,proto3" json:"URL,omitempty"` Connected bool `protobuf:"varint,2,opt,name=connected,proto3" json:"connected,omitempty"` + Error string `protobuf:"bytes,3,opt,name=error,proto3" json:"error,omitempty"` } func (x *SignalState) Reset() { @@ -967,6 +1008,13 @@ func (x *SignalState) GetConnected() bool { return false } +func (x *SignalState) GetError() string { + if x != nil { + return x.Error + } + return "" +} + // ManagementState contains the latest state of a management connection type ManagementState struct { state protoimpl.MessageState @@ -975,6 +1023,7 @@ type ManagementState struct { URL string `protobuf:"bytes,1,opt,name=URL,proto3" json:"URL,omitempty"` Connected bool `protobuf:"varint,2,opt,name=connected,proto3" json:"connected,omitempty"` + Error string `protobuf:"bytes,3,opt,name=error,proto3" json:"error,omitempty"` } func (x *ManagementState) Reset() { @@ -1023,6 +1072,77 @@ func (x *ManagementState) GetConnected() bool { return false } +func (x *ManagementState) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +// RelayState contains the latest state of the relay +type RelayState struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + URI string `protobuf:"bytes,1,opt,name=URI,proto3" json:"URI,omitempty"` + Available bool `protobuf:"varint,2,opt,name=available,proto3" json:"available,omitempty"` + Error string `protobuf:"bytes,3,opt,name=error,proto3" json:"error,omitempty"` +} + +func (x *RelayState) Reset() { + *x = RelayState{} + if protoimpl.UnsafeEnabled { + mi := &file_daemon_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RelayState) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RelayState) ProtoMessage() {} + +func (x *RelayState) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[16] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RelayState.ProtoReflect.Descriptor instead. +func (*RelayState) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{16} +} + +func (x *RelayState) GetURI() string { + if x != nil { + return x.URI + } + return "" +} + +func (x *RelayState) GetAvailable() bool { + if x != nil { + return x.Available + } + return false +} + +func (x *RelayState) GetError() string { + if x != nil { + return x.Error + } + return "" +} + // FullStatus contains the full state held by the Status instance type FullStatus struct { state protoimpl.MessageState @@ -1033,12 +1153,13 @@ type FullStatus struct { SignalState *SignalState `protobuf:"bytes,2,opt,name=signalState,proto3" json:"signalState,omitempty"` LocalPeerState *LocalPeerState `protobuf:"bytes,3,opt,name=localPeerState,proto3" json:"localPeerState,omitempty"` Peers []*PeerState `protobuf:"bytes,4,rep,name=peers,proto3" json:"peers,omitempty"` + Relays []*RelayState `protobuf:"bytes,5,rep,name=relays,proto3" json:"relays,omitempty"` } func (x *FullStatus) Reset() { *x = FullStatus{} if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[16] + mi := &file_daemon_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1051,7 +1172,7 @@ func (x *FullStatus) String() string { func (*FullStatus) ProtoMessage() {} func (x *FullStatus) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[16] + mi := &file_daemon_proto_msgTypes[17] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1064,7 +1185,7 @@ func (x *FullStatus) ProtoReflect() protoreflect.Message { // Deprecated: Use FullStatus.ProtoReflect.Descriptor instead. func (*FullStatus) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{16} + return file_daemon_proto_rawDescGZIP(), []int{17} } func (x *FullStatus) GetManagementState() *ManagementState { @@ -1095,6 +1216,13 @@ func (x *FullStatus) GetPeers() []*PeerState { return nil } +func (x *FullStatus) GetRelays() []*RelayState { + if x != nil { + return x.Relays + } + return nil +} + var File_daemon_proto protoreflect.FileDescriptor var file_daemon_proto_rawDesc = []byte{ @@ -1190,7 +1318,7 @@ var file_daemon_proto_rawDesc = []byte{ 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x55, 0x52, 0x4c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x55, 0x52, 0x4c, 0x22, - 0xcf, 0x02, 0x0a, 0x09, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, + 0xd5, 0x04, 0x0a, 0x09, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, @@ -1211,62 +1339,89 @@ var file_daemon_proto_rawDesc = []byte{ 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x16, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, - 0x6e, 0x22, 0x76, 0x0a, 0x0e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, - 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x28, 0x0a, 0x0f, 0x6b, - 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x74, 0x65, - 0x72, 0x66, 0x61, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x22, 0x3d, 0x0a, 0x0b, 0x53, 0x69, 0x67, - 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52, 0x4c, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x4c, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, - 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x63, - 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x22, 0x41, 0x0a, 0x0f, 0x4d, 0x61, 0x6e, 0x61, - 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, - 0x52, 0x4c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x4c, 0x12, 0x1c, 0x0a, - 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x22, 0xef, 0x01, 0x0a, 0x0a, - 0x46, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x41, 0x0a, 0x0f, 0x6d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x61, 0x6e, - 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0f, 0x6d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x35, 0x0a, - 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x69, 0x67, 0x6e, - 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, - 0x74, 0x61, 0x74, 0x65, 0x12, 0x3e, 0x0a, 0x0e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, - 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x64, - 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, - 0x74, 0x61, 0x74, 0x65, 0x52, 0x0e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, - 0x74, 0x61, 0x74, 0x65, 0x12, 0x27, 0x0a, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x65, 0x65, - 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x32, 0xf7, 0x02, - 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, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x6e, 0x12, 0x3c, 0x0a, 0x19, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, + 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x0a, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x19, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, + 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, + 0x3e, 0x0a, 0x1a, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, + 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x0b, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x1a, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, + 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, + 0x52, 0x0a, 0x16, 0x6c, 0x61, 0x73, 0x74, 0x57, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, + 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x16, 0x6c, 0x61, 0x73, + 0x74, 0x57, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, + 0x61, 0x6b, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x52, 0x78, 0x18, 0x0d, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x52, 0x78, 0x12, 0x18, 0x0a, + 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x54, 0x78, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, + 0x62, 0x79, 0x74, 0x65, 0x73, 0x54, 0x78, 0x22, 0x76, 0x0a, 0x0e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, + 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, + 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, + 0x79, 0x12, 0x28, 0x0a, 0x0f, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x74, 0x65, 0x72, + 0x66, 0x61, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x6b, 0x65, 0x72, 0x6e, + 0x65, 0x6c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x66, + 0x71, 0x64, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x22, + 0x53, 0x0a, 0x0b, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, + 0x0a, 0x03, 0x55, 0x52, 0x4c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x4c, + 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x12, 0x14, + 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, + 0x72, 0x72, 0x6f, 0x72, 0x22, 0x57, 0x0a, 0x0f, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52, 0x4c, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x4c, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, + 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x63, 0x6f, + 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x52, 0x0a, + 0x0a, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, + 0x52, 0x49, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x49, 0x12, 0x1c, 0x0a, + 0x09, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x09, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, + 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, + 0x72, 0x22, 0x9b, 0x02, 0x0a, 0x0a, 0x46, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x12, 0x41, 0x0a, 0x0f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, + 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x64, 0x61, 0x65, 0x6d, + 0x6f, 0x6e, 0x2e, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, + 0x74, 0x65, 0x52, 0x0f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, + 0x61, 0x74, 0x65, 0x12, 0x35, 0x0a, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, + 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, + 0x6e, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0b, 0x73, + 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x3e, 0x0a, 0x0e, 0x6c, 0x6f, + 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x63, 0x61, + 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0e, 0x6c, 0x6f, 0x63, 0x61, + 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x27, 0x0a, 0x05, 0x70, 0x65, + 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x64, 0x61, 0x65, 0x6d, + 0x6f, 0x6e, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x70, 0x65, + 0x65, 0x72, 0x73, 0x12, 0x2a, 0x0a, 0x06, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x73, 0x18, 0x05, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x6c, + 0x61, 0x79, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x06, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x73, 0x32, + 0xf7, 0x02, 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, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -1281,7 +1436,7 @@ func file_daemon_proto_rawDescGZIP() []byte { return file_daemon_proto_rawDescData } -var file_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 17) +var file_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 18) var file_daemon_proto_goTypes = []interface{}{ (*LoginRequest)(nil), // 0: daemon.LoginRequest (*LoginResponse)(nil), // 1: daemon.LoginResponse @@ -1299,33 +1454,36 @@ var file_daemon_proto_goTypes = []interface{}{ (*LocalPeerState)(nil), // 13: daemon.LocalPeerState (*SignalState)(nil), // 14: daemon.SignalState (*ManagementState)(nil), // 15: daemon.ManagementState - (*FullStatus)(nil), // 16: daemon.FullStatus - (*timestamppb.Timestamp)(nil), // 17: google.protobuf.Timestamp + (*RelayState)(nil), // 16: daemon.RelayState + (*FullStatus)(nil), // 17: daemon.FullStatus + (*timestamppb.Timestamp)(nil), // 18: google.protobuf.Timestamp } var file_daemon_proto_depIdxs = []int32{ - 16, // 0: daemon.StatusResponse.fullStatus:type_name -> daemon.FullStatus - 17, // 1: daemon.PeerState.connStatusUpdate:type_name -> google.protobuf.Timestamp - 15, // 2: daemon.FullStatus.managementState:type_name -> daemon.ManagementState - 14, // 3: daemon.FullStatus.signalState:type_name -> daemon.SignalState - 13, // 4: daemon.FullStatus.localPeerState:type_name -> daemon.LocalPeerState - 12, // 5: daemon.FullStatus.peers:type_name -> daemon.PeerState - 0, // 6: daemon.DaemonService.Login:input_type -> daemon.LoginRequest - 2, // 7: daemon.DaemonService.WaitSSOLogin:input_type -> daemon.WaitSSOLoginRequest - 4, // 8: daemon.DaemonService.Up:input_type -> daemon.UpRequest - 6, // 9: daemon.DaemonService.Status:input_type -> daemon.StatusRequest - 8, // 10: daemon.DaemonService.Down:input_type -> daemon.DownRequest - 10, // 11: daemon.DaemonService.GetConfig:input_type -> daemon.GetConfigRequest - 1, // 12: daemon.DaemonService.Login:output_type -> daemon.LoginResponse - 3, // 13: daemon.DaemonService.WaitSSOLogin:output_type -> daemon.WaitSSOLoginResponse - 5, // 14: daemon.DaemonService.Up:output_type -> daemon.UpResponse - 7, // 15: daemon.DaemonService.Status:output_type -> daemon.StatusResponse - 9, // 16: daemon.DaemonService.Down:output_type -> daemon.DownResponse - 11, // 17: daemon.DaemonService.GetConfig:output_type -> daemon.GetConfigResponse - 12, // [12:18] is the sub-list for method output_type - 6, // [6:12] is the sub-list for method input_type - 6, // [6:6] is the sub-list for extension type_name - 6, // [6:6] is the sub-list for extension extendee - 0, // [0:6] is the sub-list for field type_name + 17, // 0: daemon.StatusResponse.fullStatus:type_name -> daemon.FullStatus + 18, // 1: daemon.PeerState.connStatusUpdate:type_name -> google.protobuf.Timestamp + 18, // 2: daemon.PeerState.lastWireguardHandshake:type_name -> google.protobuf.Timestamp + 15, // 3: daemon.FullStatus.managementState:type_name -> daemon.ManagementState + 14, // 4: daemon.FullStatus.signalState:type_name -> daemon.SignalState + 13, // 5: daemon.FullStatus.localPeerState:type_name -> daemon.LocalPeerState + 12, // 6: daemon.FullStatus.peers:type_name -> daemon.PeerState + 16, // 7: daemon.FullStatus.relays:type_name -> daemon.RelayState + 0, // 8: daemon.DaemonService.Login:input_type -> daemon.LoginRequest + 2, // 9: daemon.DaemonService.WaitSSOLogin:input_type -> daemon.WaitSSOLoginRequest + 4, // 10: daemon.DaemonService.Up:input_type -> daemon.UpRequest + 6, // 11: daemon.DaemonService.Status:input_type -> daemon.StatusRequest + 8, // 12: daemon.DaemonService.Down:input_type -> daemon.DownRequest + 10, // 13: daemon.DaemonService.GetConfig:input_type -> daemon.GetConfigRequest + 1, // 14: daemon.DaemonService.Login:output_type -> daemon.LoginResponse + 3, // 15: daemon.DaemonService.WaitSSOLogin:output_type -> daemon.WaitSSOLoginResponse + 5, // 16: daemon.DaemonService.Up:output_type -> daemon.UpResponse + 7, // 17: daemon.DaemonService.Status:output_type -> daemon.StatusResponse + 9, // 18: daemon.DaemonService.Down:output_type -> daemon.DownResponse + 11, // 19: daemon.DaemonService.GetConfig:output_type -> daemon.GetConfigResponse + 14, // [14:20] is the sub-list for method output_type + 8, // [8:14] is the sub-list for method input_type + 8, // [8:8] is the sub-list for extension type_name + 8, // [8:8] is the sub-list for extension extendee + 0, // [0:8] is the sub-list for field type_name } func init() { file_daemon_proto_init() } @@ -1527,6 +1685,18 @@ func file_daemon_proto_init() { } } file_daemon_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RelayState); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_daemon_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*FullStatus); i { case 0: return &v.state @@ -1546,7 +1716,7 @@ func file_daemon_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_daemon_proto_rawDesc, NumEnums: 0, - NumMessages: 17, + NumMessages: 18, NumExtensions: 0, NumServices: 1, }, diff --git a/client/proto/daemon.proto b/client/proto/daemon.proto index 36fd48fc6..76d9ac209 100644 --- a/client/proto/daemon.proto +++ b/client/proto/daemon.proto @@ -127,15 +127,20 @@ message PeerState { bool relayed = 5; bool direct = 6; string localIceCandidateType = 7; - string remoteIceCandidateType =8; + string remoteIceCandidateType = 8; string fqdn = 9; + string localIceCandidateEndpoint = 10; + string remoteIceCandidateEndpoint = 11; + google.protobuf.Timestamp lastWireguardHandshake = 12; + int64 bytesRx = 13; + int64 bytesTx = 14; } // LocalPeerState contains the latest state of the local peer message LocalPeerState { string IP = 1; string pubKey = 2; - bool kernelInterface =3; + bool kernelInterface = 3; string fqdn = 4; } @@ -143,17 +148,28 @@ message LocalPeerState { message SignalState { string URL = 1; bool connected = 2; + string error = 3; } // ManagementState contains the latest state of a management connection message ManagementState { string URL = 1; bool connected = 2; + string error = 3; } + +// RelayState contains the latest state of the relay +message RelayState { + string URI = 1; + bool available = 2; + string error = 3; +} + // FullStatus contains the full state held by the Status instance message FullStatus { - ManagementState managementState = 1; - SignalState signalState = 2; - LocalPeerState localPeerState = 3; - repeated PeerState peers = 4; + ManagementState managementState = 1; + SignalState signalState = 2; + LocalPeerState localPeerState = 3; + repeated PeerState peers = 4; + repeated RelayState relays = 5; } \ No newline at end of file diff --git a/client/proto/generate.sh b/client/proto/generate.sh index 454bc9db7..52fe23d7f 100755 --- a/client/proto/generate.sh +++ b/client/proto/generate.sh @@ -13,5 +13,5 @@ script_path=$(dirname $(realpath "$0")) cd "$script_path" go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.26 go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.1 -protoc -I ./ ./daemon.proto --go_out=../ --go-grpc_out=../ +protoc -I ./ ./daemon.proto --go_out=../ --go-grpc_out=../ --experimental_allow_proto3_optional cd "$old_pwd" \ No newline at end of file diff --git a/client/server/server.go b/client/server/server.go index 1791832bb..ec59de7e9 100644 --- a/client/server/server.go +++ b/client/server/server.go @@ -21,6 +21,8 @@ import ( "github.com/netbirdio/netbird/version" ) +const probeThreshold = time.Second * 5 + // Server for service control. type Server struct { rootCtx context.Context @@ -37,6 +39,12 @@ type Server struct { proto.UnimplementedDaemonServiceServer statusRecorder *peer.Status + + mgmProbe *internal.Probe + signalProbe *internal.Probe + relayProbe *internal.Probe + wgProbe *internal.Probe + lastProbe time.Time } type oauthAuthFlow struct { @@ -53,7 +61,11 @@ func New(ctx context.Context, configPath, logFile string) *Server { latestConfigInput: internal.ConfigInput{ ConfigPath: configPath, }, - logFile: logFile, + logFile: logFile, + mgmProbe: internal.NewProbe(), + signalProbe: internal.NewProbe(), + relayProbe: internal.NewProbe(), + wgProbe: internal.NewProbe(), } } @@ -105,7 +117,7 @@ func (s *Server) Start() error { } go func() { - if err := internal.RunClient(ctx, config, s.statusRecorder); err != nil { + if err := internal.RunClientWithProbes(ctx, config, s.statusRecorder, s.mgmProbe, s.signalProbe, s.relayProbe, s.wgProbe); err != nil { log.Errorf("init connections: %v", err) } }() @@ -409,7 +421,7 @@ func (s *Server) Up(callerCtx context.Context, _ *proto.UpRequest) (*proto.UpRes } go func() { - if err := internal.RunClient(ctx, s.config, s.statusRecorder); err != nil { + if err := internal.RunClientWithProbes(ctx, s.config, s.statusRecorder, s.mgmProbe, s.signalProbe, s.relayProbe, s.wgProbe); err != nil { log.Errorf("run client connection: %v", err) return } @@ -433,7 +445,7 @@ func (s *Server) Down(_ context.Context, _ *proto.DownRequest) (*proto.DownRespo return &proto.DownResponse{}, nil } -// Status starts engine work in the daemon. +// Status returns the daemon status func (s *Server) Status( _ context.Context, msg *proto.StatusRequest, @@ -455,6 +467,8 @@ func (s *Server) Status( } if msg.GetFullPeerStatus { + s.runProbes() + fullStatus := s.statusRecorder.GetFullStatus() pbFullStatus := toProtoFullStatus(fullStatus) statusResponse.FullStatus = pbFullStatus @@ -463,6 +477,20 @@ func (s *Server) Status( return &statusResponse, nil } +func (s *Server) runProbes() { + if time.Since(s.lastProbe) > probeThreshold { + managementHealthy := s.mgmProbe.Probe() + signalHealthy := s.signalProbe.Probe() + relayHealthy := s.relayProbe.Probe() + wgProbe := s.wgProbe.Probe() + + // Update last time only if all probes were successful + if managementHealthy && signalHealthy && relayHealthy && wgProbe { + s.lastProbe = time.Now() + } + } +} + // GetConfig of the daemon. func (s *Server) GetConfig(_ context.Context, _ *proto.GetConfigRequest) (*proto.GetConfigResponse, error) { s.mutex.Lock() @@ -503,13 +531,20 @@ func toProtoFullStatus(fullStatus peer.FullStatus) *proto.FullStatus { SignalState: &proto.SignalState{}, LocalPeerState: &proto.LocalPeerState{}, Peers: []*proto.PeerState{}, + Relays: []*proto.RelayState{}, } pbFullStatus.ManagementState.URL = fullStatus.ManagementState.URL pbFullStatus.ManagementState.Connected = fullStatus.ManagementState.Connected + if err := fullStatus.ManagementState.Error; err != nil { + pbFullStatus.ManagementState.Error = err.Error() + } pbFullStatus.SignalState.URL = fullStatus.SignalState.URL pbFullStatus.SignalState.Connected = fullStatus.SignalState.Connected + if err := fullStatus.SignalState.Error; err != nil { + pbFullStatus.SignalState.Error = err.Error() + } pbFullStatus.LocalPeerState.IP = fullStatus.LocalPeerState.IP pbFullStatus.LocalPeerState.PubKey = fullStatus.LocalPeerState.PubKey @@ -518,17 +553,34 @@ func toProtoFullStatus(fullStatus peer.FullStatus) *proto.FullStatus { for _, peerState := range fullStatus.Peers { pbPeerState := &proto.PeerState{ - IP: peerState.IP, - PubKey: peerState.PubKey, - ConnStatus: peerState.ConnStatus.String(), - ConnStatusUpdate: timestamppb.New(peerState.ConnStatusUpdate), - Relayed: peerState.Relayed, - Direct: peerState.Direct, - LocalIceCandidateType: peerState.LocalIceCandidateType, - RemoteIceCandidateType: peerState.RemoteIceCandidateType, - Fqdn: peerState.FQDN, + IP: peerState.IP, + PubKey: peerState.PubKey, + ConnStatus: peerState.ConnStatus.String(), + ConnStatusUpdate: timestamppb.New(peerState.ConnStatusUpdate), + Relayed: peerState.Relayed, + Direct: peerState.Direct, + LocalIceCandidateType: peerState.LocalIceCandidateType, + RemoteIceCandidateType: peerState.RemoteIceCandidateType, + LocalIceCandidateEndpoint: peerState.LocalIceCandidateEndpoint, + RemoteIceCandidateEndpoint: peerState.RemoteIceCandidateEndpoint, + Fqdn: peerState.FQDN, + LastWireguardHandshake: timestamppb.New(peerState.LastWireguardHandshake), + BytesRx: peerState.BytesRx, + BytesTx: peerState.BytesTx, } pbFullStatus.Peers = append(pbFullStatus.Peers, pbPeerState) } + + for _, relayState := range fullStatus.Relays { + pbRelayState := &proto.RelayState{ + URI: relayState.URI.String(), + Available: relayState.Err == nil, + } + if err := relayState.Err; err != nil { + pbRelayState.Error = err.Error() + } + pbFullStatus.Relays = append(pbFullStatus.Relays, pbRelayState) + } + return &pbFullStatus } diff --git a/iface/iface.go b/iface/iface.go index 0e6da2547..3ae40ad4c 100644 --- a/iface/iface.go +++ b/iface/iface.go @@ -27,6 +27,12 @@ type WGIface struct { filter PacketFilter } +type WGStats struct { + LastHandshake time.Time + TxBytes int64 + RxBytes int64 +} + // IsUserspaceBind indicates whether this interfaces is userspace with bind.ICEBind func (w *WGIface) IsUserspaceBind() bool { return w.userspaceBind @@ -139,3 +145,8 @@ func (w *WGIface) GetDevice() *DeviceWrapper { return w.tun.Wrapper() } + +// GetStats returns the last handshake time, rx and tx bytes for the given peer +func (w *WGIface) GetStats(peerKey string) (WGStats, error) { + return w.configurer.getStats(peerKey) +} diff --git a/iface/wg_configurer.go b/iface/wg_configurer.go index b56d75084..91c57eb9c 100644 --- a/iface/wg_configurer.go +++ b/iface/wg_configurer.go @@ -14,4 +14,5 @@ type wgConfigurer interface { addAllowedIP(peerKey string, allowedIP string) error removeAllowedIP(peerKey string, allowedIP string) error close() + getStats(peerKey string) (WGStats, error) } diff --git a/iface/wg_configurer_kernel.go b/iface/wg_configurer_kernel.go index 768d7c69f..36fd13cc2 100644 --- a/iface/wg_configurer_kernel.go +++ b/iface/wg_configurer_kernel.go @@ -207,3 +207,15 @@ func (c *wgKernelConfigurer) configure(config wgtypes.Config) error { func (c *wgKernelConfigurer) close() { } + +func (c *wgKernelConfigurer) getStats(peerKey string) (WGStats, error) { + peer, err := c.getPeer(c.deviceName, peerKey) + if err != nil { + return WGStats{}, fmt.Errorf("get wireguard stats: %w", err) + } + return WGStats{ + LastHandshake: peer.LastHandshakeTime, + TxBytes: peer.TransmitBytes, + RxBytes: peer.ReceiveBytes, + }, nil +} diff --git a/iface/wg_configurer_usp.go b/iface/wg_configurer_usp.go index cf12b9900..200bfbc96 100644 --- a/iface/wg_configurer_usp.go +++ b/iface/wg_configurer_usp.go @@ -6,6 +6,7 @@ import ( "net" "os" "runtime" + "strconv" "strings" "time" @@ -207,6 +208,93 @@ func (t *wgUSPConfigurer) close() { } } +func (t *wgUSPConfigurer) getStats(peerKey string) (WGStats, error) { + ipc, err := t.device.IpcGet() + if err != nil { + return WGStats{}, fmt.Errorf("ipc get: %w", err) + } + + stats, err := findPeerInfo(ipc, peerKey, []string{ + "last_handshake_time_sec", + "last_handshake_time_nsec", + "tx_bytes", + "rx_bytes", + }) + if err != nil { + return WGStats{}, fmt.Errorf("find peer info: %w", err) + } + + sec, err := strconv.ParseInt(stats["last_handshake_time_sec"], 10, 64) + if err != nil { + return WGStats{}, fmt.Errorf("parse handshake sec: %w", err) + } + nsec, err := strconv.ParseInt(stats["last_handshake_time_nsec"], 10, 64) + if err != nil { + return WGStats{}, fmt.Errorf("parse handshake nsec: %w", err) + } + txBytes, err := strconv.ParseInt(stats["tx_bytes"], 10, 64) + if err != nil { + return WGStats{}, fmt.Errorf("parse tx_bytes: %w", err) + } + rxBytes, err := strconv.ParseInt(stats["rx_bytes"], 10, 64) + if err != nil { + return WGStats{}, fmt.Errorf("parse rx_bytes: %w", err) + } + + return WGStats{ + LastHandshake: time.Unix(sec, nsec), + TxBytes: txBytes, + RxBytes: rxBytes, + }, nil +} + +func findPeerInfo(ipcInput string, peerKey string, searchConfigKeys []string) (map[string]string, error) { + peerKeyParsed, err := wgtypes.ParseKey(peerKey) + if err != nil { + return nil, fmt.Errorf("parse key: %w", err) + } + + hexKey := hex.EncodeToString(peerKeyParsed[:]) + + lines := strings.Split(ipcInput, "\n") + + configFound := map[string]string{} + foundPeer := false + for _, line := range lines { + line = strings.TrimSpace(line) + + // If we're within the details of the found peer and encounter another public key, + // this means we're starting another peer's details. So, stop. + if strings.HasPrefix(line, "public_key=") && foundPeer { + break + } + + // Identify the peer with the specific public key + if line == fmt.Sprintf("public_key=%s", hexKey) { + foundPeer = true + } + + for _, key := range searchConfigKeys { + if foundPeer && strings.HasPrefix(line, key+"=") { + v := strings.SplitN(line, "=", 2) + configFound[v[0]] = v[1] + } + } + } + + // todo: use multierr + for _, key := range searchConfigKeys { + if _, ok := configFound[key]; !ok { + return configFound, fmt.Errorf("config key not found: %s", key) + } + } + if !foundPeer { + return nil, fmt.Errorf("peer not found: %s", peerKey) + } + + return configFound, nil +} + func toWgUserspaceString(wgCfg wgtypes.Config) string { var sb strings.Builder if wgCfg.PrivateKey != nil { diff --git a/iface/wg_configurer_usp_test.go b/iface/wg_configurer_usp_test.go new file mode 100644 index 000000000..ac0fc6130 --- /dev/null +++ b/iface/wg_configurer_usp_test.go @@ -0,0 +1,104 @@ +package iface + +import ( + "encoding/hex" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" +) + +var ipcFixture = ` +private_key=e84b5a6d2717c1003a13b431570353dbaca9146cf150c5f8575680feba52027a +listen_port=12912 +public_key=b85996fecc9c7f1fc6d2572a76eda11d59bcd20be8e543b15ce4bd85a8e75a33 +preshared_key=188515093e952f5f22e865cef3012e72f8b5f0b598ac0309d5dacce3b70fcf52 +allowed_ip=192.168.4.4/32 +endpoint=[abcd:23::33%2]:51820 +public_key=58402e695ba1772b1cc9309755f043251ea77fdcf10fbe63989ceb7e19321376 +tx_bytes=38333 +rx_bytes=2224 +allowed_ip=192.168.4.6/32 +persistent_keepalive_interval=111 +endpoint=182.122.22.19:3233 +public_key=662e14fd594556f522604703340351258903b64f35553763f19426ab2a515c58 +endpoint=5.152.198.39:51820 +allowed_ip=192.168.4.10/32 +allowed_ip=192.168.4.11/32 +tx_bytes=1212111 +rx_bytes=1929999999 +protocol_version=1 +errno=0 + +` + +func Test_findPeerInfo(t *testing.T) { + tests := []struct { + name string + peerKey string + searchKeys []string + want map[string]string + wantErr bool + }{ + { + name: "single", + peerKey: "58402e695ba1772b1cc9309755f043251ea77fdcf10fbe63989ceb7e19321376", + searchKeys: []string{"tx_bytes"}, + want: map[string]string{ + "tx_bytes": "38333", + }, + wantErr: false, + }, + { + name: "multiple", + peerKey: "58402e695ba1772b1cc9309755f043251ea77fdcf10fbe63989ceb7e19321376", + searchKeys: []string{"tx_bytes", "rx_bytes"}, + want: map[string]string{ + "tx_bytes": "38333", + "rx_bytes": "2224", + }, + wantErr: false, + }, + { + name: "lastpeer", + peerKey: "662e14fd594556f522604703340351258903b64f35553763f19426ab2a515c58", + searchKeys: []string{"tx_bytes", "rx_bytes"}, + want: map[string]string{ + "tx_bytes": "1212111", + "rx_bytes": "1929999999", + }, + wantErr: false, + }, + { + name: "peer not found", + peerKey: "1111111111111111111111111111111111111111111111111111111111111111", + searchKeys: nil, + want: nil, + wantErr: true, + }, + { + name: "key not found", + peerKey: "662e14fd594556f522604703340351258903b64f35553763f19426ab2a515c58", + searchKeys: []string{"tx_bytes", "unknown_key"}, + want: map[string]string{ + "tx_bytes": "1212111", + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + res, err := hex.DecodeString(tt.peerKey) + require.NoError(t, err) + + key, err := wgtypes.NewKey(res) + require.NoError(t, err) + + got, err := findPeerInfo(ipcFixture, key.String(), tt.searchKeys) + assert.Equalf(t, tt.wantErr, err != nil, fmt.Sprintf("findPeerInfo(%v, %v, %v)", ipcFixture, key.String(), tt.searchKeys)) + assert.Equalf(t, tt.want, got, "findPeerInfo(%v, %v, %v)", ipcFixture, key.String(), tt.searchKeys) + }) + } +} diff --git a/management/client/client.go b/management/client/client.go index db7bd239b..166fd02b1 100644 --- a/management/client/client.go +++ b/management/client/client.go @@ -17,4 +17,5 @@ type Client interface { GetDeviceAuthorizationFlow(serverKey wgtypes.Key) (*proto.DeviceAuthorizationFlow, error) GetPKCEAuthorizationFlow(serverKey wgtypes.Key) (*proto.PKCEAuthorizationFlow, error) GetNetworkMap() (*proto.NetworkMap, error) + IsHealthy() bool } diff --git a/management/client/grpc.go b/management/client/grpc.go index ddb420ee2..e6c696403 100644 --- a/management/client/grpc.go +++ b/management/client/grpc.go @@ -28,7 +28,7 @@ import ( // ConnStateNotifier is a wrapper interface of the status recorders type ConnStateNotifier interface { - MarkManagementDisconnected() + MarkManagementDisconnected(error) MarkManagementConnected() } @@ -154,7 +154,7 @@ func (c *GrpcClient) Sync(msgHandler func(msg *proto.SyncResponse) error) error return nil default: backOff.Reset() // reset backoff counter after successful connection - c.notifyDisconnected() + c.notifyDisconnected(err) log.Warnf("disconnected from the Management service but will retry silently. Reason: %v", err) return err } @@ -283,6 +283,32 @@ func (c *GrpcClient) GetServerPublicKey() (*wgtypes.Key, error) { return &serverKey, nil } +// IsHealthy probes the gRPC connection and returns false on errors +func (c *GrpcClient) IsHealthy() bool { + switch c.conn.GetState() { + case connectivity.TransientFailure: + return false + case connectivity.Connecting: + return true + case connectivity.Shutdown: + return true + case connectivity.Idle: + case connectivity.Ready: + } + + ctx, cancel := context.WithTimeout(c.ctx, 1*time.Second) + defer cancel() + + _, err := c.realClient.GetServerKey(ctx, &proto.Empty{}) + if err != nil { + c.notifyDisconnected(err) + log.Warnf("health check returned: %s", err) + return false + } + c.notifyConnected() + return true +} + func (c *GrpcClient) login(serverKey wgtypes.Key, req *proto.LoginRequest) (*proto.LoginResponse, error) { if !c.ready() { return nil, fmt.Errorf("no connection to management") @@ -400,14 +426,14 @@ func (c *GrpcClient) GetPKCEAuthorizationFlow(serverKey wgtypes.Key) (*proto.PKC return flowInfoResp, nil } -func (c *GrpcClient) notifyDisconnected() { +func (c *GrpcClient) notifyDisconnected(err error) { c.connStateCallbackLock.RLock() defer c.connStateCallbackLock.RUnlock() if c.connStateCallback == nil { return } - c.connStateCallback.MarkManagementDisconnected() + c.connStateCallback.MarkManagementDisconnected(err) } func (c *GrpcClient) notifyConnected() { diff --git a/management/client/mock.go b/management/client/mock.go index 3f2e13cc7..042f837b8 100644 --- a/management/client/mock.go +++ b/management/client/mock.go @@ -1,9 +1,10 @@ package client import ( + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" + "github.com/netbirdio/netbird/client/system" "github.com/netbirdio/netbird/management/proto" - "golang.zx2c4.com/wireguard/wgctrl/wgtypes" ) type MockClient struct { @@ -16,6 +17,10 @@ type MockClient struct { GetPKCEAuthorizationFlowFunc func(serverKey wgtypes.Key) (*proto.PKCEAuthorizationFlow, error) } +func (m *MockClient) IsHealthy() bool { + return true +} + func (m *MockClient) Close() error { if m.CloseFunc == nil { return nil diff --git a/signal/client/client.go b/signal/client/client.go index 3c46cc96f..dc73b2ce5 100644 --- a/signal/client/client.go +++ b/signal/client/client.go @@ -35,6 +35,7 @@ type Client interface { GetStatus() Status Receive(msgHandler func(msg *proto.Message) error) error Ready() bool + IsHealthy() bool WaitStreamConnected() SendToStream(msg *proto.EncryptedMessage) error Send(msg *proto.Message) error diff --git a/signal/client/grpc.go b/signal/client/grpc.go index 7aa9f9ce9..07276aef1 100644 --- a/signal/client/grpc.go +++ b/signal/client/grpc.go @@ -28,7 +28,7 @@ const defaultSendTimeout = 5 * time.Second // ConnStateNotifier is a wrapper interface of the status recorder type ConnStateNotifier interface { - MarkSignalDisconnected() + MarkSignalDisconnected(error) MarkSignalConnected() } @@ -166,7 +166,7 @@ func (c *GrpcClient) Receive(msgHandler func(msg *proto.Message) error) error { // we need this reset because after a successful connection and a consequent error, backoff lib doesn't // reset times and next try will start with a long delay backOff.Reset() - c.notifyDisconnected() + c.notifyDisconnected(err) log.Warnf("disconnected from the Signal service but will retry silently. Reason: %v", err) return err } @@ -238,6 +238,35 @@ func (c *GrpcClient) Ready() bool { return c.signalConn.GetState() == connectivity.Ready || c.signalConn.GetState() == connectivity.Idle } +// IsHealthy probes the gRPC connection and returns false on errors +func (c *GrpcClient) IsHealthy() bool { + switch c.signalConn.GetState() { + case connectivity.TransientFailure: + return false + case connectivity.Connecting: + return true + case connectivity.Shutdown: + return true + case connectivity.Idle: + case connectivity.Ready: + } + + ctx, cancel := context.WithTimeout(c.ctx, 1*time.Second) + defer cancel() + _, err := c.realClient.Send(ctx, &proto.EncryptedMessage{ + Key: c.key.PublicKey().String(), + RemoteKey: "dummy", + Body: nil, + }) + if err != nil { + c.notifyDisconnected(err) + log.Warnf("health check returned: %s", err) + return false + } + c.notifyConnected() + return true +} + // WaitStreamConnected waits until the client is connected to the Signal stream func (c *GrpcClient) WaitStreamConnected() { @@ -383,14 +412,14 @@ func (c *GrpcClient) receive(stream proto.SignalExchange_ConnectStreamClient, } } -func (c *GrpcClient) notifyDisconnected() { +func (c *GrpcClient) notifyDisconnected(err error) { c.connStateCallbackLock.RLock() defer c.connStateCallbackLock.RUnlock() if c.connStateCallback == nil { return } - c.connStateCallback.MarkSignalDisconnected() + c.connStateCallback.MarkSignalDisconnected(err) } func (c *GrpcClient) notifyConnected() { diff --git a/signal/client/mock.go b/signal/client/mock.go index 21ec77cd6..a0ce13aed 100644 --- a/signal/client/mock.go +++ b/signal/client/mock.go @@ -15,6 +15,10 @@ type MockClient struct { SendFunc func(msg *proto.Message) error } +func (sm *MockClient) IsHealthy() bool { + return true +} + func (sm *MockClient) Close() error { if sm.CloseFunc == nil { return nil