mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-16 07:16:38 +00:00
* Add client metrics * Add client metrics system with OpenTelemetry and VictoriaMetrics support Implements a comprehensive client metrics system to track peer connection stages and performance. The system supports multiple backend implementations (OpenTelemetry, VictoriaMetrics, and no-op) and tracks detailed connection stage durations from creation through WireGuard handshake. Key changes: - Add metrics package with pluggable backend implementations - Implement OpenTelemetry metrics backend - Implement VictoriaMetrics metrics backend - Add no-op metrics implementation for disabled state - Track connection stages: creation, semaphore, signaling, connection ready, and WireGuard handshake - Move WireGuard watcher functionality to conn.go - Refactor engine to integrate metrics tracking - Add metrics export endpoint in debug server * Add signaling metrics tracking for initial and reconnection attempts * Reset connection stage timestamps during reconnections to exclude unnecessary metrics tracking * Delete otel lib from client * Update unit tests * Invoke callback on handshake success in WireGuard watcher * Add Netbird version tracking to client metrics Integrate Netbird version into VictoriaMetrics backend and metrics labels. Update `ClientMetrics` constructor and metric name formatting to include version information. * Add sync duration tracking to client metrics Introduce `RecordSyncDuration` for measuring sync message processing time. Update all metrics implementations (VictoriaMetrics, no-op) to support the new method. Refactor `ClientMetrics` to use `AgentInfo` for static agent data. * Remove no-op metrics implementation and simplify ClientMetrics constructor Eliminate unused `noopMetrics` and refactor `ClientMetrics` to always use the VictoriaMetrics implementation. Update associated logic to reflect these changes. * Add total duration tracking for connection attempts Calculate total duration for both initial connections and reconnections, accounting for different timestamp scenarios. Update `Export` method to include Prometheus HELP comments. * Add metrics push support to VictoriaMetrics integration * [client] anchor connection metrics to first signal received * Remove creation_to_semaphore connection stage metric The semaphore queuing stage (Created → SemaphoreAcquired) is no longer tracked. Connection metrics now start from SignalingReceived. Updated docs and Grafana dashboard accordingly. * [client] Add remote push config for metrics with version-based eligibility Introduce remoteconfig.Manager that fetches a remote JSON config to control metrics push interval and restrict pushing to a specific agent version range. When NB_METRICS_INTERVAL is set, remote config is bypassed entirely for local override. * [client] Add WASM-compatible NewClientMetrics implementation Replace NewClientMetrics in metrics.go with a WASM-specific stub in metrics_js.go, returning nil for compatibility with JS builds. Simplify method usage for WASM targets. * Add missing file * Update default case in DeploymentType.String to return "unknown" instead of "selfhosted" * [client] Rework metrics to use timestamped samples instead of histograms Replace cumulative Prometheus histograms with timestamped point-in-time samples that are pushed once and cleared. This fixes metrics for sparse events (connections/syncs that happen once at startup) where rate() and increase() produced incorrect or empty results. Changes: - Switch from VictoriaMetrics histogram library to raw Prometheus text format with explicit millisecond timestamps - Reset samples after successful push (no resending stale data) - Rename connection_to_handshake → connection_to_wg_handshake - Add netbird_peer_connection_count metric for ICE vs Relay tracking - Simplify dashboard: point-based scatter plots, donut pie chart - Add maxStalenessInterval=1m to VictoriaMetrics to prevent forward-fill - Fix deployment_type Unknown returning "selfhosted" instead of "unknown" - Fix inverted shouldPush condition in push.go * [client] Add InfluxDB metrics backend alongside VictoriaMetrics Add influxdb.go with timestamped line protocol export for sparse one-shot events. Restore victoria.go to use proper Prometheus histograms. Update Grafana dashboards, add InfluxDB datasource, and update docs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * [client] Fix metrics issues and update dev docker setup - Fix StopPush not clearing push state, preventing restart - Fix race condition reading currentConnPriority without lock in recordConnectionMetrics - Fix stale comment referencing old metrics server URL - Update docker-compose for InfluxDB: add scoped tokens, .env config, init scripts - Rename docker-compose.victoria.yml to docker-compose.yml * [client] Add anonymised peer tracking to pushed metrics Introduce peer_id and connection_pair_id tags to InfluxDB metrics. Public keys are hashed (truncated SHA-256) for anonymisation. The connection pair ID is deterministic regardless of which side computes it, enabling deduplication of reconnections in the ICE vs Relay dashboard. Also pin Grafana to v11.6.0 for file-based provisioning and fix datasource UID references. * Remove unused dependencies from go.mod and go.sum * Refactor InfluxDB ingest pipeline: extract validation logic - Move line validation logic to `validateLine` and `validateField` helper functions. - Improve error handling with structured validation and clearer separation of concerns. - Add stderr redirection for error messages in `create-tokens.sh`. * Set non-root user in Dockerfile for Ingest service * Fix Windows CI: command line too long * Remove Victoria metrics * Add hashed peer ID as Authorization header in metrics push * Revert influxdb in docker compose * Enable gzip compression and authorization validation for metrics push and ingest * Reducate code of complexity * Update debug documentation to include metrics.txt description * Increase `maxBodySize` limit to 50 MB and update gzip reader wrapping logic * Refactor deployment type detection to use URL parsing for improved accuracy * Update readme * Throttle remote config retries on fetch failure * Preserve first WG handshake timestamp, ignore rekeys * Skip adding empty metrics.txt to debug bundle in debug mode * Update default metrics server URL to https://ingest.netbird.io * Atomic metrics export-and-reset to prevent sample loss between Export and Reset calls * Fix doc * Refactor Push configuration to improve clarity and enforce minimum push interval * Remove `minPushInterval` and update push interval validation logic * Revert ExportAndReset, it is acceptable data loss * Fix metrics review issues: rename env var, remove stale infra, add tests - Rename NB_METRICS_ENABLED to NB_METRICS_PUSH_ENABLED to clarify that collection is always active (for debug bundles) and only push is opt-in - Change default config URL from staging to production (ingest.netbird.io) - Delete broken Prometheus dashboard (used non-existent metric names) - Delete unused VictoriaMetrics datasource config - Replace committed .env with .env.example containing placeholder values - Wire Grafana admin credentials through env vars in docker-compose - Make metricsStages a pointer to prevent reset-vs-write race on reconnect - Fix typed-nil interface in debug bundle path (GetClientMetrics) - Use deterministic field order in InfluxDB Export (sorted keys) - Replace Authorization header with X-Peer-ID for metrics push - Fix ingest server timeout to use time.Second instead of float - Fix gzip double-close, stale comments, trim log levels - Add tests for influxdb.go and MetricsStages * Add login duration metric, ingest tag validation, and duration bounds - Add netbird_login measurement recording login/auth duration to management server, with success/failure result tag - Validate InfluxDB tags against per-measurement allowlists in ingest server to prevent arbitrary tag injection - Cap all duration fields (*_seconds) at 300s instead of only total_seconds - Add ingest server tests for tag/field validation, bounds, and auth * Add arch tag to all metrics * Fix Grafana dashboard: add arch to drop columns, add login panels * Validate NB_METRICS_SERVER_URL is an absolute HTTP(S) URL * Address review comments: fix README wording, update stale comments * Clarify env var precedence does not bypass remote config eligibility * Remove accidentally committed pprof files --------- Co-authored-by: Viktor Liu <viktor@netbird.io>
1707 lines
46 KiB
Go
1707 lines
46 KiB
Go
package internal
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"net/netip"
|
|
"os"
|
|
"runtime"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/golang/mock/gomock"
|
|
"github.com/google/uuid"
|
|
log "github.com/sirupsen/logrus"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"go.opentelemetry.io/otel"
|
|
wgdevice "golang.zx2c4.com/wireguard/device"
|
|
"golang.zx2c4.com/wireguard/tun/netstack"
|
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/keepalive"
|
|
|
|
"github.com/netbirdio/netbird/client/internal/stdnet"
|
|
"github.com/netbirdio/netbird/management/server/job"
|
|
|
|
"github.com/netbirdio/management-integrations/integrations"
|
|
|
|
"github.com/netbirdio/netbird/management/internals/controllers/network_map/controller"
|
|
"github.com/netbirdio/netbird/management/internals/controllers/network_map/update_channel"
|
|
"github.com/netbirdio/netbird/management/internals/modules/peers"
|
|
"github.com/netbirdio/netbird/management/internals/modules/peers/ephemeral/manager"
|
|
nbgrpc "github.com/netbirdio/netbird/management/internals/shared/grpc"
|
|
|
|
"github.com/netbirdio/netbird/management/internals/server/config"
|
|
"github.com/netbirdio/netbird/management/server/groups"
|
|
|
|
"github.com/netbirdio/netbird/client/iface"
|
|
"github.com/netbirdio/netbird/client/iface/configurer"
|
|
"github.com/netbirdio/netbird/client/iface/device"
|
|
"github.com/netbirdio/netbird/client/iface/udpmux"
|
|
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
|
"github.com/netbirdio/netbird/client/iface/wgproxy"
|
|
"github.com/netbirdio/netbird/client/internal/dns"
|
|
"github.com/netbirdio/netbird/client/internal/peer"
|
|
"github.com/netbirdio/netbird/client/internal/peer/guard"
|
|
icemaker "github.com/netbirdio/netbird/client/internal/peer/ice"
|
|
"github.com/netbirdio/netbird/client/internal/profilemanager"
|
|
"github.com/netbirdio/netbird/client/internal/routemanager"
|
|
nbssh "github.com/netbirdio/netbird/client/ssh"
|
|
"github.com/netbirdio/netbird/client/system"
|
|
nbdns "github.com/netbirdio/netbird/dns"
|
|
"github.com/netbirdio/netbird/management/server"
|
|
"github.com/netbirdio/netbird/management/server/activity"
|
|
"github.com/netbirdio/netbird/management/server/integrations/port_forwarding"
|
|
"github.com/netbirdio/netbird/management/server/permissions"
|
|
"github.com/netbirdio/netbird/management/server/settings"
|
|
"github.com/netbirdio/netbird/management/server/store"
|
|
"github.com/netbirdio/netbird/management/server/telemetry"
|
|
"github.com/netbirdio/netbird/management/server/types"
|
|
"github.com/netbirdio/netbird/monotime"
|
|
"github.com/netbirdio/netbird/route"
|
|
mgmt "github.com/netbirdio/netbird/shared/management/client"
|
|
mgmtProto "github.com/netbirdio/netbird/shared/management/proto"
|
|
relayClient "github.com/netbirdio/netbird/shared/relay/client"
|
|
signal "github.com/netbirdio/netbird/shared/signal/client"
|
|
"github.com/netbirdio/netbird/shared/signal/proto"
|
|
signalServer "github.com/netbirdio/netbird/signal/server"
|
|
"github.com/netbirdio/netbird/util"
|
|
)
|
|
|
|
var (
|
|
kaep = keepalive.EnforcementPolicy{
|
|
MinTime: 15 * time.Second,
|
|
PermitWithoutStream: true,
|
|
}
|
|
|
|
kasp = keepalive.ServerParameters{
|
|
MaxConnectionIdle: 15 * time.Second,
|
|
MaxConnectionAgeGrace: 5 * time.Second,
|
|
Time: 5 * time.Second,
|
|
Timeout: 2 * time.Second,
|
|
}
|
|
)
|
|
|
|
type MockWGIface struct {
|
|
CreateFunc func() error
|
|
CreateOnAndroidFunc func(routeRange []string, ip string, domains []string) error
|
|
IsUserspaceBindFunc func() bool
|
|
NameFunc func() string
|
|
AddressFunc func() wgaddr.Address
|
|
ToInterfaceFunc func() *net.Interface
|
|
UpFunc func() (*udpmux.UniversalUDPMuxDefault, error)
|
|
UpdateAddrFunc func(newAddr string) error
|
|
UpdatePeerFunc func(peerKey string, allowedIps []netip.Prefix, keepAlive time.Duration, endpoint *net.UDPAddr, preSharedKey *wgtypes.Key) error
|
|
RemovePeerFunc func(peerKey string) error
|
|
AddAllowedIPFunc func(peerKey string, allowedIP netip.Prefix) error
|
|
RemoveAllowedIPFunc func(peerKey string, allowedIP netip.Prefix) error
|
|
CloseFunc func() error
|
|
SetFilterFunc func(filter device.PacketFilter) error
|
|
GetFilterFunc func() device.PacketFilter
|
|
GetDeviceFunc func() *device.FilteredDevice
|
|
GetWGDeviceFunc func() *wgdevice.Device
|
|
GetStatsFunc func() (map[string]configurer.WGStats, error)
|
|
GetInterfaceGUIDStringFunc func() (string, error)
|
|
GetProxyFunc func() wgproxy.Proxy
|
|
GetProxyPortFunc func() uint16
|
|
GetNetFunc func() *netstack.Net
|
|
LastActivitiesFunc func() map[string]monotime.Time
|
|
}
|
|
|
|
func (m *MockWGIface) RenewTun(_ int) error {
|
|
return nil
|
|
}
|
|
|
|
func (m *MockWGIface) RemoveEndpointAddress(_ string) error {
|
|
return nil
|
|
}
|
|
|
|
func (m *MockWGIface) FullStats() (*configurer.Stats, error) {
|
|
return nil, fmt.Errorf("not implemented")
|
|
}
|
|
|
|
func (m *MockWGIface) GetInterfaceGUIDString() (string, error) {
|
|
return m.GetInterfaceGUIDStringFunc()
|
|
}
|
|
|
|
func (m *MockWGIface) Create() error {
|
|
return m.CreateFunc()
|
|
}
|
|
|
|
func (m *MockWGIface) CreateOnAndroid(routeRange []string, ip string, domains []string) error {
|
|
return m.CreateOnAndroidFunc(routeRange, ip, domains)
|
|
}
|
|
|
|
func (m *MockWGIface) IsUserspaceBind() bool {
|
|
return m.IsUserspaceBindFunc()
|
|
}
|
|
|
|
func (m *MockWGIface) Name() string {
|
|
return m.NameFunc()
|
|
}
|
|
|
|
func (m *MockWGIface) Address() wgaddr.Address {
|
|
return m.AddressFunc()
|
|
}
|
|
|
|
func (m *MockWGIface) ToInterface() *net.Interface {
|
|
return m.ToInterfaceFunc()
|
|
}
|
|
|
|
func (m *MockWGIface) Up() (*udpmux.UniversalUDPMuxDefault, error) {
|
|
return m.UpFunc()
|
|
}
|
|
|
|
func (m *MockWGIface) UpdateAddr(newAddr string) error {
|
|
return m.UpdateAddrFunc(newAddr)
|
|
}
|
|
|
|
func (m *MockWGIface) UpdatePeer(peerKey string, allowedIps []netip.Prefix, keepAlive time.Duration, endpoint *net.UDPAddr, preSharedKey *wgtypes.Key) error {
|
|
return m.UpdatePeerFunc(peerKey, allowedIps, keepAlive, endpoint, preSharedKey)
|
|
}
|
|
|
|
func (m *MockWGIface) RemovePeer(peerKey string) error {
|
|
return m.RemovePeerFunc(peerKey)
|
|
}
|
|
|
|
func (m *MockWGIface) AddAllowedIP(peerKey string, allowedIP netip.Prefix) error {
|
|
return m.AddAllowedIPFunc(peerKey, allowedIP)
|
|
}
|
|
|
|
func (m *MockWGIface) RemoveAllowedIP(peerKey string, allowedIP netip.Prefix) error {
|
|
return m.RemoveAllowedIPFunc(peerKey, allowedIP)
|
|
}
|
|
|
|
func (m *MockWGIface) Close() error {
|
|
return m.CloseFunc()
|
|
}
|
|
|
|
func (m *MockWGIface) SetFilter(filter device.PacketFilter) error {
|
|
return m.SetFilterFunc(filter)
|
|
}
|
|
|
|
func (m *MockWGIface) GetFilter() device.PacketFilter {
|
|
return m.GetFilterFunc()
|
|
}
|
|
|
|
func (m *MockWGIface) GetDevice() *device.FilteredDevice {
|
|
return m.GetDeviceFunc()
|
|
}
|
|
|
|
func (m *MockWGIface) GetWGDevice() *wgdevice.Device {
|
|
return m.GetWGDeviceFunc()
|
|
}
|
|
|
|
func (m *MockWGIface) GetStats() (map[string]configurer.WGStats, error) {
|
|
return m.GetStatsFunc()
|
|
}
|
|
|
|
func (m *MockWGIface) GetProxy() wgproxy.Proxy {
|
|
return m.GetProxyFunc()
|
|
}
|
|
|
|
func (m *MockWGIface) GetProxyPort() uint16 {
|
|
if m.GetProxyPortFunc != nil {
|
|
return m.GetProxyPortFunc()
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func (m *MockWGIface) GetNet() *netstack.Net {
|
|
return m.GetNetFunc()
|
|
}
|
|
|
|
func (m *MockWGIface) LastActivities() map[string]monotime.Time {
|
|
if m.LastActivitiesFunc != nil {
|
|
return m.LastActivitiesFunc()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *MockWGIface) SetPresharedKey(peerKey string, psk wgtypes.Key, updateOnly bool) error {
|
|
return nil
|
|
}
|
|
|
|
func TestMain(m *testing.M) {
|
|
_ = util.InitLog("debug", util.LogConsole)
|
|
code := m.Run()
|
|
os.Exit(code)
|
|
}
|
|
|
|
func TestEngine_SSH(t *testing.T) {
|
|
key, err := wgtypes.GeneratePrivateKey()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
return
|
|
}
|
|
|
|
sshKey, err := nbssh.GeneratePrivateKey(nbssh.ED25519)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
return
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
relayMgr := relayClient.NewManager(ctx, nil, key.PublicKey().String(), iface.DefaultMTU)
|
|
engine := NewEngine(
|
|
ctx, cancel,
|
|
&EngineConfig{
|
|
WgIfaceName: "utun101",
|
|
WgAddr: "100.64.0.1/24",
|
|
WgPrivateKey: key,
|
|
WgPort: 33100,
|
|
ServerSSHAllowed: true,
|
|
MTU: iface.DefaultMTU,
|
|
SSHKey: sshKey,
|
|
},
|
|
EngineServices{
|
|
SignalClient: &signal.MockClient{},
|
|
MgmClient: &mgmt.MockClient{},
|
|
RelayManager: relayMgr,
|
|
StatusRecorder: peer.NewRecorder("https://mgm"),
|
|
},
|
|
MobileDependency{},
|
|
)
|
|
|
|
engine.dnsServer = &dns.MockServer{
|
|
UpdateDNSServerFunc: func(serial uint64, update nbdns.Config) error { return nil },
|
|
}
|
|
|
|
err = engine.Start(nil, nil)
|
|
require.NoError(t, err)
|
|
|
|
defer func() {
|
|
err := engine.Stop()
|
|
if err != nil {
|
|
return
|
|
}
|
|
}()
|
|
|
|
peerWithSSH := &mgmtProto.RemotePeerConfig{
|
|
WgPubKey: "MNHf3Ma6z6mdLbriAJbqhX7+nM/B71lgw2+91q3LfhU=",
|
|
AllowedIps: []string{"100.64.0.21/24"},
|
|
SshConfig: &mgmtProto.SSHConfig{
|
|
SshPubKey: []byte("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFATYCqaQw/9id1Qkq3n16JYhDhXraI6Pc1fgB8ynEfQ"),
|
|
},
|
|
}
|
|
|
|
// SSH server is not enabled so SSH config of a remote peer should be ignored
|
|
networkMap := &mgmtProto.NetworkMap{
|
|
Serial: 6,
|
|
PeerConfig: nil,
|
|
RemotePeers: []*mgmtProto.RemotePeerConfig{peerWithSSH},
|
|
RemotePeersIsEmpty: false,
|
|
}
|
|
|
|
err = engine.updateNetworkMap(networkMap)
|
|
require.NoError(t, err)
|
|
|
|
assert.Nil(t, engine.sshServer)
|
|
|
|
// SSH server is enabled, therefore SSH config should be applied
|
|
networkMap = &mgmtProto.NetworkMap{
|
|
Serial: 7,
|
|
PeerConfig: &mgmtProto.PeerConfig{Address: "100.64.0.1/24",
|
|
SshConfig: &mgmtProto.SSHConfig{
|
|
SshEnabled: true,
|
|
JwtConfig: &mgmtProto.JWTConfig{
|
|
Issuer: "test-issuer",
|
|
Audience: "test-audience",
|
|
KeysLocation: "test-keys",
|
|
MaxTokenAge: 3600,
|
|
},
|
|
}},
|
|
RemotePeers: []*mgmtProto.RemotePeerConfig{peerWithSSH},
|
|
RemotePeersIsEmpty: false,
|
|
}
|
|
|
|
err = engine.updateNetworkMap(networkMap)
|
|
require.NoError(t, err)
|
|
|
|
time.Sleep(250 * time.Millisecond)
|
|
assert.NotNil(t, engine.sshServer)
|
|
|
|
// now remove peer
|
|
networkMap = &mgmtProto.NetworkMap{
|
|
Serial: 8,
|
|
RemotePeers: []*mgmtProto.RemotePeerConfig{},
|
|
RemotePeersIsEmpty: false,
|
|
}
|
|
|
|
err = engine.updateNetworkMap(networkMap)
|
|
require.NoError(t, err)
|
|
|
|
// time.Sleep(250 * time.Millisecond)
|
|
assert.NotNil(t, engine.sshServer)
|
|
|
|
// now disable SSH server
|
|
networkMap = &mgmtProto.NetworkMap{
|
|
Serial: 9,
|
|
PeerConfig: &mgmtProto.PeerConfig{Address: "100.64.0.1/24",
|
|
SshConfig: &mgmtProto.SSHConfig{SshEnabled: false}},
|
|
RemotePeers: []*mgmtProto.RemotePeerConfig{peerWithSSH},
|
|
RemotePeersIsEmpty: false,
|
|
}
|
|
|
|
err = engine.updateNetworkMap(networkMap)
|
|
require.NoError(t, err)
|
|
|
|
assert.Nil(t, engine.sshServer)
|
|
}
|
|
|
|
func TestEngine_SSHUpdateLogic(t *testing.T) {
|
|
// Test that SSH server start/stop logic works based on config
|
|
engine := &Engine{
|
|
config: &EngineConfig{
|
|
ServerSSHAllowed: false, // Start with SSH disabled
|
|
},
|
|
syncMsgMux: &sync.Mutex{},
|
|
}
|
|
|
|
// Test SSH disabled config
|
|
sshConfig := &mgmtProto.SSHConfig{SshEnabled: false}
|
|
err := engine.updateSSH(sshConfig)
|
|
assert.NoError(t, err)
|
|
assert.Nil(t, engine.sshServer)
|
|
|
|
// Test inbound blocked
|
|
engine.config.BlockInbound = true
|
|
err = engine.updateSSH(&mgmtProto.SSHConfig{SshEnabled: true})
|
|
assert.NoError(t, err)
|
|
assert.Nil(t, engine.sshServer)
|
|
engine.config.BlockInbound = false
|
|
|
|
// Test with server SSH not allowed
|
|
err = engine.updateSSH(&mgmtProto.SSHConfig{SshEnabled: true})
|
|
assert.NoError(t, err)
|
|
assert.Nil(t, engine.sshServer)
|
|
}
|
|
|
|
func TestEngine_SSHServerConsistency(t *testing.T) {
|
|
|
|
t.Run("server set only on successful creation", func(t *testing.T) {
|
|
engine := &Engine{
|
|
config: &EngineConfig{
|
|
ServerSSHAllowed: true,
|
|
SSHKey: []byte("test-key"),
|
|
},
|
|
syncMsgMux: &sync.Mutex{},
|
|
}
|
|
|
|
engine.wgInterface = nil
|
|
|
|
err := engine.updateSSH(&mgmtProto.SSHConfig{SshEnabled: true})
|
|
|
|
assert.Error(t, err)
|
|
assert.Nil(t, engine.sshServer)
|
|
})
|
|
|
|
t.Run("cleanup handles nil gracefully", func(t *testing.T) {
|
|
engine := &Engine{
|
|
config: &EngineConfig{
|
|
ServerSSHAllowed: false,
|
|
},
|
|
syncMsgMux: &sync.Mutex{},
|
|
}
|
|
|
|
err := engine.stopSSHServer()
|
|
assert.NoError(t, err)
|
|
assert.Nil(t, engine.sshServer)
|
|
})
|
|
}
|
|
|
|
func TestEngine_UpdateNetworkMap(t *testing.T) {
|
|
// test setup
|
|
key, err := wgtypes.GeneratePrivateKey()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
return
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
relayMgr := relayClient.NewManager(ctx, nil, key.PublicKey().String(), iface.DefaultMTU)
|
|
engine := NewEngine(ctx, cancel, &EngineConfig{
|
|
WgIfaceName: "utun102",
|
|
WgAddr: "100.64.0.1/24",
|
|
WgPrivateKey: key,
|
|
WgPort: 33100,
|
|
MTU: iface.DefaultMTU,
|
|
}, EngineServices{
|
|
SignalClient: &signal.MockClient{},
|
|
MgmClient: &mgmt.MockClient{},
|
|
RelayManager: relayMgr,
|
|
StatusRecorder: peer.NewRecorder("https://mgm"),
|
|
}, MobileDependency{})
|
|
|
|
wgIface := &MockWGIface{
|
|
NameFunc: func() string { return "utun102" },
|
|
RemovePeerFunc: func(peerKey string) error {
|
|
return nil
|
|
},
|
|
AddressFunc: func() wgaddr.Address {
|
|
return wgaddr.Address{
|
|
IP: netip.MustParseAddr("10.20.0.1"),
|
|
Network: netip.MustParsePrefix("10.20.0.0/24"),
|
|
}
|
|
},
|
|
UpdatePeerFunc: func(peerKey string, allowedIps []netip.Prefix, keepAlive time.Duration, endpoint *net.UDPAddr, preSharedKey *wgtypes.Key) error {
|
|
return nil
|
|
},
|
|
}
|
|
engine.wgInterface = wgIface
|
|
engine.routeManager = routemanager.NewManager(routemanager.ManagerConfig{
|
|
Context: ctx,
|
|
PublicKey: key.PublicKey().String(),
|
|
DNSRouteInterval: time.Minute,
|
|
WGInterface: engine.wgInterface,
|
|
StatusRecorder: engine.statusRecorder,
|
|
RelayManager: relayMgr,
|
|
})
|
|
err = engine.routeManager.Init()
|
|
require.NoError(t, err)
|
|
engine.dnsServer = &dns.MockServer{
|
|
UpdateDNSServerFunc: func(serial uint64, update nbdns.Config) error { return nil },
|
|
}
|
|
conn, err := net.ListenUDP("udp4", nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
engine.udpMux = udpmux.NewUniversalUDPMuxDefault(udpmux.UniversalUDPMuxParams{UDPConn: conn, MTU: 1280})
|
|
engine.ctx = ctx
|
|
engine.srWatcher = guard.NewSRWatcher(nil, nil, nil, icemaker.Config{})
|
|
engine.connMgr = NewConnMgr(engine.config, engine.statusRecorder, engine.peerStore, wgIface)
|
|
engine.connMgr.Start(ctx)
|
|
|
|
type testCase struct {
|
|
name string
|
|
networkMap *mgmtProto.NetworkMap
|
|
|
|
expectedLen int
|
|
expectedPeers []*mgmtProto.RemotePeerConfig
|
|
expectedSerial uint64
|
|
}
|
|
|
|
peer1 := &mgmtProto.RemotePeerConfig{
|
|
WgPubKey: "RRHf3Ma6z6mdLbriAJbqhX7+nM/B71lgw2+91q3LfhU=",
|
|
AllowedIps: []string{"100.64.0.10/24"},
|
|
}
|
|
|
|
peer2 := &mgmtProto.RemotePeerConfig{
|
|
WgPubKey: "LLHf3Ma6z6mdLbriAJbqhX7+nM/B71lgw2+91q3LfhU=",
|
|
AllowedIps: []string{"100.64.0.11/24"},
|
|
}
|
|
|
|
peer3 := &mgmtProto.RemotePeerConfig{
|
|
WgPubKey: "GGHf3Ma6z6mdLbriAJbqhX7+nM/B71lgw2+91q3LfhU=",
|
|
AllowedIps: []string{"100.64.0.12/24"},
|
|
}
|
|
|
|
modifiedPeer3 := &mgmtProto.RemotePeerConfig{
|
|
WgPubKey: "GGHf3Ma6z6mdLbriAJbqhX7+nM/B71lgw2+91q3LfhU=",
|
|
AllowedIps: []string{"100.64.0.20/24"},
|
|
}
|
|
|
|
case1 := testCase{
|
|
name: "input with a new peer to add",
|
|
networkMap: &mgmtProto.NetworkMap{
|
|
Serial: 1,
|
|
PeerConfig: nil,
|
|
RemotePeers: []*mgmtProto.RemotePeerConfig{
|
|
peer1,
|
|
},
|
|
RemotePeersIsEmpty: false,
|
|
},
|
|
expectedLen: 1,
|
|
expectedPeers: []*mgmtProto.RemotePeerConfig{peer1},
|
|
expectedSerial: 1,
|
|
}
|
|
|
|
// 2nd case - one extra peer added and network map has CurrentSerial grater than local => apply the update
|
|
case2 := testCase{
|
|
name: "input with an old peer and a new peer to add",
|
|
networkMap: &mgmtProto.NetworkMap{
|
|
Serial: 2,
|
|
PeerConfig: nil,
|
|
RemotePeers: []*mgmtProto.RemotePeerConfig{
|
|
peer1, peer2,
|
|
},
|
|
RemotePeersIsEmpty: false,
|
|
},
|
|
expectedLen: 2,
|
|
expectedPeers: []*mgmtProto.RemotePeerConfig{peer1, peer2},
|
|
expectedSerial: 2,
|
|
}
|
|
|
|
case3 := testCase{
|
|
name: "input with outdated (old) update to ignore",
|
|
networkMap: &mgmtProto.NetworkMap{
|
|
Serial: 0,
|
|
PeerConfig: nil,
|
|
RemotePeers: []*mgmtProto.RemotePeerConfig{
|
|
peer1, peer2, peer3,
|
|
},
|
|
RemotePeersIsEmpty: false,
|
|
},
|
|
expectedLen: 2,
|
|
expectedPeers: []*mgmtProto.RemotePeerConfig{peer1, peer2},
|
|
expectedSerial: 2,
|
|
}
|
|
|
|
case4 := testCase{
|
|
name: "input with one peer to remove and one new to add",
|
|
networkMap: &mgmtProto.NetworkMap{
|
|
Serial: 4,
|
|
PeerConfig: nil,
|
|
RemotePeers: []*mgmtProto.RemotePeerConfig{
|
|
peer2, peer3,
|
|
},
|
|
RemotePeersIsEmpty: false,
|
|
},
|
|
expectedLen: 2,
|
|
expectedPeers: []*mgmtProto.RemotePeerConfig{peer2, peer3},
|
|
expectedSerial: 4,
|
|
}
|
|
|
|
case5 := testCase{
|
|
name: "input with one peer to modify",
|
|
networkMap: &mgmtProto.NetworkMap{
|
|
Serial: 4,
|
|
PeerConfig: nil,
|
|
RemotePeers: []*mgmtProto.RemotePeerConfig{
|
|
modifiedPeer3, peer2,
|
|
},
|
|
RemotePeersIsEmpty: false,
|
|
},
|
|
expectedLen: 2,
|
|
expectedPeers: []*mgmtProto.RemotePeerConfig{peer2, modifiedPeer3},
|
|
expectedSerial: 4,
|
|
}
|
|
|
|
case6 := testCase{
|
|
name: "input with all peers to remove",
|
|
networkMap: &mgmtProto.NetworkMap{
|
|
Serial: 5,
|
|
PeerConfig: nil,
|
|
RemotePeers: []*mgmtProto.RemotePeerConfig{},
|
|
RemotePeersIsEmpty: true,
|
|
},
|
|
expectedLen: 0,
|
|
expectedPeers: nil,
|
|
expectedSerial: 5,
|
|
}
|
|
|
|
for _, c := range []testCase{case1, case2, case3, case4, case5, case6} {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
err = engine.updateNetworkMap(c.networkMap)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
return
|
|
}
|
|
|
|
if len(engine.peerStore.PeersPubKey()) != c.expectedLen {
|
|
t.Errorf("expecting Engine.peerConns to be of size %d, got %d", c.expectedLen, len(engine.peerStore.PeersPubKey()))
|
|
}
|
|
|
|
if engine.networkSerial != c.expectedSerial {
|
|
t.Errorf("expecting Engine.networkSerial to be equal to %d, actual %d", c.expectedSerial, engine.networkSerial)
|
|
}
|
|
|
|
for _, p := range c.expectedPeers {
|
|
conn, ok := engine.peerStore.PeerConn(p.GetWgPubKey())
|
|
if !ok {
|
|
t.Errorf("expecting Engine.peerConns to contain peer %s", p)
|
|
}
|
|
expectedAllowedIPs := strings.Join(p.AllowedIps, ",")
|
|
if !compareNetIPLists(conn.WgConfig().AllowedIps, p.AllowedIps) {
|
|
t.Errorf("expecting peer %s to have AllowedIPs= %s, got %s", p.GetWgPubKey(),
|
|
expectedAllowedIPs, conn.WgConfig().AllowedIps)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEngine_Sync(t *testing.T) {
|
|
key, err := wgtypes.GeneratePrivateKey()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
return
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
// feed updates to Engine via mocked Management client
|
|
updates := make(chan *mgmtProto.SyncResponse)
|
|
defer close(updates)
|
|
syncFunc := func(ctx context.Context, info *system.Info, msgHandler func(msg *mgmtProto.SyncResponse) error) error {
|
|
for msg := range updates {
|
|
err := msgHandler(msg)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
relayMgr := relayClient.NewManager(ctx, nil, key.PublicKey().String(), iface.DefaultMTU)
|
|
engine := NewEngine(ctx, cancel, &EngineConfig{
|
|
WgIfaceName: "utun103",
|
|
WgAddr: "100.64.0.1/24",
|
|
WgPrivateKey: key,
|
|
WgPort: 33100,
|
|
MTU: iface.DefaultMTU,
|
|
}, EngineServices{
|
|
SignalClient: &signal.MockClient{},
|
|
MgmClient: &mgmt.MockClient{SyncFunc: syncFunc},
|
|
RelayManager: relayMgr,
|
|
StatusRecorder: peer.NewRecorder("https://mgm"),
|
|
}, MobileDependency{})
|
|
engine.ctx = ctx
|
|
|
|
engine.dnsServer = &dns.MockServer{
|
|
UpdateDNSServerFunc: func(serial uint64, update nbdns.Config) error { return nil },
|
|
}
|
|
|
|
defer func() {
|
|
err := engine.Stop()
|
|
if err != nil {
|
|
return
|
|
}
|
|
}()
|
|
|
|
err = engine.Start(nil, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
return
|
|
}
|
|
|
|
peer1 := &mgmtProto.RemotePeerConfig{
|
|
WgPubKey: "RRHf3Ma6z6mdLbriAJbqhX7+nM/B71lgw2+91q3LfhU=",
|
|
AllowedIps: []string{"100.64.0.10/24"},
|
|
}
|
|
peer2 := &mgmtProto.RemotePeerConfig{
|
|
WgPubKey: "LLHf3Ma6z6mdLbriAJbqhX9+nM/B71lgw2+91q3LlhU=",
|
|
AllowedIps: []string{"100.64.0.11/24"},
|
|
}
|
|
peer3 := &mgmtProto.RemotePeerConfig{
|
|
WgPubKey: "GGHf3Ma6z6mdLbriAJbqhX9+nM/B71lgw2+91q3LlhU=",
|
|
AllowedIps: []string{"100.64.0.12/24"},
|
|
}
|
|
// 1st update with just 1 peer and serial larger than the current serial of the engine => apply update
|
|
updates <- &mgmtProto.SyncResponse{
|
|
NetworkMap: &mgmtProto.NetworkMap{
|
|
Serial: 10,
|
|
PeerConfig: nil,
|
|
RemotePeers: []*mgmtProto.RemotePeerConfig{peer1, peer2, peer3},
|
|
RemotePeersIsEmpty: false,
|
|
},
|
|
}
|
|
|
|
timeout := time.After(time.Second * 2)
|
|
for {
|
|
select {
|
|
case <-timeout:
|
|
t.Fatalf("timeout while waiting for test to finish")
|
|
return
|
|
default:
|
|
}
|
|
|
|
if getPeers(engine) == 3 && engine.networkSerial == 10 {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEngine_UpdateNetworkMapWithRoutes(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
inputErr error
|
|
networkMap *mgmtProto.NetworkMap
|
|
expectedLen int
|
|
expectedClientRoutes route.HAMap
|
|
expectedSerial uint64
|
|
}{
|
|
{
|
|
name: "Routes Config Should Be Passed To Manager",
|
|
networkMap: &mgmtProto.NetworkMap{
|
|
Serial: 1,
|
|
PeerConfig: nil,
|
|
RemotePeersIsEmpty: false,
|
|
Routes: []*mgmtProto.Route{
|
|
{
|
|
ID: "a",
|
|
Network: "192.168.0.0/24",
|
|
NetID: "n1",
|
|
Peer: "p1",
|
|
NetworkType: 1,
|
|
Masquerade: false,
|
|
},
|
|
{
|
|
ID: "b",
|
|
Network: "192.168.1.0/24",
|
|
NetID: "n2",
|
|
Peer: "p1",
|
|
NetworkType: 1,
|
|
Masquerade: false,
|
|
},
|
|
},
|
|
},
|
|
expectedLen: 2,
|
|
expectedClientRoutes: route.HAMap{
|
|
"n1|192.168.0.0/24": []*route.Route{
|
|
{
|
|
ID: "a",
|
|
Network: netip.MustParsePrefix("192.168.0.0/24"),
|
|
NetID: "n1",
|
|
Peer: "p1",
|
|
NetworkType: 1,
|
|
Masquerade: false,
|
|
},
|
|
},
|
|
"n2|192.168.1.0/24": []*route.Route{
|
|
{
|
|
ID: "b",
|
|
Network: netip.MustParsePrefix("192.168.1.0/24"),
|
|
NetID: "n2",
|
|
Peer: "p1",
|
|
NetworkType: 1,
|
|
Masquerade: false,
|
|
},
|
|
},
|
|
},
|
|
expectedSerial: 1,
|
|
},
|
|
{
|
|
name: "Empty Routes Config Should Be Passed",
|
|
networkMap: &mgmtProto.NetworkMap{
|
|
Serial: 1,
|
|
PeerConfig: nil,
|
|
RemotePeersIsEmpty: false,
|
|
Routes: nil,
|
|
},
|
|
expectedLen: 0,
|
|
expectedClientRoutes: nil,
|
|
expectedSerial: 1,
|
|
},
|
|
{
|
|
name: "Error Shouldn't Break Engine",
|
|
inputErr: fmt.Errorf("mocking error"),
|
|
networkMap: &mgmtProto.NetworkMap{
|
|
Serial: 1,
|
|
PeerConfig: nil,
|
|
RemotePeersIsEmpty: false,
|
|
Routes: nil,
|
|
},
|
|
expectedLen: 0,
|
|
expectedClientRoutes: nil,
|
|
expectedSerial: 1,
|
|
},
|
|
}
|
|
|
|
for n, testCase := range testCases {
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
// test setup
|
|
key, err := wgtypes.GeneratePrivateKey()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
return
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
wgIfaceName := fmt.Sprintf("utun%d", 104+n)
|
|
wgAddr := fmt.Sprintf("100.66.%d.1/24", n)
|
|
|
|
relayMgr := relayClient.NewManager(ctx, nil, key.PublicKey().String(), iface.DefaultMTU)
|
|
engine := NewEngine(ctx, cancel, &EngineConfig{
|
|
WgIfaceName: wgIfaceName,
|
|
WgAddr: wgAddr,
|
|
WgPrivateKey: key,
|
|
WgPort: 33100,
|
|
MTU: iface.DefaultMTU,
|
|
}, EngineServices{
|
|
SignalClient: &signal.MockClient{},
|
|
MgmClient: &mgmt.MockClient{},
|
|
RelayManager: relayMgr,
|
|
StatusRecorder: peer.NewRecorder("https://mgm"),
|
|
}, MobileDependency{})
|
|
engine.ctx = ctx
|
|
newNet, err := stdnet.NewNet(context.Background(), nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
opts := iface.WGIFaceOpts{
|
|
IFaceName: wgIfaceName,
|
|
Address: wgAddr,
|
|
WGPort: engine.config.WgPort,
|
|
WGPrivKey: key.String(),
|
|
MTU: iface.DefaultMTU,
|
|
TransportNet: newNet,
|
|
}
|
|
engine.wgInterface, err = iface.NewWGIFace(opts)
|
|
assert.NoError(t, err, "shouldn't return error")
|
|
input := struct {
|
|
inputSerial uint64
|
|
clientRoutes route.HAMap
|
|
}{}
|
|
|
|
mockRouteManager := &routemanager.MockManager{
|
|
UpdateRoutesFunc: func(updateSerial uint64, serverRoutes map[route.ID]*route.Route, clientRoutes route.HAMap, useNewDNSRoute bool) error {
|
|
input.inputSerial = updateSerial
|
|
input.clientRoutes = clientRoutes
|
|
return testCase.inputErr
|
|
},
|
|
ClassifyRoutesFunc: func(newRoutes []*route.Route) (map[route.ID]*route.Route, route.HAMap) {
|
|
if len(newRoutes) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
// Classify all routes as client routes (not matching our public key)
|
|
clientRoutes := make(route.HAMap)
|
|
for _, r := range newRoutes {
|
|
haID := r.GetHAUniqueID()
|
|
clientRoutes[haID] = append(clientRoutes[haID], r)
|
|
}
|
|
return nil, clientRoutes
|
|
},
|
|
}
|
|
|
|
engine.routeManager = mockRouteManager
|
|
engine.dnsServer = &dns.MockServer{}
|
|
engine.connMgr = NewConnMgr(engine.config, engine.statusRecorder, engine.peerStore, engine.wgInterface)
|
|
engine.connMgr.Start(ctx)
|
|
|
|
defer func() {
|
|
exitErr := engine.Stop()
|
|
if exitErr != nil {
|
|
return
|
|
}
|
|
}()
|
|
|
|
err = engine.updateNetworkMap(testCase.networkMap)
|
|
assert.NoError(t, err, "shouldn't return error")
|
|
assert.Equal(t, testCase.expectedSerial, input.inputSerial, "serial should match")
|
|
assert.Len(t, input.clientRoutes, testCase.expectedLen, "clientRoutes len should match")
|
|
assert.Equal(t, testCase.expectedClientRoutes, input.clientRoutes, "clientRoutes should match")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEngine_UpdateNetworkMapWithDNSUpdate(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
inputErr error
|
|
networkMap *mgmtProto.NetworkMap
|
|
expectedZonesLen int
|
|
expectedZones []nbdns.CustomZone
|
|
expectedNSGroupsLen int
|
|
expectedNSGroups []*nbdns.NameServerGroup
|
|
expectedSerial uint64
|
|
}{
|
|
{
|
|
name: "DNS Config Should Be Passed To DNS Server",
|
|
networkMap: &mgmtProto.NetworkMap{
|
|
Serial: 1,
|
|
PeerConfig: nil,
|
|
RemotePeersIsEmpty: false,
|
|
Routes: nil,
|
|
DNSConfig: &mgmtProto.DNSConfig{
|
|
ServiceEnable: true,
|
|
CustomZones: []*mgmtProto.CustomZone{
|
|
{
|
|
Domain: "netbird.cloud.",
|
|
Records: []*mgmtProto.SimpleRecord{
|
|
{
|
|
Name: "peer-a.netbird.cloud.",
|
|
Type: 1,
|
|
Class: nbdns.DefaultClass,
|
|
TTL: 300,
|
|
RData: "100.64.0.1",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Domain: "0.66.100.in-addr.arpa.",
|
|
},
|
|
},
|
|
NameServerGroups: []*mgmtProto.NameServerGroup{
|
|
{
|
|
Primary: true,
|
|
NameServers: []*mgmtProto.NameServer{
|
|
{
|
|
IP: "8.8.8.8",
|
|
NSType: 1,
|
|
Port: 53,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectedZonesLen: 1,
|
|
expectedZones: []nbdns.CustomZone{
|
|
{
|
|
Domain: "netbird.cloud.",
|
|
Records: []nbdns.SimpleRecord{
|
|
{
|
|
Name: "peer-a.netbird.cloud.",
|
|
Type: 1,
|
|
Class: nbdns.DefaultClass,
|
|
TTL: 300,
|
|
RData: "100.64.0.1",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Domain: "0.66.100.in-addr.arpa.",
|
|
},
|
|
},
|
|
expectedNSGroupsLen: 1,
|
|
expectedNSGroups: []*nbdns.NameServerGroup{
|
|
{
|
|
Primary: true,
|
|
NameServers: []nbdns.NameServer{
|
|
{
|
|
IP: netip.MustParseAddr("8.8.8.8"),
|
|
NSType: 1,
|
|
Port: 53,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectedSerial: 1,
|
|
},
|
|
{
|
|
name: "Empty DNS Config Should Be OK",
|
|
networkMap: &mgmtProto.NetworkMap{
|
|
Serial: 1,
|
|
PeerConfig: nil,
|
|
RemotePeersIsEmpty: false,
|
|
Routes: nil,
|
|
DNSConfig: nil,
|
|
},
|
|
expectedZonesLen: 0,
|
|
expectedZones: []nbdns.CustomZone{},
|
|
expectedNSGroupsLen: 0,
|
|
expectedNSGroups: []*nbdns.NameServerGroup{},
|
|
expectedSerial: 1,
|
|
},
|
|
{
|
|
name: "Error Shouldn't Break Engine",
|
|
inputErr: fmt.Errorf("mocking error"),
|
|
networkMap: &mgmtProto.NetworkMap{
|
|
Serial: 1,
|
|
PeerConfig: nil,
|
|
RemotePeersIsEmpty: false,
|
|
Routes: nil,
|
|
},
|
|
expectedZonesLen: 0,
|
|
expectedZones: []nbdns.CustomZone{},
|
|
expectedNSGroupsLen: 0,
|
|
expectedNSGroups: []*nbdns.NameServerGroup{},
|
|
expectedSerial: 1,
|
|
},
|
|
}
|
|
|
|
for n, testCase := range testCases {
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
// test setup
|
|
key, err := wgtypes.GeneratePrivateKey()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
return
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
wgIfaceName := fmt.Sprintf("utun%d", 104+n)
|
|
wgAddr := fmt.Sprintf("100.66.%d.1/24", n)
|
|
|
|
relayMgr := relayClient.NewManager(ctx, nil, key.PublicKey().String(), iface.DefaultMTU)
|
|
engine := NewEngine(ctx, cancel, &EngineConfig{
|
|
WgIfaceName: wgIfaceName,
|
|
WgAddr: wgAddr,
|
|
WgPrivateKey: key,
|
|
WgPort: 33100,
|
|
MTU: iface.DefaultMTU,
|
|
}, EngineServices{
|
|
SignalClient: &signal.MockClient{},
|
|
MgmClient: &mgmt.MockClient{},
|
|
RelayManager: relayMgr,
|
|
StatusRecorder: peer.NewRecorder("https://mgm"),
|
|
}, MobileDependency{})
|
|
engine.ctx = ctx
|
|
|
|
newNet, err := stdnet.NewNet(context.Background(), nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
opts := iface.WGIFaceOpts{
|
|
IFaceName: wgIfaceName,
|
|
Address: wgAddr,
|
|
WGPort: 33100,
|
|
WGPrivKey: key.String(),
|
|
MTU: iface.DefaultMTU,
|
|
TransportNet: newNet,
|
|
}
|
|
engine.wgInterface, err = iface.NewWGIFace(opts)
|
|
assert.NoError(t, err, "shouldn't return error")
|
|
|
|
mockRouteManager := &routemanager.MockManager{
|
|
UpdateRoutesFunc: func(updateSerial uint64, serverRoutes map[route.ID]*route.Route, clientRoutes route.HAMap, useNewDNSRoute bool) error {
|
|
return nil
|
|
},
|
|
}
|
|
|
|
engine.routeManager = mockRouteManager
|
|
|
|
input := struct {
|
|
inputSerial uint64
|
|
inputNSGroups []*nbdns.NameServerGroup
|
|
inputZones []nbdns.CustomZone
|
|
}{}
|
|
|
|
mockDNSServer := &dns.MockServer{
|
|
UpdateDNSServerFunc: func(serial uint64, update nbdns.Config) error {
|
|
input.inputSerial = serial
|
|
input.inputZones = update.CustomZones
|
|
input.inputNSGroups = update.NameServerGroups
|
|
return testCase.inputErr
|
|
},
|
|
}
|
|
|
|
engine.dnsServer = mockDNSServer
|
|
engine.connMgr = NewConnMgr(engine.config, engine.statusRecorder, engine.peerStore, engine.wgInterface)
|
|
engine.connMgr.Start(ctx)
|
|
|
|
defer func() {
|
|
exitErr := engine.Stop()
|
|
if exitErr != nil {
|
|
return
|
|
}
|
|
}()
|
|
|
|
err = engine.updateNetworkMap(testCase.networkMap)
|
|
assert.NoError(t, err, "shouldn't return error")
|
|
assert.Equal(t, testCase.expectedSerial, input.inputSerial, "serial should match")
|
|
assert.Len(t, input.inputNSGroups, testCase.expectedZonesLen, "zones len should match")
|
|
assert.Equal(t, testCase.expectedZones, input.inputZones, "custom zones should match")
|
|
assert.Len(t, input.inputNSGroups, testCase.expectedNSGroupsLen, "ns groups len should match")
|
|
assert.Equal(t, testCase.expectedNSGroups, input.inputNSGroups, "ns groups should match")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEngine_MultiplePeers(t *testing.T) {
|
|
// log.SetLevel(log.DebugLevel)
|
|
|
|
ctx, cancel := context.WithCancel(CtxInitState(context.Background()))
|
|
defer cancel()
|
|
|
|
sigServer, signalAddr, err := startSignal(t)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
return
|
|
}
|
|
defer sigServer.Stop()
|
|
mgmtServer, mgmtAddr, err := startManagement(t, t.TempDir(), "../testdata/store.sql")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
return
|
|
}
|
|
defer mgmtServer.GracefulStop()
|
|
|
|
setupKey := "A2C8E62B-38F5-4553-B31E-DD66C696CEBB"
|
|
|
|
mu := sync.Mutex{}
|
|
engines := []*Engine{}
|
|
numPeers := 10
|
|
wg := sync.WaitGroup{}
|
|
wg.Add(numPeers)
|
|
// create and start peers
|
|
for i := 0; i < numPeers; i++ {
|
|
j := i
|
|
go func() {
|
|
engine, err := createEngine(ctx, cancel, setupKey, j, mgmtAddr, signalAddr)
|
|
if err != nil {
|
|
wg.Done()
|
|
t.Errorf("unable to create the engine for peer %d with error %v", j, err)
|
|
return
|
|
}
|
|
engine.dnsServer = &dns.MockServer{}
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
guid := fmt.Sprintf("{%s}", uuid.New().String())
|
|
device.CustomWindowsGUIDString = strings.ToLower(guid)
|
|
err = engine.Start(nil, nil)
|
|
if err != nil {
|
|
t.Errorf("unable to start engine for peer %d with error %v", j, err)
|
|
wg.Done()
|
|
return
|
|
}
|
|
engines = append(engines, engine)
|
|
wg.Done()
|
|
}()
|
|
}
|
|
|
|
// wait until all have been created and started
|
|
wg.Wait()
|
|
if len(engines) != numPeers {
|
|
t.Fatal("not all peers was started")
|
|
}
|
|
// check whether all the peer have expected peers connected
|
|
|
|
expectedConnected := numPeers * (numPeers - 1)
|
|
|
|
// adjust according to timeouts
|
|
timeout := 50 * time.Second
|
|
timeoutChan := time.After(timeout)
|
|
ticker := time.NewTicker(time.Second)
|
|
defer ticker.Stop()
|
|
loop:
|
|
for {
|
|
select {
|
|
case <-timeoutChan:
|
|
t.Fatalf("waiting for expected connections timeout after %s", timeout.String())
|
|
break loop
|
|
case <-ticker.C:
|
|
totalConnected := 0
|
|
for _, engine := range engines {
|
|
totalConnected += getConnectedPeers(engine)
|
|
}
|
|
if totalConnected == expectedConnected {
|
|
log.Infof("total connected=%d", totalConnected)
|
|
break loop
|
|
}
|
|
log.Infof("total connected=%d", totalConnected)
|
|
}
|
|
}
|
|
// cleanup test
|
|
for n, peerEngine := range engines {
|
|
t.Logf("stopping peer with interface %s from multipeer test, loopIndex %d", peerEngine.wgInterface.Name(), n)
|
|
errStop := peerEngine.mgmClient.Close()
|
|
if errStop != nil {
|
|
log.Infoln("got error trying to close management clients from engine: ", errStop)
|
|
}
|
|
errStop = peerEngine.Stop()
|
|
if errStop != nil {
|
|
log.Infoln("got error trying to close testing peers engine: ", errStop)
|
|
}
|
|
}
|
|
}
|
|
|
|
func Test_ParseNATExternalIPMappings(t *testing.T) {
|
|
ifaceList, err := net.Interfaces()
|
|
if err != nil {
|
|
t.Fatalf("could get the interface list, got error: %s", err)
|
|
}
|
|
|
|
var testingIP string
|
|
var testingInterface string
|
|
|
|
for _, iface := range ifaceList {
|
|
addrList, err := iface.Addrs()
|
|
if err != nil {
|
|
t.Fatalf("could get the addr list, got error: %s", err)
|
|
}
|
|
for _, addr := range addrList {
|
|
prefix := netip.MustParsePrefix(addr.String())
|
|
if prefix.Addr().Is4() && !prefix.Addr().IsLoopback() {
|
|
testingIP = prefix.Addr().String()
|
|
testingInterface = iface.Name
|
|
}
|
|
}
|
|
}
|
|
|
|
testCases := []struct {
|
|
name string
|
|
inputMapList []string
|
|
inputBlacklistInterface []string
|
|
expectedOutput []string
|
|
}{
|
|
{
|
|
name: "Parse Valid List Should Be OK",
|
|
inputBlacklistInterface: profilemanager.DefaultInterfaceBlacklist,
|
|
inputMapList: []string{"1.1.1.1", "8.8.8.8/" + testingInterface},
|
|
expectedOutput: []string{"1.1.1.1", "8.8.8.8/" + testingIP},
|
|
},
|
|
{
|
|
name: "Only Interface name Should Return Nil",
|
|
inputBlacklistInterface: profilemanager.DefaultInterfaceBlacklist,
|
|
inputMapList: []string{testingInterface},
|
|
expectedOutput: nil,
|
|
},
|
|
{
|
|
name: "Invalid IP Return Nil",
|
|
inputBlacklistInterface: profilemanager.DefaultInterfaceBlacklist,
|
|
inputMapList: []string{"1.1.1.1000"},
|
|
expectedOutput: nil,
|
|
},
|
|
{
|
|
name: "Invalid Mapping Element Should return Nil",
|
|
inputBlacklistInterface: profilemanager.DefaultInterfaceBlacklist,
|
|
inputMapList: []string{"1.1.1.1/10.10.10.1/eth0"},
|
|
expectedOutput: nil,
|
|
},
|
|
}
|
|
for _, testCase := range testCases {
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
engine := &Engine{
|
|
config: &EngineConfig{
|
|
IFaceBlackList: testCase.inputBlacklistInterface,
|
|
NATExternalIPs: testCase.inputMapList,
|
|
MTU: iface.DefaultMTU,
|
|
},
|
|
}
|
|
parsedList := engine.parseNATExternalIPMappings()
|
|
require.ElementsMatchf(t, testCase.expectedOutput, parsedList, "elements of parsed list should match expected list")
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_CheckFilesEqual(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
inputChecks1 []*mgmtProto.Checks
|
|
inputChecks2 []*mgmtProto.Checks
|
|
expectedBool bool
|
|
}{
|
|
{
|
|
name: "Equal Files In Equal Order Should Return True",
|
|
inputChecks1: []*mgmtProto.Checks{
|
|
{
|
|
Files: []string{
|
|
"testfile1",
|
|
"testfile2",
|
|
},
|
|
},
|
|
},
|
|
inputChecks2: []*mgmtProto.Checks{
|
|
{
|
|
Files: []string{
|
|
"testfile1",
|
|
"testfile2",
|
|
},
|
|
},
|
|
},
|
|
expectedBool: true,
|
|
},
|
|
{
|
|
name: "Equal Files In Reverse Order Should Return True",
|
|
inputChecks1: []*mgmtProto.Checks{
|
|
{
|
|
Files: []string{
|
|
"testfile1",
|
|
"testfile2",
|
|
},
|
|
},
|
|
},
|
|
inputChecks2: []*mgmtProto.Checks{
|
|
{
|
|
Files: []string{
|
|
"testfile2",
|
|
"testfile1",
|
|
},
|
|
},
|
|
},
|
|
expectedBool: true,
|
|
},
|
|
{
|
|
name: "Unequal Files Should Return False",
|
|
inputChecks1: []*mgmtProto.Checks{
|
|
{
|
|
Files: []string{
|
|
"testfile1",
|
|
"testfile2",
|
|
},
|
|
},
|
|
},
|
|
inputChecks2: []*mgmtProto.Checks{
|
|
{
|
|
Files: []string{
|
|
"testfile1",
|
|
"testfile3",
|
|
},
|
|
},
|
|
},
|
|
expectedBool: false,
|
|
},
|
|
{
|
|
name: "Compared With Empty Should Return False",
|
|
inputChecks1: []*mgmtProto.Checks{
|
|
{
|
|
Files: []string{
|
|
"testfile1",
|
|
"testfile2",
|
|
},
|
|
},
|
|
},
|
|
inputChecks2: []*mgmtProto.Checks{
|
|
{
|
|
Files: []string{},
|
|
},
|
|
},
|
|
expectedBool: false,
|
|
},
|
|
{
|
|
name: "Compared Slices with same files but different order should return true",
|
|
inputChecks1: []*mgmtProto.Checks{
|
|
{
|
|
Files: []string{
|
|
"testfile1",
|
|
"testfile2",
|
|
},
|
|
},
|
|
{
|
|
Files: []string{
|
|
"testfile4",
|
|
"testfile3",
|
|
},
|
|
},
|
|
},
|
|
inputChecks2: []*mgmtProto.Checks{
|
|
{
|
|
Files: []string{
|
|
"testfile3",
|
|
"testfile4",
|
|
},
|
|
},
|
|
{
|
|
Files: []string{
|
|
"testfile2",
|
|
"testfile1",
|
|
},
|
|
},
|
|
},
|
|
expectedBool: true,
|
|
},
|
|
{
|
|
name: "Compared Slices with same files but different order while first is equal should return true",
|
|
inputChecks1: []*mgmtProto.Checks{
|
|
{
|
|
Files: []string{
|
|
"testfile0",
|
|
"testfile1",
|
|
},
|
|
},
|
|
{
|
|
Files: []string{
|
|
"testfile0",
|
|
"testfile2",
|
|
},
|
|
},
|
|
{
|
|
Files: []string{
|
|
"testfile0",
|
|
"testfile3",
|
|
},
|
|
},
|
|
},
|
|
inputChecks2: []*mgmtProto.Checks{
|
|
{
|
|
Files: []string{
|
|
"testfile0",
|
|
"testfile1",
|
|
},
|
|
},
|
|
{
|
|
Files: []string{
|
|
"testfile0",
|
|
"testfile3",
|
|
},
|
|
},
|
|
{
|
|
Files: []string{
|
|
"testfile0",
|
|
"testfile2",
|
|
},
|
|
},
|
|
},
|
|
expectedBool: true,
|
|
},
|
|
}
|
|
for _, testCase := range testCases {
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
result := isChecksEqual(testCase.inputChecks1, testCase.inputChecks2)
|
|
assert.Equal(t, testCase.expectedBool, result, "result should match expected bool")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCompareNetIPLists(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
list1 []netip.Prefix
|
|
list2 []string
|
|
expected bool
|
|
}{
|
|
{
|
|
name: "both empty",
|
|
list1: []netip.Prefix{},
|
|
list2: []string{},
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "single match ipv4",
|
|
list1: []netip.Prefix{netip.MustParsePrefix("192.168.0.0/24")},
|
|
list2: []string{"192.168.0.0/24"},
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "multiple match ipv4, different order",
|
|
list1: []netip.Prefix{netip.MustParsePrefix("192.168.1.0/24"), netip.MustParsePrefix("10.0.0.0/8")},
|
|
list2: []string{"10.0.0.0/8", "192.168.1.0/24"},
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "ipv4 mismatch due to extra element in list2",
|
|
list1: []netip.Prefix{netip.MustParsePrefix("192.168.1.0/24")},
|
|
list2: []string{"192.168.1.0/24", "10.0.0.0/8"},
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "ipv4 mismatch due to duplicate count",
|
|
list1: []netip.Prefix{netip.MustParsePrefix("192.168.1.0/24"), netip.MustParsePrefix("192.168.1.0/24")},
|
|
list2: []string{"192.168.1.0/24"},
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "invalid prefix in list2",
|
|
list1: []netip.Prefix{netip.MustParsePrefix("192.168.1.0/24")},
|
|
list2: []string{"invalid-prefix"},
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "ipv4 mismatch because different prefixes",
|
|
list1: []netip.Prefix{netip.MustParsePrefix("192.168.1.0/24")},
|
|
list2: []string{"10.0.0.0/8"},
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "single match ipv6",
|
|
list1: []netip.Prefix{netip.MustParsePrefix("2001:db8::/32")},
|
|
list2: []string{"2001:db8::/32"},
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "multiple match ipv6, different order",
|
|
list1: []netip.Prefix{netip.MustParsePrefix("2001:db8::/32"), netip.MustParsePrefix("fe80::/10")},
|
|
list2: []string{"fe80::/10", "2001:db8::/32"},
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "mixed ipv4 and ipv6 match",
|
|
list1: []netip.Prefix{netip.MustParsePrefix("192.168.1.0/24"), netip.MustParsePrefix("2001:db8::/32")},
|
|
list2: []string{"2001:db8::/32", "192.168.1.0/24"},
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "ipv6 mismatch with invalid prefix",
|
|
list1: []netip.Prefix{netip.MustParsePrefix("2001:db8::/32")},
|
|
list2: []string{"invalid-ipv6"},
|
|
expected: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := compareNetIPLists(tt.list1, tt.list2)
|
|
if result != tt.expected {
|
|
t.Errorf("compareNetIPLists(%v, %v) = %v; want %v", tt.list1, tt.list2, result, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func createEngine(ctx context.Context, cancel context.CancelFunc, setupKey string, i int, mgmtAddr string, signalAddr string) (*Engine, error) {
|
|
key, err := wgtypes.GeneratePrivateKey()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
mgmtClient, err := mgmt.NewClient(ctx, mgmtAddr, key, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
signalClient, err := signal.NewClient(ctx, signalAddr, key, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
publicKey, err := mgmtClient.GetServerPublicKey()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
info := system.GetInfo(ctx)
|
|
resp, err := mgmtClient.Register(*publicKey, setupKey, "", info, nil, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var ifaceName string
|
|
if runtime.GOOS == "darwin" {
|
|
ifaceName = fmt.Sprintf("utun1%d", i)
|
|
} else {
|
|
ifaceName = fmt.Sprintf("wt%d", i)
|
|
}
|
|
|
|
wgPort := 33100 + i
|
|
conf := &EngineConfig{
|
|
WgIfaceName: ifaceName,
|
|
WgAddr: resp.PeerConfig.Address,
|
|
WgPrivateKey: key,
|
|
WgPort: wgPort,
|
|
MTU: iface.DefaultMTU,
|
|
}
|
|
|
|
relayMgr := relayClient.NewManager(ctx, nil, key.PublicKey().String(), iface.DefaultMTU)
|
|
e, err := NewEngine(ctx, cancel, conf, EngineServices{
|
|
SignalClient: signalClient,
|
|
MgmClient: mgmtClient,
|
|
RelayManager: relayMgr,
|
|
StatusRecorder: peer.NewRecorder("https://mgm"),
|
|
}, MobileDependency{}), nil
|
|
e.ctx = ctx
|
|
return e, err
|
|
}
|
|
|
|
func startSignal(t *testing.T) (*grpc.Server, string, error) {
|
|
t.Helper()
|
|
|
|
s := grpc.NewServer(grpc.KeepaliveEnforcementPolicy(kaep), grpc.KeepaliveParams(kasp))
|
|
|
|
lis, err := net.Listen("tcp", "localhost:0")
|
|
if err != nil {
|
|
log.Fatalf("failed to listen: %v", err)
|
|
}
|
|
|
|
srv, err := signalServer.NewServer(context.Background(), otel.Meter(""))
|
|
require.NoError(t, err)
|
|
proto.RegisterSignalExchangeServer(s, srv)
|
|
|
|
go func() {
|
|
if err = s.Serve(lis); err != nil {
|
|
log.Fatalf("failed to serve: %v", err)
|
|
}
|
|
}()
|
|
|
|
return s, lis.Addr().String(), nil
|
|
}
|
|
|
|
func startManagement(t *testing.T, dataDir, testFile string) (*grpc.Server, string, error) {
|
|
t.Helper()
|
|
|
|
config := &config.Config{
|
|
Stuns: []*config.Host{},
|
|
TURNConfig: &config.TURNConfig{},
|
|
Relay: &config.Relay{
|
|
Addresses: []string{"127.0.0.1:1234"},
|
|
CredentialsTTL: util.Duration{Duration: time.Hour},
|
|
Secret: "222222222222222222",
|
|
},
|
|
Signal: &config.Host{
|
|
Proto: "http",
|
|
URI: "localhost:10000",
|
|
},
|
|
Datadir: dataDir,
|
|
HttpConfig: nil,
|
|
}
|
|
|
|
lis, err := net.Listen("tcp", "localhost:0")
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
s := grpc.NewServer(grpc.KeepaliveEnforcementPolicy(kaep), grpc.KeepaliveParams(kasp))
|
|
|
|
store, cleanUp, err := store.NewTestStoreFromSQL(context.Background(), testFile, config.Datadir)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
t.Cleanup(cleanUp)
|
|
|
|
eventStore := &activity.InMemoryEventStore{}
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
permissionsManager := permissions.NewManager(store)
|
|
peersManager := peers.NewManager(store, permissionsManager)
|
|
jobManager := job.NewJobManager(nil, store, peersManager)
|
|
|
|
ia, _ := integrations.NewIntegratedValidator(context.Background(), peersManager, nil, eventStore)
|
|
|
|
metrics, err := telemetry.NewDefaultAppMetrics(context.Background())
|
|
require.NoError(t, err)
|
|
|
|
ctrl := gomock.NewController(t)
|
|
t.Cleanup(ctrl.Finish)
|
|
settingsMockManager := settings.NewMockManager(ctrl)
|
|
settingsMockManager.EXPECT().
|
|
GetSettings(gomock.Any(), gomock.Any(), gomock.Any()).
|
|
Return(&types.Settings{}, nil).
|
|
AnyTimes()
|
|
settingsMockManager.EXPECT().
|
|
GetExtraSettings(gomock.Any(), gomock.Any()).
|
|
Return(&types.ExtraSettings{}, nil).
|
|
AnyTimes()
|
|
|
|
groupsManager := groups.NewManagerMock()
|
|
|
|
updateManager := update_channel.NewPeersUpdateManager(metrics)
|
|
requestBuffer := server.NewAccountRequestBuffer(context.Background(), store)
|
|
networkMapController := controller.NewController(context.Background(), store, metrics, updateManager, requestBuffer, server.MockIntegratedValidator{}, settingsMockManager, "netbird.selfhosted", port_forwarding.NewControllerMock(), manager.NewEphemeralManager(store, peersManager), config)
|
|
accountManager, err := server.BuildManager(context.Background(), config, store, networkMapController, jobManager, nil, "", eventStore, nil, false, ia, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
secretsManager, err := nbgrpc.NewTimeBasedAuthSecretsManager(updateManager, config.TURNConfig, config.Relay, settingsMockManager, groupsManager)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
mgmtServer, err := nbgrpc.NewServer(config, accountManager, settingsMockManager, jobManager, secretsManager, nil, nil, &server.MockIntegratedValidator{}, networkMapController, nil)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
mgmtProto.RegisterManagementServiceServer(s, mgmtServer)
|
|
go func() {
|
|
if err = s.Serve(lis); err != nil {
|
|
log.Fatalf("failed to serve: %v", err)
|
|
}
|
|
}()
|
|
|
|
return s, lis.Addr().String(), nil
|
|
}
|
|
|
|
// getConnectedPeers returns a connection Status or nil if peer connection wasn't found
|
|
func getConnectedPeers(e *Engine) int {
|
|
e.syncMsgMux.Lock()
|
|
defer e.syncMsgMux.Unlock()
|
|
i := 0
|
|
for _, id := range e.peerStore.PeersPubKey() {
|
|
conn, _ := e.peerStore.PeerConn(id)
|
|
if conn.IsConnected() {
|
|
i++
|
|
}
|
|
}
|
|
return i
|
|
}
|
|
|
|
func getPeers(e *Engine) int {
|
|
e.syncMsgMux.Lock()
|
|
defer e.syncMsgMux.Unlock()
|
|
|
|
return len(e.peerStore.PeersPubKey())
|
|
}
|