mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-12 16:39:09 +02:00
Pure plumbing, no change on the wire. Both halves exist for the same reason: the value this PR needs to publish is not reachable from the code paths that have to publish it, and widening the signatures is separable from moving the value. On the management side, the resolution between the account setting and the server-config default moves into resolveDebugUploadURL, and the default becomes a parameter that ToSyncResponse and ToComponentSyncResponse take rather than something read off the server config at the point of use. The callers that have a server config pass its value, which is what the code read before, so every response keeps the destination it had. The peer fan-out builds its sync with a nil server config, so anything derived from that config inside the conversion is unavailable there; a parameter is reachable from the fan-out. On the client side, Engine.Start takes the login response's PeerConfig. Nothing reads it yet.
338 lines
11 KiB
Go
338 lines
11 KiB
Go
package grpc
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"net/netip"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
nbdns "github.com/netbirdio/netbird/dns"
|
|
"github.com/netbirdio/netbird/management/internals/controllers/network_map"
|
|
"github.com/netbirdio/netbird/management/internals/controllers/network_map/controller/cache"
|
|
nbconfig "github.com/netbirdio/netbird/management/internals/server/config"
|
|
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
|
"github.com/netbirdio/netbird/management/server/types"
|
|
"github.com/netbirdio/netbird/shared/management/networkmap"
|
|
)
|
|
|
|
func TestToProtocolDNSConfigWithCache(t *testing.T) {
|
|
var cache cache.DNSConfigCache
|
|
|
|
// Create two different configs
|
|
config1 := nbdns.Config{
|
|
ServiceEnable: true,
|
|
CustomZones: []nbdns.CustomZone{
|
|
{
|
|
Domain: "example.com",
|
|
Records: []nbdns.SimpleRecord{
|
|
{Name: "www", Type: 1, Class: "IN", TTL: 300, RData: "192.168.1.1"},
|
|
},
|
|
},
|
|
},
|
|
NameServerGroups: []*nbdns.NameServerGroup{
|
|
{
|
|
ID: "group1",
|
|
Name: "Group 1",
|
|
NameServers: []nbdns.NameServer{
|
|
{IP: netip.MustParseAddr("8.8.8.8"), Port: 53},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
config2 := nbdns.Config{
|
|
ServiceEnable: true,
|
|
CustomZones: []nbdns.CustomZone{
|
|
{
|
|
Domain: "example.org",
|
|
Records: []nbdns.SimpleRecord{
|
|
{Name: "mail", Type: 1, Class: "IN", TTL: 300, RData: "192.168.1.2"},
|
|
},
|
|
},
|
|
},
|
|
NameServerGroups: []*nbdns.NameServerGroup{
|
|
{
|
|
ID: "group2",
|
|
Name: "Group 2",
|
|
NameServers: []nbdns.NameServer{
|
|
{IP: netip.MustParseAddr("8.8.4.4"), Port: 53},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
// First run with config1
|
|
result1 := networkmap.ToProtocolDNSConfig(config1, &cache, int64(network_map.DnsForwarderPort))
|
|
|
|
// Second run with config2
|
|
result2 := networkmap.ToProtocolDNSConfig(config2, &cache, int64(network_map.DnsForwarderPort))
|
|
|
|
// Third run with config1 again
|
|
result3 := networkmap.ToProtocolDNSConfig(config1, &cache, int64(network_map.DnsForwarderPort))
|
|
|
|
// Verify that result1 and result3 are identical
|
|
if !reflect.DeepEqual(result1, result3) {
|
|
t.Errorf("Results are not identical when run with the same input. Expected %v, got %v", result1, result3)
|
|
}
|
|
|
|
// Verify that result2 is different from result1 and result3
|
|
if reflect.DeepEqual(result1, result2) || reflect.DeepEqual(result2, result3) {
|
|
t.Errorf("Results should be different for different inputs")
|
|
}
|
|
|
|
if _, exists := cache.GetNameServerGroup("group1"); !exists {
|
|
t.Errorf("Cache should contain name server group 'group1'")
|
|
}
|
|
|
|
if _, exists := cache.GetNameServerGroup("group2"); !exists {
|
|
t.Errorf("Cache should contain name server group 'group2'")
|
|
}
|
|
}
|
|
|
|
func BenchmarkToProtocolDNSConfig(b *testing.B) {
|
|
sizes := []int{10, 100, 1000}
|
|
|
|
for _, size := range sizes {
|
|
testData := generateTestData(size)
|
|
|
|
b.Run(fmt.Sprintf("WithCache-Size%d", size), func(b *testing.B) {
|
|
cache := &cache.DNSConfigCache{}
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
networkmap.ToProtocolDNSConfig(testData, cache, int64(network_map.DnsForwarderPort))
|
|
}
|
|
})
|
|
|
|
b.Run(fmt.Sprintf("WithoutCache-Size%d", size), func(b *testing.B) {
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
networkmap.ToProtocolDNSConfig(testData, nil, int64(network_map.DnsForwarderPort))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func generateTestData(size int) nbdns.Config {
|
|
config := nbdns.Config{
|
|
ServiceEnable: true,
|
|
CustomZones: make([]nbdns.CustomZone, size),
|
|
NameServerGroups: make([]*nbdns.NameServerGroup, size),
|
|
}
|
|
|
|
for i := 0; i < size; i++ {
|
|
config.CustomZones[i] = nbdns.CustomZone{
|
|
Domain: fmt.Sprintf("domain%d.com", i),
|
|
Records: []nbdns.SimpleRecord{
|
|
{
|
|
Name: fmt.Sprintf("record%d", i),
|
|
Type: 1,
|
|
Class: "IN",
|
|
TTL: 3600,
|
|
RData: "192.168.1.1",
|
|
},
|
|
},
|
|
}
|
|
|
|
config.NameServerGroups[i] = &nbdns.NameServerGroup{
|
|
ID: fmt.Sprintf("group%d", i),
|
|
Primary: i == 0,
|
|
Domains: []string{fmt.Sprintf("domain%d.com", i)},
|
|
SearchDomainsEnabled: true,
|
|
NameServers: []nbdns.NameServer{
|
|
{
|
|
IP: netip.MustParseAddr("8.8.8.8"),
|
|
Port: 53,
|
|
NSType: 1,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
return config
|
|
}
|
|
|
|
func TestBuildJWTConfig_Audiences(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
authAudience string
|
|
cliAuthAudience string
|
|
expectedAudiences []string
|
|
expectedAudience string
|
|
}{
|
|
{
|
|
name: "only_auth_audience",
|
|
authAudience: "dashboard-aud",
|
|
cliAuthAudience: "",
|
|
expectedAudiences: []string{"dashboard-aud"},
|
|
expectedAudience: "dashboard-aud",
|
|
},
|
|
{
|
|
name: "both_audiences_different",
|
|
authAudience: "dashboard-aud",
|
|
cliAuthAudience: "cli-aud",
|
|
expectedAudiences: []string{"dashboard-aud", "cli-aud"},
|
|
expectedAudience: "cli-aud",
|
|
},
|
|
{
|
|
name: "both_audiences_same",
|
|
authAudience: "same-aud",
|
|
cliAuthAudience: "same-aud",
|
|
expectedAudiences: []string{"same-aud"},
|
|
expectedAudience: "same-aud",
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
config := &nbconfig.HttpServerConfig{
|
|
AuthIssuer: "https://issuer.example.com",
|
|
AuthAudience: tc.authAudience,
|
|
CLIAuthAudience: tc.cliAuthAudience,
|
|
}
|
|
|
|
result := buildJWTConfig(config, nil)
|
|
|
|
assert.NotNil(t, result)
|
|
assert.Equal(t, tc.expectedAudiences, result.Audiences, "audiences should match expected")
|
|
//nolint:staticcheck // SA1019: Testing backwards compatibility - Audience field must still be populated
|
|
assert.Equal(t, tc.expectedAudience, result.Audience, "audience should match expected")
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestShouldSkipSendingDeprecatedRemotePeers covers the version gate that
|
|
// stops populating the deprecated top-level SyncResponse.RemotePeers field for
|
|
// peers new enough to read RemotePeers off the NetworkMap. Development builds
|
|
// are treated as latest and skip the field. The gate otherwise fails safe: a
|
|
// release version older than the boundary, or one that can't be parsed (empty,
|
|
// garbage, prereleases of the boundary) still receives the deprecated field so
|
|
// older/unknown clients keep working.
|
|
func TestShouldSkipSendingDeprecatedRemotePeers(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
peerVersion string
|
|
wantSkip bool
|
|
}{
|
|
{"exact boundary skips", "0.29.3", true},
|
|
{"newer patch skips", "0.29.4", true},
|
|
{"newer minor skips", "0.30.0", true},
|
|
{"newer major skips", "1.0.0", true},
|
|
{"v-prefixed newer skips", "v0.30.0", true},
|
|
{"development build skips", "development", true},
|
|
{"development build with commit skips", "development-abc123def456-dirty", true},
|
|
{"older patch keeps field", "0.29.2", false},
|
|
{"older minor keeps field", "0.28.0", false},
|
|
{"prerelease of boundary keeps field", "0.29.3-SNAPSHOT", false},
|
|
{"tagged dev prerelease keeps field", "v0.31.1-dev", false},
|
|
{"empty version keeps field", "", false},
|
|
{"garbage version keeps field", "not-a-version", false},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got := shouldSkipSendingDeprecatedRemotePeers(tc.peerVersion)
|
|
assert.Equal(t, tc.wantSkip, got, "skip decision for peer version %q", tc.peerVersion)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestEncodeSessionExpiresAt pins the wire encoding the client's
|
|
// applySessionDeadline depends on:
|
|
//
|
|
// - zero deadline → &Timestamp{} (seconds=0, nanos=0): the explicit
|
|
// "expiry disabled or peer is not SSO-tracked" sentinel.
|
|
// - non-zero → timestamppb.New(deadline): the absolute UTC deadline.
|
|
//
|
|
// The third state (nil pointer = "no info in this snapshot") is the caller's
|
|
// responsibility on the Sync path when settings could not be resolved; the
|
|
// helper itself never returns nil.
|
|
func TestEncodeSessionExpiresAt(t *testing.T) {
|
|
t.Run("zero deadline encodes as explicit-zero sentinel", func(t *testing.T) {
|
|
got := encodeSessionExpiresAt(time.Time{})
|
|
assert.NotNil(t, got, "must not return nil; nil means 'no info', not 'disabled'")
|
|
assert.Equal(t, int64(0), got.GetSeconds())
|
|
assert.Equal(t, int32(0), got.GetNanos())
|
|
})
|
|
|
|
t.Run("non-zero deadline round-trips", func(t *testing.T) {
|
|
deadline := time.Date(2030, 1, 2, 3, 4, 5, 0, time.UTC)
|
|
got := encodeSessionExpiresAt(deadline)
|
|
assert.NotNil(t, got)
|
|
assert.True(t, got.AsTime().Equal(deadline))
|
|
})
|
|
}
|
|
|
|
// TestToNetbirdConfig_RelayInvariant guards against the v0.74.0 relay-wipe regression.
|
|
// Clients treat any non-nil NetbirdConfig as authoritative and interpret a missing relay
|
|
// section as relay disabled, wiping their relay URLs. toNetbirdConfig must therefore
|
|
// return nil when no server config is set (the fan-out network-map path) instead of a
|
|
// partial config, and a result built from a relay-enabled config must carry the relay
|
|
// section.
|
|
func TestToNetbirdConfig_RelayInvariant(t *testing.T) {
|
|
settings := &types.Settings{MetricsPushEnabled: true}
|
|
|
|
t.Run("nil server config returns nil config", func(t *testing.T) {
|
|
nbCfg := toNetbirdConfig(nil, nil, nil, nil, types.TwinAccountSettings(settings), "")
|
|
assert.Nil(t, nbCfg, "fan-out updates must not carry a partial NetbirdConfig even when settings are present")
|
|
})
|
|
|
|
t.Run("relay-enabled config carries relay section", func(t *testing.T) {
|
|
cfg := &nbconfig.Config{
|
|
Stuns: []*nbconfig.Host{{Proto: nbconfig.UDP, URI: "stun:stun.example.com:3478"}},
|
|
TURNConfig: &nbconfig.TURNConfig{
|
|
Turns: []*nbconfig.Host{{Proto: nbconfig.UDP, URI: "turn:turn.example.com:3478", Username: "user", Password: "pass"}},
|
|
},
|
|
Relay: &nbconfig.Relay{Addresses: []string{"rels://relay.example.com:443"}},
|
|
Signal: &nbconfig.Host{Proto: nbconfig.HTTP, URI: "signal.example.com:10000"},
|
|
}
|
|
relayToken := &Token{Payload: "token-payload", Signature: "token-signature"}
|
|
|
|
nbCfg := toNetbirdConfig(cfg, nil, relayToken, nil, types.TwinAccountSettings(settings), "")
|
|
require.NotNil(t, nbCfg)
|
|
require.NotNil(t, nbCfg.Relay, "non-nil NetbirdConfig must include the relay section")
|
|
assert.Equal(t, cfg.Relay.Addresses, nbCfg.Relay.Urls, "relay URLs should match the server config")
|
|
assert.Equal(t, relayToken.Payload, nbCfg.Relay.TokenPayload, "relay token payload should be set")
|
|
assert.Equal(t, relayToken.Signature, nbCfg.Relay.TokenSignature, "relay token signature should be set")
|
|
require.NotNil(t, nbCfg.Metrics)
|
|
assert.True(t, nbCfg.Metrics.Enabled, "metrics flag should carry the settings value")
|
|
})
|
|
}
|
|
|
|
func TestToPeerConfig_RoutingPeerDNSResolution(t *testing.T) {
|
|
network := &types.Network{Net: net.IPNet{IP: net.IPv4(100, 0, 0, 0), Mask: net.CIDRMask(8, 32)}}
|
|
|
|
newPeer := func(embedded bool) *nbpeer.Peer {
|
|
p := &nbpeer.Peer{IP: netip.MustParseAddr("100.0.0.1")}
|
|
p.ProxyMeta.Embedded = embedded
|
|
return p
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
globalFlag bool
|
|
embedded bool
|
|
forceParam bool
|
|
wantEnabled bool
|
|
}{
|
|
{name: "global off, regular peer, no force", wantEnabled: false},
|
|
{name: "global on wins", globalFlag: true, wantEnabled: true},
|
|
{name: "embedded proxy peer forced", embedded: true, wantEnabled: true},
|
|
{name: "routing peer forced via param", forceParam: true, wantEnabled: true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
settings := &types.Settings{RoutingPeerDNSResolutionEnabled: tt.globalFlag}
|
|
cfg := toPeerConfig(types.TwinPeer(newPeer(tt.embedded)), types.TwinNetwork(network), "netbird.selfhosted", types.TwinAccountSettings(settings), nil, nil, false, tt.forceParam)
|
|
assert.Equal(t, tt.wantEnabled, cfg.RoutingPeerDnsResolutionEnabled,
|
|
"RoutingPeerDnsResolutionEnabled should reflect global || embedded || forced")
|
|
})
|
|
}
|
|
}
|