mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-16 15:26:40 +00:00
822 lines
26 KiB
Go
822 lines
26 KiB
Go
package debug
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bytes"
|
|
"encoding/json"
|
|
"net"
|
|
"net/netip"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/netbirdio/netbird/client/anonymize"
|
|
"github.com/netbirdio/netbird/client/configs"
|
|
mgmProto "github.com/netbirdio/netbird/shared/management/proto"
|
|
"github.com/netbirdio/netbird/shared/netiputil"
|
|
)
|
|
|
|
func TestAnonymizeStateFile(t *testing.T) {
|
|
testState := map[string]json.RawMessage{
|
|
"null_state": json.RawMessage("null"),
|
|
"test_state": mustMarshal(map[string]any{
|
|
// Test simple fields
|
|
"public_ip": "203.0.113.1",
|
|
"private_ip": "192.168.1.1",
|
|
"protected_ip": "100.64.0.1",
|
|
"well_known_ip": "8.8.8.8",
|
|
"ipv6_addr": "2001:db8::1",
|
|
"private_ipv6": "fd00::1",
|
|
"domain": "test.example.com",
|
|
"uri": "stun:stun.example.com:3478",
|
|
"uri_with_ip": "turn:203.0.113.1:3478",
|
|
"netbird_domain": "device.netbird.cloud",
|
|
|
|
// Test CIDR ranges
|
|
"public_cidr": "203.0.113.0/24",
|
|
"private_cidr": "192.168.0.0/16",
|
|
"protected_cidr": "100.64.0.0/10",
|
|
"ipv6_cidr": "2001:db8::/32",
|
|
"private_ipv6_cidr": "fd00::/8",
|
|
|
|
// Test nested structures
|
|
"nested": map[string]any{
|
|
"ip": "203.0.113.2",
|
|
"domain": "nested.example.com",
|
|
"more_nest": map[string]any{
|
|
"ip": "203.0.113.3",
|
|
"domain": "deep.example.com",
|
|
},
|
|
},
|
|
|
|
// Test arrays
|
|
"string_array": []any{
|
|
"203.0.113.4",
|
|
"test1.example.com",
|
|
"test2.example.com",
|
|
},
|
|
"object_array": []any{
|
|
map[string]any{
|
|
"ip": "203.0.113.5",
|
|
"domain": "array1.example.com",
|
|
},
|
|
map[string]any{
|
|
"ip": "203.0.113.6",
|
|
"domain": "array2.example.com",
|
|
},
|
|
},
|
|
|
|
// Test multiple occurrences of same value
|
|
"duplicate_ip": "203.0.113.1", // Same as public_ip
|
|
"duplicate_domain": "test.example.com", // Same as domain
|
|
|
|
// Test URIs with various schemes
|
|
"stun_uri": "stun:stun.example.com:3478",
|
|
"turns_uri": "turns:turns.example.com:5349",
|
|
"http_uri": "http://web.example.com:80",
|
|
"https_uri": "https://secure.example.com:443",
|
|
|
|
// Test strings that might look like IPs but aren't
|
|
"not_ip": "300.300.300.300",
|
|
"partial_ip": "192.168",
|
|
"ip_like_string": "1234.5678",
|
|
|
|
// Test mixed content strings
|
|
"mixed_content": "Server at 203.0.113.1 (test.example.com) on port 80",
|
|
|
|
// Test empty and special values
|
|
"empty_string": "",
|
|
"null_value": nil,
|
|
"numeric_value": 42,
|
|
"boolean_value": true,
|
|
}),
|
|
"route_state": mustMarshal(map[string]any{
|
|
"routes": []any{
|
|
map[string]any{
|
|
"network": "203.0.113.0/24",
|
|
"gateway": "203.0.113.1",
|
|
"domains": []any{
|
|
"route1.example.com",
|
|
"route2.example.com",
|
|
},
|
|
},
|
|
map[string]any{
|
|
"network": "2001:db8::/32",
|
|
"gateway": "2001:db8::1",
|
|
"domains": []any{
|
|
"route3.example.com",
|
|
"route4.example.com",
|
|
},
|
|
},
|
|
},
|
|
// Test map with IP/CIDR keys
|
|
"refCountMap": map[string]any{
|
|
"203.0.113.1/32": map[string]any{
|
|
"Count": 1,
|
|
"Out": map[string]any{
|
|
"IP": "192.168.0.1",
|
|
"Intf": map[string]any{
|
|
"Name": "eth0",
|
|
"Index": 1,
|
|
},
|
|
},
|
|
},
|
|
"2001:db8::1/128": map[string]any{
|
|
"Count": 1,
|
|
"Out": map[string]any{
|
|
"IP": "fe80::1",
|
|
"Intf": map[string]any{
|
|
"Name": "eth0",
|
|
"Index": 1,
|
|
},
|
|
},
|
|
},
|
|
"10.0.0.1/32": map[string]any{ // private IP should remain unchanged
|
|
"Count": 1,
|
|
"Out": map[string]any{
|
|
"IP": "192.168.0.1",
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
}
|
|
|
|
anonymizer := anonymize.NewAnonymizer(anonymize.DefaultAddresses())
|
|
|
|
// Pre-seed the domains we need to verify in the test assertions
|
|
anonymizer.AnonymizeDomain("test.example.com")
|
|
anonymizer.AnonymizeDomain("nested.example.com")
|
|
anonymizer.AnonymizeDomain("deep.example.com")
|
|
anonymizer.AnonymizeDomain("array1.example.com")
|
|
|
|
err := anonymizeStateFile(&testState, anonymizer)
|
|
require.NoError(t, err)
|
|
|
|
// Helper function to unmarshal and get nested values
|
|
var state map[string]any
|
|
err = json.Unmarshal(testState["test_state"], &state)
|
|
require.NoError(t, err)
|
|
|
|
// Test null state remains unchanged
|
|
require.Equal(t, "null", string(testState["null_state"]))
|
|
|
|
// Basic assertions
|
|
assert.NotEqual(t, "203.0.113.1", state["public_ip"])
|
|
assert.Equal(t, "192.168.1.1", state["private_ip"]) // Private IP unchanged
|
|
assert.Equal(t, "100.64.0.1", state["protected_ip"]) // Protected IP unchanged
|
|
assert.Equal(t, "8.8.8.8", state["well_known_ip"]) // Well-known IP unchanged
|
|
assert.NotEqual(t, "2001:db8::1", state["ipv6_addr"])
|
|
assert.NotEqual(t, "fd00::1", state["private_ipv6"]) // ULA IPv6 anonymized (global ID is a fingerprint)
|
|
assert.NotEqual(t, "test.example.com", state["domain"])
|
|
assert.True(t, strings.HasSuffix(state["domain"].(string), ".domain"))
|
|
assert.Equal(t, "device.netbird.cloud", state["netbird_domain"]) // Netbird domain unchanged
|
|
|
|
// CIDR ranges
|
|
assert.NotEqual(t, "203.0.113.0/24", state["public_cidr"])
|
|
assert.Contains(t, state["public_cidr"], "/24") // Prefix preserved
|
|
assert.Equal(t, "192.168.0.0/16", state["private_cidr"]) // Private CIDR unchanged
|
|
assert.Equal(t, "100.64.0.0/10", state["protected_cidr"]) // Protected CIDR unchanged
|
|
assert.NotEqual(t, "2001:db8::/32", state["ipv6_cidr"])
|
|
assert.Contains(t, state["ipv6_cidr"], "/32") // IPv6 prefix preserved
|
|
|
|
// Nested structures
|
|
nested := state["nested"].(map[string]any)
|
|
assert.NotEqual(t, "203.0.113.2", nested["ip"])
|
|
assert.NotEqual(t, "nested.example.com", nested["domain"])
|
|
moreNest := nested["more_nest"].(map[string]any)
|
|
assert.NotEqual(t, "203.0.113.3", moreNest["ip"])
|
|
assert.NotEqual(t, "deep.example.com", moreNest["domain"])
|
|
|
|
// Arrays
|
|
strArray := state["string_array"].([]any)
|
|
assert.NotEqual(t, "203.0.113.4", strArray[0])
|
|
assert.NotEqual(t, "test1.example.com", strArray[1])
|
|
assert.True(t, strings.HasSuffix(strArray[1].(string), ".domain"))
|
|
|
|
objArray := state["object_array"].([]any)
|
|
firstObj := objArray[0].(map[string]any)
|
|
assert.NotEqual(t, "203.0.113.5", firstObj["ip"])
|
|
assert.NotEqual(t, "array1.example.com", firstObj["domain"])
|
|
|
|
// Duplicate values should be anonymized consistently
|
|
assert.Equal(t, state["public_ip"], state["duplicate_ip"])
|
|
assert.Equal(t, state["domain"], state["duplicate_domain"])
|
|
|
|
// URIs
|
|
assert.NotContains(t, state["stun_uri"], "stun.example.com")
|
|
assert.NotContains(t, state["turns_uri"], "turns.example.com")
|
|
assert.NotContains(t, state["http_uri"], "web.example.com")
|
|
assert.NotContains(t, state["https_uri"], "secure.example.com")
|
|
|
|
// Non-IP strings should remain unchanged
|
|
assert.Equal(t, "300.300.300.300", state["not_ip"])
|
|
assert.Equal(t, "192.168", state["partial_ip"])
|
|
assert.Equal(t, "1234.5678", state["ip_like_string"])
|
|
|
|
// Mixed content should have IPs and domains replaced
|
|
mixedContent := state["mixed_content"].(string)
|
|
assert.NotContains(t, mixedContent, "203.0.113.1")
|
|
assert.NotContains(t, mixedContent, "test.example.com")
|
|
assert.Contains(t, mixedContent, "Server at ")
|
|
assert.Contains(t, mixedContent, " on port 80")
|
|
|
|
// Special values should remain unchanged
|
|
assert.Equal(t, "", state["empty_string"])
|
|
assert.Nil(t, state["null_value"])
|
|
assert.Equal(t, float64(42), state["numeric_value"])
|
|
assert.Equal(t, true, state["boolean_value"])
|
|
|
|
// Check route state
|
|
var routeState map[string]any
|
|
err = json.Unmarshal(testState["route_state"], &routeState)
|
|
require.NoError(t, err)
|
|
|
|
routes := routeState["routes"].([]any)
|
|
route1 := routes[0].(map[string]any)
|
|
assert.NotEqual(t, "203.0.113.0/24", route1["network"])
|
|
assert.Contains(t, route1["network"], "/24")
|
|
assert.NotEqual(t, "203.0.113.1", route1["gateway"])
|
|
domains := route1["domains"].([]any)
|
|
assert.True(t, strings.HasSuffix(domains[0].(string), ".domain"))
|
|
assert.True(t, strings.HasSuffix(domains[1].(string), ".domain"))
|
|
|
|
// Check map keys are anonymized
|
|
refCountMap := routeState["refCountMap"].(map[string]any)
|
|
hasPublicIPKey := false
|
|
hasIPv6Key := false
|
|
hasPrivateIPKey := false
|
|
for key := range refCountMap {
|
|
if strings.Contains(key, "203.0.113.1") {
|
|
hasPublicIPKey = true
|
|
}
|
|
if strings.Contains(key, "2001:db8::1") {
|
|
hasIPv6Key = true
|
|
}
|
|
if key == "10.0.0.1/32" {
|
|
hasPrivateIPKey = true
|
|
}
|
|
}
|
|
assert.False(t, hasPublicIPKey, "public IP in key should be anonymized")
|
|
assert.False(t, hasIPv6Key, "IPv6 in key should be anonymized")
|
|
assert.True(t, hasPrivateIPKey, "private IP in key should remain unchanged")
|
|
}
|
|
|
|
func mustMarshal(v any) json.RawMessage {
|
|
data, err := json.Marshal(v)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return data
|
|
}
|
|
|
|
func TestAnonymizeNetworkMap(t *testing.T) {
|
|
origV6Prefix := netip.MustParsePrefix("2001:db8:abcd::5/64")
|
|
networkMap := &mgmProto.NetworkMap{
|
|
PeerConfig: &mgmProto.PeerConfig{
|
|
Address: "203.0.113.5",
|
|
AddressV6: netiputil.EncodePrefix(origV6Prefix),
|
|
Dns: "1.2.3.4",
|
|
Fqdn: "peer1.corp.example.com",
|
|
SshConfig: &mgmProto.SSHConfig{
|
|
SshPubKey: []byte("ssh-rsa AAAAB3NzaC1..."),
|
|
},
|
|
},
|
|
RemotePeers: []*mgmProto.RemotePeerConfig{
|
|
{
|
|
AllowedIps: []string{
|
|
"203.0.113.1/32",
|
|
"2001:db8:1234::1/128",
|
|
"192.168.1.1/32",
|
|
"100.64.0.1/32",
|
|
"10.0.0.1/32",
|
|
},
|
|
Fqdn: "peer2.corp.example.com",
|
|
SshConfig: &mgmProto.SSHConfig{
|
|
SshPubKey: []byte("ssh-rsa AAAAB3NzaC2..."),
|
|
},
|
|
},
|
|
},
|
|
Routes: []*mgmProto.Route{
|
|
{
|
|
Network: "197.51.100.0/24",
|
|
Domains: []string{"prod.example.com", "staging.example.com"},
|
|
NetID: "net-123abc",
|
|
},
|
|
},
|
|
DNSConfig: &mgmProto.DNSConfig{
|
|
NameServerGroups: []*mgmProto.NameServerGroup{
|
|
{
|
|
NameServers: []*mgmProto.NameServer{
|
|
{IP: "8.8.8.8"},
|
|
{IP: "1.1.1.1"},
|
|
{IP: "203.0.113.53"},
|
|
},
|
|
Domains: []string{"example.com", "internal.example.com"},
|
|
},
|
|
},
|
|
CustomZones: []*mgmProto.CustomZone{
|
|
{
|
|
Domain: "custom.example.com",
|
|
Records: []*mgmProto.SimpleRecord{
|
|
{
|
|
Name: "www.custom.example.com",
|
|
Type: 1,
|
|
RData: "203.0.113.10",
|
|
},
|
|
{
|
|
Name: "internal.custom.example.com",
|
|
Type: 1,
|
|
RData: "192.168.1.10",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
// Create anonymizer with test addresses
|
|
anonymizer := anonymize.NewAnonymizer(anonymize.DefaultAddresses())
|
|
|
|
// Anonymize the network map
|
|
err := anonymizeNetworkMap(networkMap, anonymizer)
|
|
require.NoError(t, err)
|
|
|
|
// Test PeerConfig anonymization
|
|
peerCfg := networkMap.PeerConfig
|
|
require.NotEqual(t, "203.0.113.5", peerCfg.Address)
|
|
|
|
// Verify DNS and FQDN are properly anonymized
|
|
require.NotEqual(t, "1.2.3.4", peerCfg.Dns)
|
|
require.NotEqual(t, "peer1.corp.example.com", peerCfg.Fqdn)
|
|
require.True(t, strings.HasSuffix(peerCfg.Fqdn, ".domain"))
|
|
|
|
// Verify AddressV6 is anonymized but preserves prefix length
|
|
anonV6Prefix, err := netiputil.DecodePrefix(peerCfg.AddressV6)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, origV6Prefix.Bits(), anonV6Prefix.Bits(), "prefix length must be preserved")
|
|
assert.NotEqual(t, origV6Prefix.Addr(), anonV6Prefix.Addr(), "IPv6 address must be anonymized")
|
|
|
|
// Verify SSH key is replaced
|
|
require.Equal(t, []byte("ssh-placeholder-key"), peerCfg.SshConfig.SshPubKey)
|
|
|
|
// Test RemotePeers anonymization
|
|
remotePeer := networkMap.RemotePeers[0]
|
|
|
|
// Verify FQDN is anonymized
|
|
require.NotEqual(t, "peer2.corp.example.com", remotePeer.Fqdn)
|
|
require.True(t, strings.HasSuffix(remotePeer.Fqdn, ".domain"))
|
|
|
|
// Check that public IPs are anonymized but private IPs are preserved
|
|
for _, allowedIP := range remotePeer.AllowedIps {
|
|
ip, _, err := net.ParseCIDR(allowedIP)
|
|
require.NoError(t, err)
|
|
|
|
if ip.IsPrivate() || isInCGNATRange(ip) {
|
|
require.Contains(t, []string{
|
|
"192.168.1.1/32",
|
|
"100.64.0.1/32",
|
|
"10.0.0.1/32",
|
|
}, allowedIP)
|
|
} else {
|
|
require.NotContains(t, []string{
|
|
"203.0.113.1/32",
|
|
"2001:db8:1234::1/128",
|
|
}, allowedIP)
|
|
}
|
|
}
|
|
|
|
// Test Routes anonymization
|
|
route := networkMap.Routes[0]
|
|
require.NotEqual(t, "197.51.100.0/24", route.Network)
|
|
for _, domain := range route.Domains {
|
|
require.True(t, strings.HasSuffix(domain, ".domain"))
|
|
require.NotContains(t, domain, "example.com")
|
|
}
|
|
|
|
// Test DNS config anonymization
|
|
dnsConfig := networkMap.DNSConfig
|
|
nameServerGroup := dnsConfig.NameServerGroups[0]
|
|
|
|
// Verify well-known DNS servers are preserved
|
|
require.Equal(t, "8.8.8.8", nameServerGroup.NameServers[0].IP)
|
|
require.Equal(t, "1.1.1.1", nameServerGroup.NameServers[1].IP)
|
|
|
|
// Verify public DNS server is anonymized
|
|
require.NotEqual(t, "203.0.113.53", nameServerGroup.NameServers[2].IP)
|
|
|
|
// Verify domains are anonymized
|
|
for _, domain := range nameServerGroup.Domains {
|
|
require.True(t, strings.HasSuffix(domain, ".domain"))
|
|
require.NotContains(t, domain, "example.com")
|
|
}
|
|
|
|
// Test CustomZones anonymization
|
|
customZone := dnsConfig.CustomZones[0]
|
|
require.True(t, strings.HasSuffix(customZone.Domain, ".domain"))
|
|
require.NotContains(t, customZone.Domain, "example.com")
|
|
|
|
// Verify records are properly anonymized
|
|
for _, record := range customZone.Records {
|
|
require.True(t, strings.HasSuffix(record.Name, ".domain"))
|
|
require.NotContains(t, record.Name, "example.com")
|
|
|
|
ip := net.ParseIP(record.RData)
|
|
if ip != nil {
|
|
if !ip.IsPrivate() {
|
|
require.NotEqual(t, "203.0.113.10", record.RData)
|
|
} else {
|
|
require.Equal(t, "192.168.1.10", record.RData)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestIsSensitiveEnvVar(t *testing.T) {
|
|
tests := []struct {
|
|
key string
|
|
sensitive bool
|
|
}{
|
|
{"NB_SETUP_KEY", true},
|
|
{"NB_API_TOKEN", true},
|
|
{"NB_CLIENT_SECRET", true},
|
|
{"NB_PASSWORD", true},
|
|
{"NB_CREDENTIAL", true},
|
|
{"NB_LOG_LEVEL", false},
|
|
{"NB_MANAGEMENT_URL", false},
|
|
{"NB_HOSTNAME", false},
|
|
{"HOME", false},
|
|
{"PATH", false},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.key, func(t *testing.T) {
|
|
assert.Equal(t, tt.sensitive, isSensitiveEnvVar(tt.key))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSanitizeServiceEnvVars(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
anonymize bool
|
|
input map[string]any
|
|
check func(t *testing.T, params map[string]any)
|
|
}{
|
|
{
|
|
name: "no env vars key",
|
|
anonymize: false,
|
|
input: map[string]any{"management_url": "https://mgmt.example.com"},
|
|
check: func(t *testing.T, params map[string]any) {
|
|
t.Helper()
|
|
assert.Equal(t, "https://mgmt.example.com", params["management_url"], "non-env fields should be untouched")
|
|
_, ok := params[jsonKeyServiceEnv]
|
|
assert.False(t, ok, "service_env_vars should not be added")
|
|
},
|
|
},
|
|
{
|
|
name: "non-NB vars are masked",
|
|
anonymize: false,
|
|
input: map[string]any{
|
|
jsonKeyServiceEnv: map[string]any{
|
|
"HOME": "/root",
|
|
"PATH": "/usr/bin",
|
|
"NB_LOG_LEVEL": "debug",
|
|
},
|
|
},
|
|
check: func(t *testing.T, params map[string]any) {
|
|
t.Helper()
|
|
env := params[jsonKeyServiceEnv].(map[string]any)
|
|
assert.Equal(t, maskedValue, env["HOME"], "non-NB_ var should be masked")
|
|
assert.Equal(t, maskedValue, env["PATH"], "non-NB_ var should be masked")
|
|
assert.Equal(t, "debug", env["NB_LOG_LEVEL"], "safe NB_ var should pass through")
|
|
},
|
|
},
|
|
{
|
|
name: "sensitive NB vars are masked",
|
|
anonymize: false,
|
|
input: map[string]any{
|
|
jsonKeyServiceEnv: map[string]any{
|
|
"NB_SETUP_KEY": "abc123",
|
|
"NB_API_TOKEN": "tok_xyz",
|
|
"NB_LOG_LEVEL": "info",
|
|
},
|
|
},
|
|
check: func(t *testing.T, params map[string]any) {
|
|
t.Helper()
|
|
env := params[jsonKeyServiceEnv].(map[string]any)
|
|
assert.Equal(t, maskedValue, env["NB_SETUP_KEY"], "sensitive NB_ var should be masked")
|
|
assert.Equal(t, maskedValue, env["NB_API_TOKEN"], "sensitive NB_ var should be masked")
|
|
assert.Equal(t, "info", env["NB_LOG_LEVEL"], "safe NB_ var should pass through")
|
|
},
|
|
},
|
|
{
|
|
name: "safe NB vars anonymized when anonymize is true",
|
|
anonymize: true,
|
|
input: map[string]any{
|
|
jsonKeyServiceEnv: map[string]any{
|
|
"NB_MANAGEMENT_URL": "https://mgmt.example.com:443",
|
|
"NB_LOG_LEVEL": "debug",
|
|
"NB_SETUP_KEY": "secret",
|
|
"SOME_OTHER": "val",
|
|
},
|
|
},
|
|
check: func(t *testing.T, params map[string]any) {
|
|
t.Helper()
|
|
env := params[jsonKeyServiceEnv].(map[string]any)
|
|
// Safe NB_ values should be anonymized (not the original, not masked)
|
|
mgmtVal := env["NB_MANAGEMENT_URL"].(string)
|
|
assert.NotEqual(t, "https://mgmt.example.com:443", mgmtVal, "should be anonymized")
|
|
assert.NotEqual(t, maskedValue, mgmtVal, "should not be masked")
|
|
|
|
logVal := env["NB_LOG_LEVEL"].(string)
|
|
assert.NotEqual(t, maskedValue, logVal, "safe NB_ var should not be masked")
|
|
|
|
// Sensitive and non-NB_ still masked
|
|
assert.Equal(t, maskedValue, env["NB_SETUP_KEY"])
|
|
assert.Equal(t, maskedValue, env["SOME_OTHER"])
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
anonymizer := anonymize.NewAnonymizer(anonymize.DefaultAddresses())
|
|
g := &BundleGenerator{
|
|
anonymize: tt.anonymize,
|
|
anonymizer: anonymizer,
|
|
}
|
|
g.sanitizeServiceEnvVars(tt.input)
|
|
tt.check(t, tt.input)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAddServiceParams(t *testing.T) {
|
|
t.Run("missing service.json returns nil", func(t *testing.T) {
|
|
g := &BundleGenerator{
|
|
anonymizer: anonymize.NewAnonymizer(anonymize.DefaultAddresses()),
|
|
}
|
|
|
|
origStateDir := configs.StateDir
|
|
configs.StateDir = t.TempDir()
|
|
t.Cleanup(func() { configs.StateDir = origStateDir })
|
|
|
|
err := g.addServiceParams()
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("management_url anonymized when anonymize is true", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
origStateDir := configs.StateDir
|
|
configs.StateDir = dir
|
|
t.Cleanup(func() { configs.StateDir = origStateDir })
|
|
|
|
input := map[string]any{
|
|
jsonKeyManagementURL: "https://api.example.com:443",
|
|
jsonKeyServiceEnv: map[string]any{
|
|
"NB_LOG_LEVEL": "trace",
|
|
},
|
|
}
|
|
data, err := json.Marshal(input)
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(filepath.Join(dir, serviceParamsFile), data, 0600))
|
|
|
|
var buf bytes.Buffer
|
|
zw := zip.NewWriter(&buf)
|
|
|
|
g := &BundleGenerator{
|
|
anonymize: true,
|
|
anonymizer: anonymize.NewAnonymizer(anonymize.DefaultAddresses()),
|
|
archive: zw,
|
|
}
|
|
|
|
require.NoError(t, g.addServiceParams())
|
|
require.NoError(t, zw.Close())
|
|
|
|
zr, err := zip.NewReader(bytes.NewReader(buf.Bytes()), int64(buf.Len()))
|
|
require.NoError(t, err)
|
|
require.Len(t, zr.File, 1)
|
|
assert.Equal(t, serviceParamsBundle, zr.File[0].Name)
|
|
|
|
rc, err := zr.File[0].Open()
|
|
require.NoError(t, err)
|
|
defer rc.Close()
|
|
|
|
var result map[string]any
|
|
require.NoError(t, json.NewDecoder(rc).Decode(&result))
|
|
|
|
mgmt := result[jsonKeyManagementURL].(string)
|
|
assert.NotEqual(t, "https://api.example.com:443", mgmt, "management_url should be anonymized")
|
|
assert.NotEmpty(t, mgmt)
|
|
|
|
env := result[jsonKeyServiceEnv].(map[string]any)
|
|
assert.NotEqual(t, maskedValue, env["NB_LOG_LEVEL"], "safe NB_ var should not be masked")
|
|
})
|
|
|
|
t.Run("management_url preserved when anonymize is false", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
origStateDir := configs.StateDir
|
|
configs.StateDir = dir
|
|
t.Cleanup(func() { configs.StateDir = origStateDir })
|
|
|
|
input := map[string]any{
|
|
jsonKeyManagementURL: "https://api.example.com:443",
|
|
}
|
|
data, err := json.Marshal(input)
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(filepath.Join(dir, serviceParamsFile), data, 0600))
|
|
|
|
var buf bytes.Buffer
|
|
zw := zip.NewWriter(&buf)
|
|
|
|
g := &BundleGenerator{
|
|
anonymize: false,
|
|
anonymizer: anonymize.NewAnonymizer(anonymize.DefaultAddresses()),
|
|
archive: zw,
|
|
}
|
|
|
|
require.NoError(t, g.addServiceParams())
|
|
require.NoError(t, zw.Close())
|
|
|
|
zr, err := zip.NewReader(bytes.NewReader(buf.Bytes()), int64(buf.Len()))
|
|
require.NoError(t, err)
|
|
|
|
rc, err := zr.File[0].Open()
|
|
require.NoError(t, err)
|
|
defer rc.Close()
|
|
|
|
var result map[string]any
|
|
require.NoError(t, json.NewDecoder(rc).Decode(&result))
|
|
|
|
assert.Equal(t, "https://api.example.com:443", result[jsonKeyManagementURL], "management_url should be preserved")
|
|
})
|
|
}
|
|
|
|
// Helper function to check if IP is in CGNAT range
|
|
func isInCGNATRange(ip net.IP) bool {
|
|
cgnat := net.IPNet{
|
|
IP: net.ParseIP("100.64.0.0"),
|
|
Mask: net.CIDRMask(10, 32),
|
|
}
|
|
return cgnat.Contains(ip)
|
|
}
|
|
|
|
func TestAnonymizeFirewallRules(t *testing.T) {
|
|
// Example iptables-save output
|
|
iptablesSave := `# Generated by iptables-save v1.8.7 on Thu Dec 19 10:00:00 2024
|
|
*filter
|
|
:INPUT ACCEPT [0:0]
|
|
:FORWARD ACCEPT [0:0]
|
|
:OUTPUT ACCEPT [0:0]
|
|
-A INPUT -s 192.168.1.0/24 -j ACCEPT
|
|
-A INPUT -s 44.192.140.1/32 -j DROP
|
|
-A FORWARD -s 10.0.0.0/8 -j DROP
|
|
-A FORWARD -s 44.192.140.0/24 -d 52.84.12.34/24 -j ACCEPT
|
|
COMMIT
|
|
|
|
*nat
|
|
:PREROUTING ACCEPT [0:0]
|
|
:INPUT ACCEPT [0:0]
|
|
:OUTPUT ACCEPT [0:0]
|
|
:POSTROUTING ACCEPT [0:0]
|
|
-A POSTROUTING -s 192.168.100.0/24 -j MASQUERADE
|
|
-A PREROUTING -d 44.192.140.10/32 -p tcp -m tcp --dport 80 -j DNAT --to-destination 192.168.1.10:80
|
|
COMMIT`
|
|
|
|
// Example iptables -v -n -L output
|
|
iptablesVerbose := `Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
|
|
pkts bytes target prot opt in out source destination
|
|
0 0 ACCEPT all -- * * 192.168.1.0/24 0.0.0.0/0
|
|
100 1024 DROP all -- * * 44.192.140.1 0.0.0.0/0
|
|
|
|
Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
|
|
pkts bytes target prot opt in out source destination
|
|
0 0 DROP all -- * * 10.0.0.0/8 0.0.0.0/0
|
|
25 256 ACCEPT all -- * * 44.192.140.0/24 52.84.12.34/24
|
|
|
|
Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)
|
|
pkts bytes target prot opt in out source destination`
|
|
|
|
// Example ip6tables-save output
|
|
ip6tablesSave := `# Generated by ip6tables-save v1.8.7 on Thu Dec 19 10:00:00 2024
|
|
*filter
|
|
:INPUT ACCEPT [0:0]
|
|
:FORWARD ACCEPT [0:0]
|
|
:OUTPUT ACCEPT [0:0]
|
|
-A INPUT -s fd00:1234::1/128 -j ACCEPT
|
|
-A INPUT -s 2607:f8b0:4005::1/128 -j DROP
|
|
-A FORWARD -s 2001:db8::/32 -d 2607:f8b0:4005::200e/128 -j ACCEPT
|
|
COMMIT`
|
|
|
|
// Example nftables output with IPv6
|
|
nftablesRules := `table inet filter {
|
|
chain input {
|
|
type filter hook input priority filter; policy accept;
|
|
ip saddr 192.168.1.1 accept
|
|
ip saddr 44.192.140.1 drop
|
|
ip6 saddr 2607:f8b0:4005::1 drop
|
|
ip6 saddr fd00:1234::1 accept
|
|
}
|
|
chain forward {
|
|
type filter hook forward priority filter; policy accept;
|
|
ip saddr 10.0.0.0/8 drop
|
|
ip saddr 44.192.140.0/24 ip daddr 52.84.12.34/24 accept
|
|
ip6 saddr 2001:db8::/32 ip6 daddr 2607:f8b0:4005::200e/128 accept
|
|
}
|
|
}`
|
|
|
|
anonymizer := anonymize.NewAnonymizer(anonymize.DefaultAddresses())
|
|
|
|
// Test iptables-save anonymization
|
|
anonIptablesSave := anonymizer.AnonymizeString(iptablesSave)
|
|
|
|
// Private IP addresses should remain unchanged
|
|
assert.Contains(t, anonIptablesSave, "192.168.1.0/24")
|
|
assert.Contains(t, anonIptablesSave, "10.0.0.0/8")
|
|
assert.Contains(t, anonIptablesSave, "192.168.100.0/24")
|
|
assert.Contains(t, anonIptablesSave, "192.168.1.10")
|
|
|
|
// Public IP addresses should be anonymized to the default range
|
|
assert.NotContains(t, anonIptablesSave, "44.192.140.1")
|
|
assert.NotContains(t, anonIptablesSave, "44.192.140.0/24")
|
|
assert.NotContains(t, anonIptablesSave, "52.84.12.34")
|
|
assert.Contains(t, anonIptablesSave, "198.51.100.") // Default anonymous range
|
|
|
|
// Structure should be preserved
|
|
assert.Contains(t, anonIptablesSave, "*filter")
|
|
assert.Contains(t, anonIptablesSave, ":INPUT ACCEPT [0:0]")
|
|
assert.Contains(t, anonIptablesSave, "COMMIT")
|
|
assert.Contains(t, anonIptablesSave, "-j MASQUERADE")
|
|
assert.Contains(t, anonIptablesSave, "--dport 80")
|
|
|
|
// Test iptables verbose output anonymization
|
|
anonIptablesVerbose := anonymizer.AnonymizeString(iptablesVerbose)
|
|
|
|
// Private IP addresses should remain unchanged
|
|
assert.Contains(t, anonIptablesVerbose, "192.168.1.0/24")
|
|
assert.Contains(t, anonIptablesVerbose, "10.0.0.0/8")
|
|
|
|
// Public IP addresses should be anonymized to the default range
|
|
assert.NotContains(t, anonIptablesVerbose, "44.192.140.1")
|
|
assert.NotContains(t, anonIptablesVerbose, "44.192.140.0/24")
|
|
assert.NotContains(t, anonIptablesVerbose, "52.84.12.34")
|
|
assert.Contains(t, anonIptablesVerbose, "198.51.100.") // Default anonymous range
|
|
|
|
// Structure and counters should be preserved
|
|
assert.Contains(t, anonIptablesVerbose, "Chain INPUT (policy ACCEPT 0 packets, 0 bytes)")
|
|
assert.Contains(t, anonIptablesVerbose, "100 1024 DROP")
|
|
assert.Contains(t, anonIptablesVerbose, "pkts bytes target")
|
|
|
|
// Test nftables anonymization
|
|
anonNftables := anonymizer.AnonymizeString(nftablesRules)
|
|
|
|
// Private IP addresses should remain unchanged
|
|
assert.Contains(t, anonNftables, "192.168.1.1")
|
|
assert.Contains(t, anonNftables, "10.0.0.0/8")
|
|
|
|
// Public IP addresses should be anonymized to the default range
|
|
assert.NotContains(t, anonNftables, "44.192.140.1")
|
|
assert.NotContains(t, anonNftables, "44.192.140.0/24")
|
|
assert.NotContains(t, anonNftables, "52.84.12.34")
|
|
assert.Contains(t, anonNftables, "198.51.100.") // Default anonymous range
|
|
|
|
// Structure should be preserved
|
|
assert.Contains(t, anonNftables, "table inet filter {")
|
|
assert.Contains(t, anonNftables, "chain input {")
|
|
assert.Contains(t, anonNftables, "type filter hook input priority filter; policy accept;")
|
|
|
|
// IPv6 public addresses in nftables should be anonymized
|
|
assert.NotContains(t, anonNftables, "2607:f8b0:4005::1")
|
|
assert.NotContains(t, anonNftables, "2607:f8b0:4005::200e")
|
|
assert.NotContains(t, anonNftables, "2001:db8::")
|
|
assert.Contains(t, anonNftables, "2001:db8:ffff::") // Default anonymous v6 range
|
|
|
|
// ULA addresses in nftables should be anonymized (global ID is a fingerprint)
|
|
assert.NotContains(t, anonNftables, "fd00:1234::1")
|
|
|
|
// IPv6 nftables structure preserved
|
|
assert.Contains(t, anonNftables, "ip6 saddr")
|
|
assert.Contains(t, anonNftables, "ip6 daddr")
|
|
|
|
// Test ip6tables-save anonymization
|
|
anonIp6tablesSave := anonymizer.AnonymizeString(ip6tablesSave)
|
|
|
|
// ULA IPv6 should be anonymized (global ID is a fingerprint)
|
|
assert.NotContains(t, anonIp6tablesSave, "fd00:1234::1/128")
|
|
|
|
// Public IPv6 addresses should be anonymized
|
|
assert.NotContains(t, anonIp6tablesSave, "2607:f8b0:4005::1")
|
|
assert.NotContains(t, anonIp6tablesSave, "2607:f8b0:4005::200e")
|
|
assert.NotContains(t, anonIp6tablesSave, "2001:db8::")
|
|
assert.Contains(t, anonIp6tablesSave, "2001:db8:ffff::") // Default anonymous v6 range
|
|
|
|
// Structure should be preserved
|
|
assert.Contains(t, anonIp6tablesSave, "*filter")
|
|
assert.Contains(t, anonIp6tablesSave, "COMMIT")
|
|
assert.Contains(t, anonIp6tablesSave, "-j DROP")
|
|
assert.Contains(t, anonIp6tablesSave, "-j ACCEPT")
|
|
}
|