Extend netbird status command to include health information (#1471)

* Adds management, signal, and relay (STUN/TURN) health probes to the status command.

* Adds a reason when the management or signal connections are disconnected.

* Adds last wireguard handshake and received/sent bytes per peer
This commit is contained in:
Viktor Liu 2024-01-22 12:20:24 +01:00 committed by GitHub
parent d4194cba6a
commit a7d6632298
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 1571 additions and 339 deletions

View File

@ -274,6 +274,8 @@ go test -exec sudo ./...
``` ```
> On Windows use a powershell with administrator privileges > 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 ## 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: 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 - Keep functions as simple as possible, with a single purpose

View File

@ -22,14 +22,18 @@ import (
) )
type peerStateDetailOutput struct { type peerStateDetailOutput struct {
FQDN string `json:"fqdn" yaml:"fqdn"` FQDN string `json:"fqdn" yaml:"fqdn"`
IP string `json:"netbirdIp" yaml:"netbirdIp"` IP string `json:"netbirdIp" yaml:"netbirdIp"`
PubKey string `json:"publicKey" yaml:"publicKey"` PubKey string `json:"publicKey" yaml:"publicKey"`
Status string `json:"status" yaml:"status"` Status string `json:"status" yaml:"status"`
LastStatusUpdate time.Time `json:"lastStatusUpdate" yaml:"lastStatusUpdate"` LastStatusUpdate time.Time `json:"lastStatusUpdate" yaml:"lastStatusUpdate"`
ConnType string `json:"connectionType" yaml:"connectionType"` ConnType string `json:"connectionType" yaml:"connectionType"`
Direct bool `json:"direct" yaml:"direct"` Direct bool `json:"direct" yaml:"direct"`
IceCandidateType iceCandidateType `json:"iceCandidateType" yaml:"iceCandidateType"` 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 { type peersStateOutput struct {
@ -41,11 +45,25 @@ type peersStateOutput struct {
type signalStateOutput struct { type signalStateOutput struct {
URL string `json:"url" yaml:"url"` URL string `json:"url" yaml:"url"`
Connected bool `json:"connected" yaml:"connected"` Connected bool `json:"connected" yaml:"connected"`
Error string `json:"error" yaml:"error"`
} }
type managementStateOutput struct { type managementStateOutput struct {
URL string `json:"url" yaml:"url"` URL string `json:"url" yaml:"url"`
Connected bool `json:"connected" yaml:"connected"` 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 { type iceCandidateType struct {
@ -59,6 +77,7 @@ type statusOutputOverview struct {
DaemonVersion string `json:"daemonVersion" yaml:"daemonVersion"` DaemonVersion string `json:"daemonVersion" yaml:"daemonVersion"`
ManagementState managementStateOutput `json:"management" yaml:"management"` ManagementState managementStateOutput `json:"management" yaml:"management"`
SignalState signalStateOutput `json:"signal" yaml:"signal"` SignalState signalStateOutput `json:"signal" yaml:"signal"`
Relays relayStateOutput `json:"relays" yaml:"relays"`
IP string `json:"netbirdIp" yaml:"netbirdIp"` IP string `json:"netbirdIp" yaml:"netbirdIp"`
PubKey string `json:"publicKey" yaml:"publicKey"` PubKey string `json:"publicKey" yaml:"publicKey"`
KernelInterface bool `json:"usesKernelInterface" yaml:"usesKernelInterface"` KernelInterface bool `json:"usesKernelInterface" yaml:"usesKernelInterface"`
@ -146,7 +165,7 @@ func statusFunc(cmd *cobra.Command, args []string) error {
case yamlFlag: case yamlFlag:
statusOutputString, err = parseToYAML(outputInformationHolder) statusOutputString, err = parseToYAML(outputInformationHolder)
default: default:
statusOutputString = parseGeneralSummary(outputInformationHolder, false) statusOutputString = parseGeneralSummary(outputInformationHolder, false, false)
} }
if err != nil { if err != nil {
@ -220,14 +239,17 @@ func convertToStatusOutputOverview(resp *proto.StatusResponse) statusOutputOverv
managementOverview := managementStateOutput{ managementOverview := managementStateOutput{
URL: managementState.GetURL(), URL: managementState.GetURL(),
Connected: managementState.GetConnected(), Connected: managementState.GetConnected(),
Error: managementState.Error,
} }
signalState := pbFullStatus.GetSignalState() signalState := pbFullStatus.GetSignalState()
signalOverview := signalStateOutput{ signalOverview := signalStateOutput{
URL: signalState.GetURL(), URL: signalState.GetURL(),
Connected: signalState.GetConnected(), Connected: signalState.GetConnected(),
Error: signalState.Error,
} }
relayOverview := mapRelays(pbFullStatus.GetRelays())
peersOverview := mapPeers(resp.GetFullStatus().GetPeers()) peersOverview := mapPeers(resp.GetFullStatus().GetPeers())
overview := statusOutputOverview{ overview := statusOutputOverview{
@ -236,6 +258,7 @@ func convertToStatusOutputOverview(resp *proto.StatusResponse) statusOutputOverv
DaemonVersion: resp.GetDaemonVersion(), DaemonVersion: resp.GetDaemonVersion(),
ManagementState: managementOverview, ManagementState: managementOverview,
SignalState: signalOverview, SignalState: signalOverview,
Relays: relayOverview,
IP: pbFullStatus.GetLocalPeerState().GetIP(), IP: pbFullStatus.GetLocalPeerState().GetIP(),
PubKey: pbFullStatus.GetLocalPeerState().GetPubKey(), PubKey: pbFullStatus.GetLocalPeerState().GetPubKey(),
KernelInterface: pbFullStatus.GetLocalPeerState().GetKernelInterface(), KernelInterface: pbFullStatus.GetLocalPeerState().GetKernelInterface(),
@ -245,12 +268,43 @@ func convertToStatusOutputOverview(resp *proto.StatusResponse) statusOutputOverv
return overview return overview
} }
func mapRelays(relays []*proto.RelayState) relayStateOutput {
var relayStateDetail []relayStateOutputDetail
var relaysAvailable int
for _, relay := range relays {
available := relay.GetAvailable()
relayStateDetail = append(relayStateDetail,
relayStateOutputDetail{
URI: relay.URI,
Available: available,
Error: relay.GetError(),
},
)
if available {
relaysAvailable++
}
}
return relayStateOutput{
Total: len(relays),
Available: relaysAvailable,
Details: relayStateDetail,
}
}
func mapPeers(peers []*proto.PeerState) peersStateOutput { func mapPeers(peers []*proto.PeerState) peersStateOutput {
var peersStateDetail []peerStateDetailOutput var peersStateDetail []peerStateDetailOutput
localICE := "" localICE := ""
remoteICE := "" remoteICE := ""
localICEEndpoint := ""
remoteICEEndpoint := ""
connType := "" connType := ""
peersConnected := 0 peersConnected := 0
lastHandshake := time.Time{}
transferReceived := int64(0)
transferSent := int64(0)
for _, pbPeerState := range peers { for _, pbPeerState := range peers {
isPeerConnected := pbPeerState.ConnStatus == peer.StatusConnected.String() isPeerConnected := pbPeerState.ConnStatus == peer.StatusConnected.String()
if skipDetailByFilters(pbPeerState, isPeerConnected) { if skipDetailByFilters(pbPeerState, isPeerConnected) {
@ -261,10 +315,15 @@ func mapPeers(peers []*proto.PeerState) peersStateOutput {
localICE = pbPeerState.GetLocalIceCandidateType() localICE = pbPeerState.GetLocalIceCandidateType()
remoteICE = pbPeerState.GetRemoteIceCandidateType() remoteICE = pbPeerState.GetRemoteIceCandidateType()
localICEEndpoint = pbPeerState.GetLocalIceCandidateEndpoint()
remoteICEEndpoint = pbPeerState.GetRemoteIceCandidateEndpoint()
connType = "P2P" connType = "P2P"
if pbPeerState.Relayed { if pbPeerState.Relayed {
connType = "Relayed" connType = "Relayed"
} }
lastHandshake = pbPeerState.GetLastWireguardHandshake().AsTime().Local()
transferReceived = pbPeerState.GetBytesRx()
transferSent = pbPeerState.GetBytesTx()
} }
timeLocal := pbPeerState.GetConnStatusUpdate().AsTime().Local() timeLocal := pbPeerState.GetConnStatusUpdate().AsTime().Local()
@ -279,7 +338,14 @@ func mapPeers(peers []*proto.PeerState) peersStateOutput {
Local: localICE, Local: localICE,
Remote: remoteICE, Remote: remoteICE,
}, },
FQDN: pbPeerState.GetFqdn(), IceCandidateEndpoint: iceCandidateType{
Local: localICEEndpoint,
Remote: remoteICEEndpoint,
},
FQDN: pbPeerState.GetFqdn(),
LastWireguardHandshake: lastHandshake,
TransferReceived: transferReceived,
TransferSent: transferSent,
} }
peersStateDetail = append(peersStateDetail, peerState) peersStateDetail = append(peersStateDetail, peerState)
@ -329,22 +395,32 @@ func parseToYAML(overview statusOutputOverview) (string, error) {
return string(yamlBytes), nil 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 { if overview.ManagementState.Connected {
managementConnString = "Connected" managementConnString = "Connected"
if showURL { if showURL {
managementConnString = fmt.Sprintf("%s to %s", managementConnString, overview.ManagementState.URL) 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 { if overview.SignalState.Connected {
signalConnString = "Connected" signalConnString = "Connected"
if showURL { if showURL {
signalConnString = fmt.Sprintf("%s to %s", signalConnString, overview.SignalState.URL) 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" interfaceTypeString := "Userspace"
@ -356,6 +432,23 @@ func parseGeneralSummary(overview statusOutputOverview, showURL bool) string {
interfaceIP = "N/A" 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) peersCountString := fmt.Sprintf("%d/%d Connected", overview.Peers.Connected, overview.Peers.Total)
summary := fmt.Sprintf( summary := fmt.Sprintf(
@ -363,6 +456,7 @@ func parseGeneralSummary(overview statusOutputOverview, showURL bool) string {
"CLI version: %s\n"+ "CLI version: %s\n"+
"Management: %s\n"+ "Management: %s\n"+
"Signal: %s\n"+ "Signal: %s\n"+
"Relays: %s\n"+
"FQDN: %s\n"+ "FQDN: %s\n"+
"NetBird IP: %s\n"+ "NetBird IP: %s\n"+
"Interface type: %s\n"+ "Interface type: %s\n"+
@ -371,6 +465,7 @@ func parseGeneralSummary(overview statusOutputOverview, showURL bool) string {
version.NetbirdVersion(), version.NetbirdVersion(),
managementConnString, managementConnString,
signalConnString, signalConnString,
relayAvailableString,
overview.FQDN, overview.FQDN,
interfaceIP, interfaceIP,
interfaceTypeString, interfaceTypeString,
@ -381,7 +476,7 @@ func parseGeneralSummary(overview statusOutputOverview, showURL bool) string {
func parseToFullDetailSummary(overview statusOutputOverview) string { func parseToFullDetailSummary(overview statusOutputOverview) string {
parsedPeersString := parsePeers(overview.Peers) parsedPeersString := parsePeers(overview.Peers)
summary := parseGeneralSummary(overview, true) summary := parseGeneralSummary(overview, true, true)
return fmt.Sprintf( return fmt.Sprintf(
"Peers detail:"+ "Peers detail:"+
@ -409,6 +504,25 @@ func parsePeers(peers peersStateOutput) string {
remoteICE = peerState.IceCandidateType.Remote remoteICE = peerState.IceCandidateType.Remote
} }
localICEEndpoint := "-"
if peerState.IceCandidateEndpoint.Local != "" {
localICEEndpoint = peerState.IceCandidateEndpoint.Local
}
remoteICEEndpoint := "-"
if peerState.IceCandidateEndpoint.Remote != "" {
remoteICEEndpoint = peerState.IceCandidateEndpoint.Remote
}
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( peerString := fmt.Sprintf(
"\n %s:\n"+ "\n %s:\n"+
" NetBird IP: %s\n"+ " NetBird IP: %s\n"+
@ -418,7 +532,10 @@ func parsePeers(peers peersStateOutput) string {
" Connection type: %s\n"+ " Connection type: %s\n"+
" Direct: %t\n"+ " Direct: %t\n"+
" ICE candidate (Local/Remote): %s/%s\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.FQDN,
peerState.IP, peerState.IP,
peerState.PubKey, peerState.PubKey,
@ -427,7 +544,12 @@ func parsePeers(peers peersStateOutput) string {
peerState.Direct, peerState.Direct,
localICE, localICE,
remoteICE, remoteICE,
peerState.LastStatusUpdate.Format("2006-01-02 15:04:05"), localICEEndpoint,
remoteICEEndpoint,
lastStatusUpdate,
lastWireguardHandshake,
toIEC(peerState.TransferReceived),
toIEC(peerState.TransferSent),
) )
peersString += peerString peersString += peerString
@ -467,3 +589,17 @@ func skipDetailByFilters(peerState *proto.PeerState, isConnected bool) bool {
return statusEval || ipEval || nameEval 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])
}

View File

@ -1,10 +1,13 @@
package cmd package cmd
import ( import (
"bytes"
"encoding/json"
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb" "google.golang.org/protobuf/types/known/timestamppb"
"github.com/netbirdio/netbird/client/proto" "github.com/netbirdio/netbird/client/proto"
@ -25,35 +28,59 @@ var resp = &proto.StatusResponse{
FullStatus: &proto.FullStatus{ FullStatus: &proto.FullStatus{
Peers: []*proto.PeerState{ Peers: []*proto.PeerState{
{ {
IP: "192.168.178.101", IP: "192.168.178.101",
PubKey: "Pubkey1", PubKey: "Pubkey1",
Fqdn: "peer-1.awesome-domain.com", Fqdn: "peer-1.awesome-domain.com",
ConnStatus: "Connected", ConnStatus: "Connected",
ConnStatusUpdate: timestamppb.New(time.Date(2001, time.Month(1), 1, 1, 1, 1, 0, time.UTC)), ConnStatusUpdate: timestamppb.New(time.Date(2001, time.Month(1), 1, 1, 1, 1, 0, time.UTC)),
Relayed: false, Relayed: false,
Direct: true, Direct: true,
LocalIceCandidateType: "", LocalIceCandidateType: "",
RemoteIceCandidateType: "", 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", IP: "192.168.178.102",
PubKey: "Pubkey2", PubKey: "Pubkey2",
Fqdn: "peer-2.awesome-domain.com", Fqdn: "peer-2.awesome-domain.com",
ConnStatus: "Connected", ConnStatus: "Connected",
ConnStatusUpdate: timestamppb.New(time.Date(2002, time.Month(2), 2, 2, 2, 2, 0, time.UTC)), ConnStatusUpdate: timestamppb.New(time.Date(2002, time.Month(2), 2, 2, 2, 2, 0, time.UTC)),
Relayed: true, Relayed: true,
Direct: false, Direct: false,
LocalIceCandidateType: "relay", LocalIceCandidateType: "relay",
RemoteIceCandidateType: "prflx", 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{ ManagementState: &proto.ManagementState{
URL: "my-awesome-management.com:443", URL: "my-awesome-management.com:443",
Connected: true, Connected: true,
Error: "",
}, },
SignalState: &proto.SignalState{ SignalState: &proto.SignalState{
URL: "my-awesome-signal.com:443", URL: "my-awesome-signal.com:443",
Connected: true, 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{ LocalPeerState: &proto.LocalPeerState{
IP: "192.168.178.100/16", IP: "192.168.178.100/16",
@ -82,6 +109,13 @@ var overview = statusOutputOverview{
Local: "", Local: "",
Remote: "", 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", IP: "192.168.178.102",
@ -95,6 +129,13 @@ var overview = statusOutputOverview{
Local: "relay", Local: "relay",
Remote: "prflx", 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{ ManagementState: managementStateOutput{
URL: "my-awesome-management.com:443", URL: "my-awesome-management.com:443",
Connected: true, Connected: true,
Error: "",
}, },
SignalState: signalStateOutput{ SignalState: signalStateOutput{
URL: "my-awesome-signal.com:443", URL: "my-awesome-signal.com:443",
Connected: true, 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", IP: "192.168.178.100/16",
PubKey: "Some-Pub-Key", PubKey: "Some-Pub-Key",
@ -145,107 +204,163 @@ func TestSortingOfPeers(t *testing.T) {
} }
func TestParsingToJSON(t *testing.T) { func TestParsingToJSON(t *testing.T) {
json, _ := parseToJSON(overview) jsonString, _ := parseToJSON(overview)
//@formatter:off //@formatter:off
expectedJSON := "{\"" + expectedJSONString := `
"peers\":" + {
"{" + "peers": {
"\"total\":2," + "total": 2,
"\"connected\":2," + "connected": 2,
"\"details\":" + "details": [
"[" + {
"{" + "fqdn": "peer-1.awesome-domain.com",
"\"fqdn\":\"peer-1.awesome-domain.com\"," + "netbirdIp": "192.168.178.101",
"\"netbirdIp\":\"192.168.178.101\"," + "publicKey": "Pubkey1",
"\"publicKey\":\"Pubkey1\"," + "status": "Connected",
"\"status\":\"Connected\"," + "lastStatusUpdate": "2001-01-01T01:01:01Z",
"\"lastStatusUpdate\":\"2001-01-01T01:01:01Z\"," + "connectionType": "P2P",
"\"connectionType\":\"P2P\"," + "direct": true,
"\"direct\":true," + "iceCandidateType": {
"\"iceCandidateType\":" + "local": "",
"{" + "remote": ""
"\"local\":\"\"," + },
"\"remote\":\"\"" + "iceCandidateEndpoint": {
"}" + "local": "",
"}," + "remote": ""
"{" + },
"\"fqdn\":\"peer-2.awesome-domain.com\"," + "lastWireguardHandshake": "2001-01-01T01:01:02Z",
"\"netbirdIp\":\"192.168.178.102\"," + "transferReceived": 200,
"\"publicKey\":\"Pubkey2\"," + "transferSent": 100
"\"status\":\"Connected\"," + },
"\"lastStatusUpdate\":\"2002-02-02T02:02:02Z\"," + {
"\"connectionType\":\"Relayed\"," + "fqdn": "peer-2.awesome-domain.com",
"\"direct\":false," + "netbirdIp": "192.168.178.102",
"\"iceCandidateType\":" + "publicKey": "Pubkey2",
"{" + "status": "Connected",
"\"local\":\"relay\"," + "lastStatusUpdate": "2002-02-02T02:02:02Z",
"\"remote\":\"prflx\"" + "connectionType": "Relayed",
"}" + "direct": false,
"}" + "iceCandidateType": {
"]" + "local": "relay",
"}," + "remote": "prflx"
"\"cliVersion\":\"development\"," + },
"\"daemonVersion\":\"0.14.1\"," + "iceCandidateEndpoint": {
"\"management\":" + "local": "10.0.0.1:10001",
"{" + "remote": "10.0.10.1:10002"
"\"url\":\"my-awesome-management.com:443\"," + },
"\"connected\":true" + "lastWireguardHandshake": "2002-02-02T02:02:03Z",
"}," + "transferReceived": 2000,
"\"signal\":" + "transferSent": 1000
"{\"" + }
"url\":\"my-awesome-signal.com:443\"," + ]
"\"connected\":true" + },
"}," + "cliVersion": "development",
"\"netbirdIp\":\"192.168.178.100/16\"," + "daemonVersion": "0.14.1",
"\"publicKey\":\"Some-Pub-Key\"," + "management": {
"\"usesKernelInterface\":true," + "url": "my-awesome-management.com:443",
"\"fqdn\":\"some-localhost.awesome-domain.com\"" + "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 // @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) { func TestParsingToYAML(t *testing.T) {
yaml, _ := parseToYAML(overview) yaml, _ := parseToYAML(overview)
expectedYAML := "peers:\n" + expectedYAML :=
" total: 2\n" + `peers:
" connected: 2\n" + total: 2
" details:\n" + connected: 2
" - fqdn: peer-1.awesome-domain.com\n" + details:
" netbirdIp: 192.168.178.101\n" + - fqdn: peer-1.awesome-domain.com
" publicKey: Pubkey1\n" + netbirdIp: 192.168.178.101
" status: Connected\n" + publicKey: Pubkey1
" lastStatusUpdate: 2001-01-01T01:01:01Z\n" + status: Connected
" connectionType: P2P\n" + lastStatusUpdate: 2001-01-01T01:01:01Z
" direct: true\n" + connectionType: P2P
" iceCandidateType:\n" + direct: true
" local: \"\"\n" + iceCandidateType:
" remote: \"\"\n" + local: ""
" - fqdn: peer-2.awesome-domain.com\n" + remote: ""
" netbirdIp: 192.168.178.102\n" + iceCandidateEndpoint:
" publicKey: Pubkey2\n" + local: ""
" status: Connected\n" + remote: ""
" lastStatusUpdate: 2002-02-02T02:02:02Z\n" + lastWireguardHandshake: 2001-01-01T01:01:02Z
" connectionType: Relayed\n" + transferReceived: 200
" direct: false\n" + transferSent: 100
" iceCandidateType:\n" + - fqdn: peer-2.awesome-domain.com
" local: relay\n" + netbirdIp: 192.168.178.102
" remote: prflx\n" + publicKey: Pubkey2
"cliVersion: development\n" + status: Connected
"daemonVersion: 0.14.1\n" + lastStatusUpdate: 2002-02-02T02:02:02Z
"management:\n" + connectionType: Relayed
" url: my-awesome-management.com:443\n" + direct: false
" connected: true\n" + iceCandidateType:
"signal:\n" + local: relay
" url: my-awesome-signal.com:443\n" + remote: prflx
" connected: true\n" + iceCandidateEndpoint:
"netbirdIp: 192.168.178.100/16\n" + local: 10.0.0.1:10001
"publicKey: Some-Pub-Key\n" + remote: 10.0.10.1:10002
"usesKernelInterface: true\n" + lastWireguardHandshake: 2002-02-02T02:02:03Z
"fqdn: some-localhost.awesome-domain.com\n" 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) assert.Equal(t, expectedYAML, yaml)
} }
@ -253,50 +368,64 @@ func TestParsingToYAML(t *testing.T) {
func TestParsingToDetail(t *testing.T) { func TestParsingToDetail(t *testing.T) {
detail := parseToFullDetailSummary(overview) detail := parseToFullDetailSummary(overview)
expectedDetail := "Peers detail:\n" + expectedDetail :=
" peer-1.awesome-domain.com:\n" + `Peers detail:
" NetBird IP: 192.168.178.101\n" + peer-1.awesome-domain.com:
" Public key: Pubkey1\n" + NetBird IP: 192.168.178.101
" Status: Connected\n" + Public key: Pubkey1
" -- detail --\n" + Status: Connected
" Connection type: P2P\n" + -- detail --
" Direct: true\n" + Connection type: P2P
" ICE candidate (Local/Remote): -/-\n" + Direct: true
" Last connection update: 2001-01-01 01:01:01\n" + ICE candidate (Local/Remote): -/-
"\n" + ICE candidate endpoints (Local/Remote): -/-
" peer-2.awesome-domain.com:\n" + Last connection update: 2001-01-01 01:01:01
" NetBird IP: 192.168.178.102\n" + Last Wireguard handshake: 2001-01-01 01:01:02
" Public key: Pubkey2\n" + Transfer status (received/sent) 200 B/100 B
" Status: Connected\n" +
" -- detail --\n" + peer-2.awesome-domain.com:
" Connection type: Relayed\n" + NetBird IP: 192.168.178.102
" Direct: false\n" + Public key: Pubkey2
" ICE candidate (Local/Remote): relay/prflx\n" + Status: Connected
" Last connection update: 2002-02-02 02:02:02\n" + -- detail --
"\n" + Connection type: Relayed
"Daemon version: 0.14.1\n" + Direct: false
"CLI version: development\n" + ICE candidate (Local/Remote): relay/prflx
"Management: Connected to my-awesome-management.com:443\n" + ICE candidate endpoints (Local/Remote): 10.0.0.1:10001/10.0.10.1:10002
"Signal: Connected to my-awesome-signal.com:443\n" + Last connection update: 2002-02-02 02:02:02
"FQDN: some-localhost.awesome-domain.com\n" + Last Wireguard handshake: 2002-02-02 02:02:03
"NetBird IP: 192.168.178.100/16\n" + Transfer status (received/sent) 2.0 KiB/1000 B
"Interface type: Kernel\n" +
"Peers count: 2/2 Connected\n" 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) assert.Equal(t, expectedDetail, detail)
} }
func TestParsingToShortVersion(t *testing.T) { func TestParsingToShortVersion(t *testing.T) {
shortVersion := parseGeneralSummary(overview, false) shortVersion := parseGeneralSummary(overview, false, false)
expectedString := "Daemon version: 0.14.1\n" + expectedString :=
"CLI version: development\n" + `Daemon version: 0.14.1
"Management: Connected\n" + CLI version: development
"Signal: Connected\n" + Management: Connected
"FQDN: some-localhost.awesome-domain.com\n" + Signal: Connected
"NetBird IP: 192.168.178.100/16\n" + Relays: 1/2 Available
"Interface type: Kernel\n" + FQDN: some-localhost.awesome-domain.com
"Peers count: 2/2 Connected\n" NetBird IP: 192.168.178.100/16
Interface type: Kernel
Peers count: 2/2 Connected
`
assert.Equal(t, expectedString, shortVersion) assert.Equal(t, expectedString, shortVersion)
} }

View File

@ -27,11 +27,33 @@ import (
// RunClient with main logic. // RunClient with main logic.
func RunClient(ctx context.Context, config *Config, statusRecorder *peer.Status) error { 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 // 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 // in case of non Android os these variables will be nil
mobileDependency := MobileDependency{ mobileDependency := MobileDependency{
TunAdapter: tunAdapter, TunAdapter: tunAdapter,
@ -40,19 +62,35 @@ func RunClientMobile(ctx context.Context, config *Config, statusRecorder *peer.S
HostDNSAddresses: dnsAddresses, HostDNSAddresses: dnsAddresses,
DnsReadyListener: dnsReadyListener, 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{ mobileDependency := MobileDependency{
FileDescriptor: fileDescriptor, FileDescriptor: fileDescriptor,
NetworkChangeListener: networkChangeListener, NetworkChangeListener: networkChangeListener,
DnsManager: dnsManager, 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()) log.Infof("starting NetBird client version %s", version.NetbirdVersion())
backOff := &backoff.ExponentialBackOff{ backOff := &backoff.ExponentialBackOff{
@ -103,7 +141,7 @@ func runClient(ctx context.Context, config *Config, statusRecorder *peer.Status,
engineCtx, cancel := context.WithCancel(ctx) engineCtx, cancel := context.WithCancel(ctx)
defer func() { defer func() {
statusRecorder.MarkManagementDisconnected() statusRecorder.MarkManagementDisconnected(state.err)
statusRecorder.CleanLocalPeerState() statusRecorder.CleanLocalPeerState()
cancel() cancel()
}() }()
@ -152,8 +190,10 @@ func runClient(ctx context.Context, config *Config, statusRecorder *peer.Status,
statusRecorder.UpdateSignalAddress(signalURL) statusRecorder.UpdateSignalAddress(signalURL)
statusRecorder.MarkSignalDisconnected() statusRecorder.MarkSignalDisconnected(nil)
defer statusRecorder.MarkSignalDisconnected() defer func() {
statusRecorder.MarkSignalDisconnected(state.err)
}()
// with the global Wiretrustee config in hand connect (just a connection, no stream yet) Signal // with the global Wiretrustee config in hand connect (just a connection, no stream yet) Signal
signalClient, err := connectToSignal(engineCtx, loginResp.GetWiretrusteeConfig(), myPrivateKey) signalClient, err := connectToSignal(engineCtx, loginResp.GetWiretrusteeConfig(), myPrivateKey)
@ -181,7 +221,7 @@ func runClient(ctx context.Context, config *Config, statusRecorder *peer.Status,
return wrapErr(err) 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() err = engine.Start()
if err != nil { if err != nil {
log.Errorf("error while starting Netbird Connection Engine: %s", err) log.Errorf("error while starting Netbird Connection Engine: %s", err)

View File

@ -59,6 +59,10 @@ func (w *mocWGIface) SetFilter(filter iface.PacketFilter) error {
return nil return nil
} }
func (w *mocWGIface) GetStats(_ string) (iface.WGStats, error) {
return iface.WGStats{}, nil
}
var zoneRecords = []nbdns.SimpleRecord{ var zoneRecords = []nbdns.SimpleRecord{
{ {
Name: "peera.netbird.cloud", Name: "peera.netbird.cloud",

View File

@ -11,4 +11,5 @@ type WGIface interface {
IsUserspaceBind() bool IsUserspaceBind() bool
GetFilter() iface.PacketFilter GetFilter() iface.PacketFilter
GetDevice() *iface.DeviceWrapper GetDevice() *iface.DeviceWrapper
GetStats(peerKey string) (iface.WGStats, error)
} }

View File

@ -9,5 +9,6 @@ type WGIface interface {
IsUserspaceBind() bool IsUserspaceBind() bool
GetFilter() iface.PacketFilter GetFilter() iface.PacketFilter
GetDevice() *iface.DeviceWrapper GetDevice() *iface.DeviceWrapper
GetStats(peerKey string) (iface.WGStats, error)
GetInterfaceGUIDString() (string, error) GetInterfaceGUIDString() (string, error)
} }

View File

@ -22,6 +22,7 @@ import (
"github.com/netbirdio/netbird/client/internal/acl" "github.com/netbirdio/netbird/client/internal/acl"
"github.com/netbirdio/netbird/client/internal/dns" "github.com/netbirdio/netbird/client/internal/dns"
"github.com/netbirdio/netbird/client/internal/peer" "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/rosenpass"
"github.com/netbirdio/netbird/client/internal/routemanager" "github.com/netbirdio/netbird/client/internal/routemanager"
"github.com/netbirdio/netbird/client/internal/wgproxy" "github.com/netbirdio/netbird/client/internal/wgproxy"
@ -125,6 +126,11 @@ type Engine struct {
acl acl.Manager acl acl.Manager
dnsServer dns.Server dnsServer dns.Server
mgmProbe *Probe
signalProbe *Probe
relayProbe *Probe
wgProbe *Probe
} }
// Peer is an instance of the Connection Peer // Peer is an instance of the Connection Peer
@ -135,11 +141,43 @@ type Peer struct {
// NewEngine creates a new Connection Engine // NewEngine creates a new Connection Engine
func NewEngine( func NewEngine(
ctx context.Context, cancel context.CancelFunc, ctx context.Context,
signalClient signal.Client, mgmClient mgm.Client, cancel context.CancelFunc,
config *EngineConfig, mobileDep MobileDependency, statusRecorder *peer.Status, signalClient signal.Client,
mgmClient mgm.Client,
config *EngineConfig,
mobileDep MobileDependency,
statusRecorder *peer.Status,
) *Engine { ) *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{ return &Engine{
ctx: ctx, ctx: ctx,
cancel: cancel, cancel: cancel,
@ -155,6 +193,10 @@ func NewEngine(
sshServerFunc: nbssh.DefaultSSHServer, sshServerFunc: nbssh.DefaultSSHServer,
statusRecorder: statusRecorder, statusRecorder: statusRecorder,
wgProxyFactory: wgproxy.NewFactory(config.WgPort), 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.receiveSignalEvents()
e.receiveManagementEvents() e.receiveManagementEvents()
e.receiveProbeEvents()
return nil 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. // E.g. when a new peer has been registered and we are allowed to connect to it.
func (e *Engine) receiveManagementEvents() { func (e *Engine) receiveManagementEvents() {
go func() { go func() {
err := e.mgmClient.Sync(func(update *mgmProto.SyncResponse) error { err := e.mgmClient.Sync(e.handleSync)
return e.handleSync(update)
})
if err != nil { if err != nil {
// happens if management is unavailable for a long time. // happens if management is unavailable for a long time.
// We want to cancel the operation of the whole client // We want to cancel the operation of the whole client
@ -1175,3 +1216,69 @@ func (e *Engine) getRosenpassAddr() string {
} }
return "" 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)
}

View File

@ -408,12 +408,14 @@ func (conn *Conn) configureConnection(remoteConn net.Conn, remoteWgPort int, rem
conn.status = StatusConnected conn.status = StatusConnected
peerState := State{ peerState := State{
PubKey: conn.config.Key, PubKey: conn.config.Key,
ConnStatus: conn.status, ConnStatus: conn.status,
ConnStatusUpdate: time.Now(), ConnStatusUpdate: time.Now(),
LocalIceCandidateType: pair.Local.Type().String(), LocalIceCandidateType: pair.Local.Type().String(),
RemoteIceCandidateType: pair.Remote.Type().String(), RemoteIceCandidateType: pair.Remote.Type().String(),
Direct: !isRelayCandidate(pair.Local), 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 { if pair.Local.Type() == ice.CandidateTypeRelay || pair.Remote.Type() == ice.CandidateTypeRelay {
peerState.Relayed = true peerState.Relayed = true
@ -500,6 +502,9 @@ func (conn *Conn) cleanup() error {
// todo rethink status updates // todo rethink status updates
log.Debugf("error while updating peer's %s state, err: %v", conn.config.Key, err) 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) log.Debugf("cleaned up connection to peer %s", conn.config.Key)
if err1 != nil { if err1 != nil {

View File

@ -4,19 +4,27 @@ import (
"errors" "errors"
"sync" "sync"
"time" "time"
"github.com/netbirdio/netbird/client/internal/relay"
"github.com/netbirdio/netbird/iface"
) )
// State contains the latest state of a peer // State contains the latest state of a peer
type State struct { type State struct {
IP string IP string
PubKey string PubKey string
FQDN string FQDN string
ConnStatus ConnStatus ConnStatus ConnStatus
ConnStatusUpdate time.Time ConnStatusUpdate time.Time
Relayed bool Relayed bool
Direct bool Direct bool
LocalIceCandidateType string LocalIceCandidateType string
RemoteIceCandidateType string RemoteIceCandidateType string
LocalIceCandidateEndpoint string
RemoteIceCandidateEndpoint string
LastWireguardHandshake time.Time
BytesTx int64
BytesRx int64
} }
// LocalPeerState contains the latest state of the local peer // LocalPeerState contains the latest state of the local peer
@ -31,12 +39,14 @@ type LocalPeerState struct {
type SignalState struct { type SignalState struct {
URL string URL string
Connected bool Connected bool
Error error
} }
// ManagementState contains the latest state of a management connection // ManagementState contains the latest state of a management connection
type ManagementState struct { type ManagementState struct {
URL string URL string
Connected bool Connected bool
Error error
} }
// FullStatus contains the full state held by the Status instance // FullStatus contains the full state held by the Status instance
@ -45,15 +55,19 @@ type FullStatus struct {
ManagementState ManagementState ManagementState ManagementState
SignalState SignalState SignalState SignalState
LocalPeerState LocalPeerState 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 { type Status struct {
mux sync.Mutex mux sync.Mutex
peers map[string]State peers map[string]State
changeNotify map[string]chan struct{} changeNotify map[string]chan struct{}
signalState bool signalState bool
signalError error
managementState bool managementState bool
managementError error
relayStates []relay.ProbeResult
localPeer LocalPeerState localPeer LocalPeerState
offlinePeers []State offlinePeers []State
mgmAddress string mgmAddress string
@ -156,6 +170,8 @@ func (d *Status) UpdatePeerState(receivedState State) error {
peerState.Relayed = receivedState.Relayed peerState.Relayed = receivedState.Relayed
peerState.LocalIceCandidateType = receivedState.LocalIceCandidateType peerState.LocalIceCandidateType = receivedState.LocalIceCandidateType
peerState.RemoteIceCandidateType = receivedState.RemoteIceCandidateType peerState.RemoteIceCandidateType = receivedState.RemoteIceCandidateType
peerState.LocalIceCandidateEndpoint = receivedState.LocalIceCandidateEndpoint
peerState.RemoteIceCandidateEndpoint = receivedState.RemoteIceCandidateEndpoint
} }
d.peers[receivedState.PubKey] = peerState d.peers[receivedState.PubKey] = peerState
@ -174,6 +190,25 @@ func (d *Status) UpdatePeerState(receivedState State) error {
return nil 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 { func shouldSkipNotify(received, curr State) bool {
switch { switch {
case received.ConnStatus == StatusConnecting: case received.ConnStatus == StatusConnecting:
@ -248,12 +283,13 @@ func (d *Status) CleanLocalPeerState() {
} }
// MarkManagementDisconnected sets ManagementState to disconnected // MarkManagementDisconnected sets ManagementState to disconnected
func (d *Status) MarkManagementDisconnected() { func (d *Status) MarkManagementDisconnected(err error) {
d.mux.Lock() d.mux.Lock()
defer d.mux.Unlock() defer d.mux.Unlock()
defer d.onConnectionChanged() defer d.onConnectionChanged()
d.managementState = false d.managementState = false
d.managementError = err
} }
// MarkManagementConnected sets ManagementState to connected // MarkManagementConnected sets ManagementState to connected
@ -263,6 +299,7 @@ func (d *Status) MarkManagementConnected() {
defer d.onConnectionChanged() defer d.onConnectionChanged()
d.managementState = true d.managementState = true
d.managementError = nil
} }
// UpdateSignalAddress update the address of the signal server // UpdateSignalAddress update the address of the signal server
@ -280,12 +317,13 @@ func (d *Status) UpdateManagementAddress(mgmAddress string) {
} }
// MarkSignalDisconnected sets SignalState to disconnected // MarkSignalDisconnected sets SignalState to disconnected
func (d *Status) MarkSignalDisconnected() { func (d *Status) MarkSignalDisconnected(err error) {
d.mux.Lock() d.mux.Lock()
defer d.mux.Unlock() defer d.mux.Unlock()
defer d.onConnectionChanged() defer d.onConnectionChanged()
d.signalState = false d.signalState = false
d.signalError = err
} }
// MarkSignalConnected sets SignalState to connected // MarkSignalConnected sets SignalState to connected
@ -295,6 +333,33 @@ func (d *Status) MarkSignalConnected() {
defer d.onConnectionChanged() defer d.onConnectionChanged()
d.signalState = true 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 // GetFullStatus gets full status
@ -303,15 +368,10 @@ func (d *Status) GetFullStatus() FullStatus {
defer d.mux.Unlock() defer d.mux.Unlock()
fullStatus := FullStatus{ fullStatus := FullStatus{
ManagementState: ManagementState{ ManagementState: d.GetManagementState(),
d.mgmAddress, SignalState: d.GetSignalState(),
d.managementState, LocalPeerState: d.localPeer,
}, Relays: d.GetRelayStates(),
SignalState: SignalState{
d.signalAddress,
d.signalState,
},
LocalPeerState: d.localPeer,
} }
for _, status := range d.peers { for _, status := range d.peers {

View File

@ -1,6 +1,7 @@
package peer package peer
import ( import (
"errors"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -152,9 +153,10 @@ func TestUpdateSignalState(t *testing.T) {
name string name string
connected bool connected bool
want bool want bool
err error
}{ }{
{"should mark as connected", true, true}, {"should mark as connected", true, true, nil},
{"should mark as disconnected", false, false}, {"should mark as disconnected", false, false, errors.New("test")},
} }
status := NewRecorder("https://mgm") status := NewRecorder("https://mgm")
@ -165,9 +167,10 @@ func TestUpdateSignalState(t *testing.T) {
if test.connected { if test.connected {
status.MarkSignalConnected() status.MarkSignalConnected()
} else { } else {
status.MarkSignalDisconnected() status.MarkSignalDisconnected(test.err)
} }
assert.Equal(t, test.want, status.signalState, "signal status should be equal") 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 name string
connected bool connected bool
want bool want bool
err error
}{ }{
{"should mark as connected", true, true}, {"should mark as connected", true, true, nil},
{"should mark as disconnected", false, false}, {"should mark as disconnected", false, false, errors.New("test")},
} }
status := NewRecorder(url) status := NewRecorder(url)
@ -190,9 +194,10 @@ func TestUpdateManagementState(t *testing.T) {
if test.connected { if test.connected {
status.MarkManagementConnected() status.MarkManagementConnected()
} else { } else {
status.MarkManagementDisconnected() status.MarkManagementDisconnected(test.err)
} }
assert.Equal(t, test.want, status.managementState, "signalState status should be equal") assert.Equal(t, test.want, status.managementState, "signalState status should be equal")
assert.Equal(t, test.err, status.managementError)
}) })
} }
} }

51
client/internal/probe.go Normal file
View File

@ -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()
}
}
}

View File

@ -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
}

View File

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.26.0 // protoc-gen-go v1.26.0
// protoc v4.24.3 // protoc v3.21.9
// source: daemon.proto // source: daemon.proto
package proto package proto
@ -733,15 +733,20 @@ type PeerState struct {
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
IP string `protobuf:"bytes,1,opt,name=IP,proto3" json:"IP,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"` PubKey string `protobuf:"bytes,2,opt,name=pubKey,proto3" json:"pubKey,omitempty"`
ConnStatus string `protobuf:"bytes,3,opt,name=connStatus,proto3" json:"connStatus,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"` 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"` Relayed bool `protobuf:"varint,5,opt,name=relayed,proto3" json:"relayed,omitempty"`
Direct bool `protobuf:"varint,6,opt,name=direct,proto3" json:"direct,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"` LocalIceCandidateType string `protobuf:"bytes,7,opt,name=localIceCandidateType,proto3" json:"localIceCandidateType,omitempty"`
RemoteIceCandidateType string `protobuf:"bytes,8,opt,name=remoteIceCandidateType,proto3" json:"remoteIceCandidateType,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"` 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() { func (x *PeerState) Reset() {
@ -839,6 +844,41 @@ func (x *PeerState) GetFqdn() string {
return "" 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 // LocalPeerState contains the latest state of the local peer
type LocalPeerState struct { type LocalPeerState struct {
state protoimpl.MessageState state protoimpl.MessageState
@ -919,6 +959,7 @@ type SignalState struct {
URL string `protobuf:"bytes,1,opt,name=URL,proto3" json:"URL,omitempty"` URL string `protobuf:"bytes,1,opt,name=URL,proto3" json:"URL,omitempty"`
Connected bool `protobuf:"varint,2,opt,name=connected,proto3" json:"connected,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() { func (x *SignalState) Reset() {
@ -967,6 +1008,13 @@ func (x *SignalState) GetConnected() bool {
return false return false
} }
func (x *SignalState) GetError() string {
if x != nil {
return x.Error
}
return ""
}
// ManagementState contains the latest state of a management connection // ManagementState contains the latest state of a management connection
type ManagementState struct { type ManagementState struct {
state protoimpl.MessageState state protoimpl.MessageState
@ -975,6 +1023,7 @@ type ManagementState struct {
URL string `protobuf:"bytes,1,opt,name=URL,proto3" json:"URL,omitempty"` URL string `protobuf:"bytes,1,opt,name=URL,proto3" json:"URL,omitempty"`
Connected bool `protobuf:"varint,2,opt,name=connected,proto3" json:"connected,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() { func (x *ManagementState) Reset() {
@ -1023,6 +1072,77 @@ func (x *ManagementState) GetConnected() bool {
return false 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 // FullStatus contains the full state held by the Status instance
type FullStatus struct { type FullStatus struct {
state protoimpl.MessageState state protoimpl.MessageState
@ -1033,12 +1153,13 @@ type FullStatus struct {
SignalState *SignalState `protobuf:"bytes,2,opt,name=signalState,proto3" json:"signalState,omitempty"` SignalState *SignalState `protobuf:"bytes,2,opt,name=signalState,proto3" json:"signalState,omitempty"`
LocalPeerState *LocalPeerState `protobuf:"bytes,3,opt,name=localPeerState,proto3" json:"localPeerState,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"` 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() { func (x *FullStatus) Reset() {
*x = FullStatus{} *x = FullStatus{}
if protoimpl.UnsafeEnabled { if protoimpl.UnsafeEnabled {
mi := &file_daemon_proto_msgTypes[16] mi := &file_daemon_proto_msgTypes[17]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -1051,7 +1172,7 @@ func (x *FullStatus) String() string {
func (*FullStatus) ProtoMessage() {} func (*FullStatus) ProtoMessage() {}
func (x *FullStatus) ProtoReflect() protoreflect.Message { func (x *FullStatus) ProtoReflect() protoreflect.Message {
mi := &file_daemon_proto_msgTypes[16] mi := &file_daemon_proto_msgTypes[17]
if protoimpl.UnsafeEnabled && x != nil { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -1064,7 +1185,7 @@ func (x *FullStatus) ProtoReflect() protoreflect.Message {
// Deprecated: Use FullStatus.ProtoReflect.Descriptor instead. // Deprecated: Use FullStatus.ProtoReflect.Descriptor instead.
func (*FullStatus) Descriptor() ([]byte, []int) { func (*FullStatus) Descriptor() ([]byte, []int) {
return file_daemon_proto_rawDescGZIP(), []int{16} return file_daemon_proto_rawDescGZIP(), []int{17}
} }
func (x *FullStatus) GetManagementState() *ManagementState { func (x *FullStatus) GetManagementState() *ManagementState {
@ -1095,6 +1216,13 @@ func (x *FullStatus) GetPeers() []*PeerState {
return nil 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 protoreflect.FileDescriptor
var file_daemon_proto_rawDesc = []byte{ 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 0x6e, 0x12, 0x3c, 0x0a, 0x19, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e,
0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x0a,
0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x20, 0x01, 0x28, 0x09, 0x52, 0x19, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61,
0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x28, 0x0a, 0x0f, 0x6b, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12,
0x65, 0x72, 0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x18, 0x03, 0x3e, 0x0a, 0x1a, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64,
0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x74, 0x65, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x0b, 0x20,
0x72, 0x66, 0x61, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x1a, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61,
0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x22, 0x3d, 0x0a, 0x0b, 0x53, 0x69, 0x67, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12,
0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52, 0x4c, 0x18, 0x52, 0x0a, 0x16, 0x6c, 0x61, 0x73, 0x74, 0x57, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64,
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x4c, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32,
0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x63, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,
0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x22, 0x41, 0x0a, 0x0f, 0x4d, 0x61, 0x6e, 0x61, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x16, 0x6c, 0x61, 0x73,
0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x74, 0x57, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68,
0x52, 0x4c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x4c, 0x12, 0x1c, 0x0a, 0x61, 0x6b, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x52, 0x78, 0x18, 0x0d,
0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x52, 0x78, 0x12, 0x18, 0x0a,
0x52, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x22, 0xef, 0x01, 0x0a, 0x0a, 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x54, 0x78, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07,
0x46, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x41, 0x0a, 0x0f, 0x6d, 0x61, 0x62, 0x79, 0x74, 0x65, 0x73, 0x54, 0x78, 0x22, 0x76, 0x0a, 0x0e, 0x4c, 0x6f, 0x63, 0x61, 0x6c,
0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18,
0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x61, 0x6e, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62,
0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0f, 0x6d, 0x61, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65,
0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x35, 0x0a, 0x79, 0x12, 0x28, 0x0a, 0x0f, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x74, 0x65, 0x72,
0x0b, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x66, 0x61, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x6b, 0x65, 0x72, 0x6e,
0x28, 0x0b, 0x32, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x66,
0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x71, 0x64, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x22,
0x74, 0x61, 0x74, 0x65, 0x12, 0x3e, 0x0a, 0x0e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x53, 0x0a, 0x0b, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10,
0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x64, 0x0a, 0x03, 0x55, 0x52, 0x4c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x4c,
0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x02, 0x20,
0x74, 0x61, 0x74, 0x65, 0x52, 0x0e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x01, 0x28, 0x08, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x12, 0x14,
0x74, 0x61, 0x74, 0x65, 0x12, 0x27, 0x0a, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65,
0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x57, 0x0a, 0x0f, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65,
0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x32, 0xf7, 0x02, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52, 0x4c, 0x18, 0x01,
0x0a, 0x0d, 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x4c, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e,
0x36, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x63, 0x6f,
0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72,
0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x52, 0x0a,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x0c, 0x57, 0x61, 0x69, 0x74, 0x53, 0x0a, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55,
0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x52, 0x49, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x49, 0x12, 0x1c, 0x0a,
0x2e, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x09, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08,
0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57, 0x61, 0x52, 0x09, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65,
0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f,
0x73, 0x65, 0x22, 0x00, 0x12, 0x2d, 0x0a, 0x02, 0x55, 0x70, 0x12, 0x11, 0x2e, 0x64, 0x61, 0x65, 0x72, 0x22, 0x9b, 0x02, 0x0a, 0x0a, 0x46, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73,
0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x12, 0x41, 0x0a, 0x0f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74,
0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x64, 0x61, 0x65, 0x6d,
0x65, 0x22, 0x00, 0x12, 0x39, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x15, 0x2e, 0x6f, 0x6e, 0x2e, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61,
0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x74, 0x65, 0x52, 0x0f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74,
0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x35, 0x0a, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61,
0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f,
0x0a, 0x04, 0x44, 0x6f, 0x77, 0x6e, 0x12, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x6e, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0b, 0x73,
0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x64, 0x61, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x3e, 0x0a, 0x0e, 0x6c, 0x6f,
0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01,
0x65, 0x22, 0x00, 0x12, 0x42, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x63, 0x61,
0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0e, 0x6c, 0x6f, 0x63, 0x61,
0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x27, 0x0a, 0x05, 0x70, 0x65,
0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x64, 0x61, 0x65, 0x6d,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x6e, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x70, 0x65,
0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 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 ( var (
@ -1281,7 +1436,7 @@ func file_daemon_proto_rawDescGZIP() []byte {
return file_daemon_proto_rawDescData 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{}{ var file_daemon_proto_goTypes = []interface{}{
(*LoginRequest)(nil), // 0: daemon.LoginRequest (*LoginRequest)(nil), // 0: daemon.LoginRequest
(*LoginResponse)(nil), // 1: daemon.LoginResponse (*LoginResponse)(nil), // 1: daemon.LoginResponse
@ -1299,33 +1454,36 @@ var file_daemon_proto_goTypes = []interface{}{
(*LocalPeerState)(nil), // 13: daemon.LocalPeerState (*LocalPeerState)(nil), // 13: daemon.LocalPeerState
(*SignalState)(nil), // 14: daemon.SignalState (*SignalState)(nil), // 14: daemon.SignalState
(*ManagementState)(nil), // 15: daemon.ManagementState (*ManagementState)(nil), // 15: daemon.ManagementState
(*FullStatus)(nil), // 16: daemon.FullStatus (*RelayState)(nil), // 16: daemon.RelayState
(*timestamppb.Timestamp)(nil), // 17: google.protobuf.Timestamp (*FullStatus)(nil), // 17: daemon.FullStatus
(*timestamppb.Timestamp)(nil), // 18: google.protobuf.Timestamp
} }
var file_daemon_proto_depIdxs = []int32{ var file_daemon_proto_depIdxs = []int32{
16, // 0: daemon.StatusResponse.fullStatus:type_name -> daemon.FullStatus 17, // 0: daemon.StatusResponse.fullStatus:type_name -> daemon.FullStatus
17, // 1: daemon.PeerState.connStatusUpdate:type_name -> google.protobuf.Timestamp 18, // 1: daemon.PeerState.connStatusUpdate:type_name -> google.protobuf.Timestamp
15, // 2: daemon.FullStatus.managementState:type_name -> daemon.ManagementState 18, // 2: daemon.PeerState.lastWireguardHandshake:type_name -> google.protobuf.Timestamp
14, // 3: daemon.FullStatus.signalState:type_name -> daemon.SignalState 15, // 3: daemon.FullStatus.managementState:type_name -> daemon.ManagementState
13, // 4: daemon.FullStatus.localPeerState:type_name -> daemon.LocalPeerState 14, // 4: daemon.FullStatus.signalState:type_name -> daemon.SignalState
12, // 5: daemon.FullStatus.peers:type_name -> daemon.PeerState 13, // 5: daemon.FullStatus.localPeerState:type_name -> daemon.LocalPeerState
0, // 6: daemon.DaemonService.Login:input_type -> daemon.LoginRequest 12, // 6: daemon.FullStatus.peers:type_name -> daemon.PeerState
2, // 7: daemon.DaemonService.WaitSSOLogin:input_type -> daemon.WaitSSOLoginRequest 16, // 7: daemon.FullStatus.relays:type_name -> daemon.RelayState
4, // 8: daemon.DaemonService.Up:input_type -> daemon.UpRequest 0, // 8: daemon.DaemonService.Login:input_type -> daemon.LoginRequest
6, // 9: daemon.DaemonService.Status:input_type -> daemon.StatusRequest 2, // 9: daemon.DaemonService.WaitSSOLogin:input_type -> daemon.WaitSSOLoginRequest
8, // 10: daemon.DaemonService.Down:input_type -> daemon.DownRequest 4, // 10: daemon.DaemonService.Up:input_type -> daemon.UpRequest
10, // 11: daemon.DaemonService.GetConfig:input_type -> daemon.GetConfigRequest 6, // 11: daemon.DaemonService.Status:input_type -> daemon.StatusRequest
1, // 12: daemon.DaemonService.Login:output_type -> daemon.LoginResponse 8, // 12: daemon.DaemonService.Down:input_type -> daemon.DownRequest
3, // 13: daemon.DaemonService.WaitSSOLogin:output_type -> daemon.WaitSSOLoginResponse 10, // 13: daemon.DaemonService.GetConfig:input_type -> daemon.GetConfigRequest
5, // 14: daemon.DaemonService.Up:output_type -> daemon.UpResponse 1, // 14: daemon.DaemonService.Login:output_type -> daemon.LoginResponse
7, // 15: daemon.DaemonService.Status:output_type -> daemon.StatusResponse 3, // 15: daemon.DaemonService.WaitSSOLogin:output_type -> daemon.WaitSSOLoginResponse
9, // 16: daemon.DaemonService.Down:output_type -> daemon.DownResponse 5, // 16: daemon.DaemonService.Up:output_type -> daemon.UpResponse
11, // 17: daemon.DaemonService.GetConfig:output_type -> daemon.GetConfigResponse 7, // 17: daemon.DaemonService.Status:output_type -> daemon.StatusResponse
12, // [12:18] is the sub-list for method output_type 9, // 18: daemon.DaemonService.Down:output_type -> daemon.DownResponse
6, // [6:12] is the sub-list for method input_type 11, // 19: daemon.DaemonService.GetConfig:output_type -> daemon.GetConfigResponse
6, // [6:6] is the sub-list for extension type_name 14, // [14:20] is the sub-list for method output_type
6, // [6:6] is the sub-list for extension extendee 8, // [8:14] is the sub-list for method input_type
0, // [0:6] is the sub-list for field type_name 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() } 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{} { 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 { switch v := v.(*FullStatus); i {
case 0: case 0:
return &v.state return &v.state
@ -1546,7 +1716,7 @@ func file_daemon_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_daemon_proto_rawDesc, RawDescriptor: file_daemon_proto_rawDesc,
NumEnums: 0, NumEnums: 0,
NumMessages: 17, NumMessages: 18,
NumExtensions: 0, NumExtensions: 0,
NumServices: 1, NumServices: 1,
}, },

View File

@ -127,15 +127,20 @@ message PeerState {
bool relayed = 5; bool relayed = 5;
bool direct = 6; bool direct = 6;
string localIceCandidateType = 7; string localIceCandidateType = 7;
string remoteIceCandidateType =8; string remoteIceCandidateType = 8;
string fqdn = 9; 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 // LocalPeerState contains the latest state of the local peer
message LocalPeerState { message LocalPeerState {
string IP = 1; string IP = 1;
string pubKey = 2; string pubKey = 2;
bool kernelInterface =3; bool kernelInterface = 3;
string fqdn = 4; string fqdn = 4;
} }
@ -143,17 +148,28 @@ message LocalPeerState {
message SignalState { message SignalState {
string URL = 1; string URL = 1;
bool connected = 2; bool connected = 2;
string error = 3;
} }
// ManagementState contains the latest state of a management connection // ManagementState contains the latest state of a management connection
message ManagementState { message ManagementState {
string URL = 1; string URL = 1;
bool connected = 2; 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 // FullStatus contains the full state held by the Status instance
message FullStatus { message FullStatus {
ManagementState managementState = 1; ManagementState managementState = 1;
SignalState signalState = 2; SignalState signalState = 2;
LocalPeerState localPeerState = 3; LocalPeerState localPeerState = 3;
repeated PeerState peers = 4; repeated PeerState peers = 4;
repeated RelayState relays = 5;
} }

View File

@ -13,5 +13,5 @@ script_path=$(dirname $(realpath "$0"))
cd "$script_path" cd "$script_path"
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.26 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 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" cd "$old_pwd"

View File

@ -21,6 +21,8 @@ import (
"github.com/netbirdio/netbird/version" "github.com/netbirdio/netbird/version"
) )
const probeThreshold = time.Second * 5
// Server for service control. // Server for service control.
type Server struct { type Server struct {
rootCtx context.Context rootCtx context.Context
@ -37,6 +39,12 @@ type Server struct {
proto.UnimplementedDaemonServiceServer proto.UnimplementedDaemonServiceServer
statusRecorder *peer.Status statusRecorder *peer.Status
mgmProbe *internal.Probe
signalProbe *internal.Probe
relayProbe *internal.Probe
wgProbe *internal.Probe
lastProbe time.Time
} }
type oauthAuthFlow struct { type oauthAuthFlow struct {
@ -53,7 +61,11 @@ func New(ctx context.Context, configPath, logFile string) *Server {
latestConfigInput: internal.ConfigInput{ latestConfigInput: internal.ConfigInput{
ConfigPath: configPath, 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() { 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) log.Errorf("init connections: %v", err)
} }
}() }()
@ -409,7 +421,7 @@ func (s *Server) Up(callerCtx context.Context, _ *proto.UpRequest) (*proto.UpRes
} }
go func() { 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) log.Errorf("run client connection: %v", err)
return return
} }
@ -433,7 +445,7 @@ func (s *Server) Down(_ context.Context, _ *proto.DownRequest) (*proto.DownRespo
return &proto.DownResponse{}, nil return &proto.DownResponse{}, nil
} }
// Status starts engine work in the daemon. // Status returns the daemon status
func (s *Server) Status( func (s *Server) Status(
_ context.Context, _ context.Context,
msg *proto.StatusRequest, msg *proto.StatusRequest,
@ -455,6 +467,8 @@ func (s *Server) Status(
} }
if msg.GetFullPeerStatus { if msg.GetFullPeerStatus {
s.runProbes()
fullStatus := s.statusRecorder.GetFullStatus() fullStatus := s.statusRecorder.GetFullStatus()
pbFullStatus := toProtoFullStatus(fullStatus) pbFullStatus := toProtoFullStatus(fullStatus)
statusResponse.FullStatus = pbFullStatus statusResponse.FullStatus = pbFullStatus
@ -463,6 +477,20 @@ func (s *Server) Status(
return &statusResponse, nil 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. // GetConfig of the daemon.
func (s *Server) GetConfig(_ context.Context, _ *proto.GetConfigRequest) (*proto.GetConfigResponse, error) { func (s *Server) GetConfig(_ context.Context, _ *proto.GetConfigRequest) (*proto.GetConfigResponse, error) {
s.mutex.Lock() s.mutex.Lock()
@ -503,13 +531,20 @@ func toProtoFullStatus(fullStatus peer.FullStatus) *proto.FullStatus {
SignalState: &proto.SignalState{}, SignalState: &proto.SignalState{},
LocalPeerState: &proto.LocalPeerState{}, LocalPeerState: &proto.LocalPeerState{},
Peers: []*proto.PeerState{}, Peers: []*proto.PeerState{},
Relays: []*proto.RelayState{},
} }
pbFullStatus.ManagementState.URL = fullStatus.ManagementState.URL pbFullStatus.ManagementState.URL = fullStatus.ManagementState.URL
pbFullStatus.ManagementState.Connected = fullStatus.ManagementState.Connected 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.URL = fullStatus.SignalState.URL
pbFullStatus.SignalState.Connected = fullStatus.SignalState.Connected 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.IP = fullStatus.LocalPeerState.IP
pbFullStatus.LocalPeerState.PubKey = fullStatus.LocalPeerState.PubKey pbFullStatus.LocalPeerState.PubKey = fullStatus.LocalPeerState.PubKey
@ -518,17 +553,34 @@ func toProtoFullStatus(fullStatus peer.FullStatus) *proto.FullStatus {
for _, peerState := range fullStatus.Peers { for _, peerState := range fullStatus.Peers {
pbPeerState := &proto.PeerState{ pbPeerState := &proto.PeerState{
IP: peerState.IP, IP: peerState.IP,
PubKey: peerState.PubKey, PubKey: peerState.PubKey,
ConnStatus: peerState.ConnStatus.String(), ConnStatus: peerState.ConnStatus.String(),
ConnStatusUpdate: timestamppb.New(peerState.ConnStatusUpdate), ConnStatusUpdate: timestamppb.New(peerState.ConnStatusUpdate),
Relayed: peerState.Relayed, Relayed: peerState.Relayed,
Direct: peerState.Direct, Direct: peerState.Direct,
LocalIceCandidateType: peerState.LocalIceCandidateType, LocalIceCandidateType: peerState.LocalIceCandidateType,
RemoteIceCandidateType: peerState.RemoteIceCandidateType, RemoteIceCandidateType: peerState.RemoteIceCandidateType,
Fqdn: peerState.FQDN, 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) 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 return &pbFullStatus
} }

View File

@ -27,6 +27,12 @@ type WGIface struct {
filter PacketFilter filter PacketFilter
} }
type WGStats struct {
LastHandshake time.Time
TxBytes int64
RxBytes int64
}
// IsUserspaceBind indicates whether this interfaces is userspace with bind.ICEBind // IsUserspaceBind indicates whether this interfaces is userspace with bind.ICEBind
func (w *WGIface) IsUserspaceBind() bool { func (w *WGIface) IsUserspaceBind() bool {
return w.userspaceBind return w.userspaceBind
@ -139,3 +145,8 @@ func (w *WGIface) GetDevice() *DeviceWrapper {
return w.tun.Wrapper() 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)
}

View File

@ -14,4 +14,5 @@ type wgConfigurer interface {
addAllowedIP(peerKey string, allowedIP string) error addAllowedIP(peerKey string, allowedIP string) error
removeAllowedIP(peerKey string, allowedIP string) error removeAllowedIP(peerKey string, allowedIP string) error
close() close()
getStats(peerKey string) (WGStats, error)
} }

View File

@ -207,3 +207,15 @@ func (c *wgKernelConfigurer) configure(config wgtypes.Config) error {
func (c *wgKernelConfigurer) close() { 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
}

View File

@ -6,6 +6,7 @@ import (
"net" "net"
"os" "os"
"runtime" "runtime"
"strconv"
"strings" "strings"
"time" "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 { func toWgUserspaceString(wgCfg wgtypes.Config) string {
var sb strings.Builder var sb strings.Builder
if wgCfg.PrivateKey != nil { if wgCfg.PrivateKey != nil {

View File

@ -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)
})
}
}

View File

@ -17,4 +17,5 @@ type Client interface {
GetDeviceAuthorizationFlow(serverKey wgtypes.Key) (*proto.DeviceAuthorizationFlow, error) GetDeviceAuthorizationFlow(serverKey wgtypes.Key) (*proto.DeviceAuthorizationFlow, error)
GetPKCEAuthorizationFlow(serverKey wgtypes.Key) (*proto.PKCEAuthorizationFlow, error) GetPKCEAuthorizationFlow(serverKey wgtypes.Key) (*proto.PKCEAuthorizationFlow, error)
GetNetworkMap() (*proto.NetworkMap, error) GetNetworkMap() (*proto.NetworkMap, error)
IsHealthy() bool
} }

View File

@ -28,7 +28,7 @@ import (
// ConnStateNotifier is a wrapper interface of the status recorders // ConnStateNotifier is a wrapper interface of the status recorders
type ConnStateNotifier interface { type ConnStateNotifier interface {
MarkManagementDisconnected() MarkManagementDisconnected(error)
MarkManagementConnected() MarkManagementConnected()
} }
@ -154,7 +154,7 @@ func (c *GrpcClient) Sync(msgHandler func(msg *proto.SyncResponse) error) error
return nil return nil
default: default:
backOff.Reset() // reset backoff counter after successful connection 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) log.Warnf("disconnected from the Management service but will retry silently. Reason: %v", err)
return err return err
} }
@ -283,6 +283,32 @@ func (c *GrpcClient) GetServerPublicKey() (*wgtypes.Key, error) {
return &serverKey, nil 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) { func (c *GrpcClient) login(serverKey wgtypes.Key, req *proto.LoginRequest) (*proto.LoginResponse, error) {
if !c.ready() { if !c.ready() {
return nil, fmt.Errorf("no connection to management") return nil, fmt.Errorf("no connection to management")
@ -400,14 +426,14 @@ func (c *GrpcClient) GetPKCEAuthorizationFlow(serverKey wgtypes.Key) (*proto.PKC
return flowInfoResp, nil return flowInfoResp, nil
} }
func (c *GrpcClient) notifyDisconnected() { func (c *GrpcClient) notifyDisconnected(err error) {
c.connStateCallbackLock.RLock() c.connStateCallbackLock.RLock()
defer c.connStateCallbackLock.RUnlock() defer c.connStateCallbackLock.RUnlock()
if c.connStateCallback == nil { if c.connStateCallback == nil {
return return
} }
c.connStateCallback.MarkManagementDisconnected() c.connStateCallback.MarkManagementDisconnected(err)
} }
func (c *GrpcClient) notifyConnected() { func (c *GrpcClient) notifyConnected() {

View File

@ -1,9 +1,10 @@
package client package client
import ( import (
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"github.com/netbirdio/netbird/client/system" "github.com/netbirdio/netbird/client/system"
"github.com/netbirdio/netbird/management/proto" "github.com/netbirdio/netbird/management/proto"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
) )
type MockClient struct { type MockClient struct {
@ -16,6 +17,10 @@ type MockClient struct {
GetPKCEAuthorizationFlowFunc func(serverKey wgtypes.Key) (*proto.PKCEAuthorizationFlow, error) GetPKCEAuthorizationFlowFunc func(serverKey wgtypes.Key) (*proto.PKCEAuthorizationFlow, error)
} }
func (m *MockClient) IsHealthy() bool {
return true
}
func (m *MockClient) Close() error { func (m *MockClient) Close() error {
if m.CloseFunc == nil { if m.CloseFunc == nil {
return nil return nil

View File

@ -35,6 +35,7 @@ type Client interface {
GetStatus() Status GetStatus() Status
Receive(msgHandler func(msg *proto.Message) error) error Receive(msgHandler func(msg *proto.Message) error) error
Ready() bool Ready() bool
IsHealthy() bool
WaitStreamConnected() WaitStreamConnected()
SendToStream(msg *proto.EncryptedMessage) error SendToStream(msg *proto.EncryptedMessage) error
Send(msg *proto.Message) error Send(msg *proto.Message) error

View File

@ -28,7 +28,7 @@ const defaultSendTimeout = 5 * time.Second
// ConnStateNotifier is a wrapper interface of the status recorder // ConnStateNotifier is a wrapper interface of the status recorder
type ConnStateNotifier interface { type ConnStateNotifier interface {
MarkSignalDisconnected() MarkSignalDisconnected(error)
MarkSignalConnected() 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 // 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 // reset times and next try will start with a long delay
backOff.Reset() backOff.Reset()
c.notifyDisconnected() c.notifyDisconnected(err)
log.Warnf("disconnected from the Signal service but will retry silently. Reason: %v", err) log.Warnf("disconnected from the Signal service but will retry silently. Reason: %v", err)
return err return err
} }
@ -238,6 +238,35 @@ func (c *GrpcClient) Ready() bool {
return c.signalConn.GetState() == connectivity.Ready || c.signalConn.GetState() == connectivity.Idle 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 // WaitStreamConnected waits until the client is connected to the Signal stream
func (c *GrpcClient) WaitStreamConnected() { 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() c.connStateCallbackLock.RLock()
defer c.connStateCallbackLock.RUnlock() defer c.connStateCallbackLock.RUnlock()
if c.connStateCallback == nil { if c.connStateCallback == nil {
return return
} }
c.connStateCallback.MarkSignalDisconnected() c.connStateCallback.MarkSignalDisconnected(err)
} }
func (c *GrpcClient) notifyConnected() { func (c *GrpcClient) notifyConnected() {

View File

@ -15,6 +15,10 @@ type MockClient struct {
SendFunc func(msg *proto.Message) error SendFunc func(msg *proto.Message) error
} }
func (sm *MockClient) IsHealthy() bool {
return true
}
func (sm *MockClient) Close() error { func (sm *MockClient) Close() error {
if sm.CloseFunc == nil { if sm.CloseFunc == nil {
return nil return nil