mirror of
https://github.com/netbirdio/netbird.git
synced 2026-06-15 20:39:55 +00:00
* Initial scaffolding * Applies MDM override * Unit tests * Helpers business logic * Return error if trying to modify any config that is gated by MDM * Add ManagedFields to returned config over GetConfig * Adds initial 101 MDM policy business logic testing * gRPC MDM changes * MDM Name scoping for clarity * Implements windows loading of MDM policy * Adds missing WGPort config * Cleanup setupKey to align to linear * Align split tunnel code * Adds some log * Prefix every log with MDM * Adds debug config cobra command This can be useful for troubleshooting and checking config now that its resolution is not trivial defaults > config > env cars > CLI/UI > MDM * Adds MDM 1m diff checker & reloader * Adds also up/start after cancel * Publishes event for UI to sync upon MDM changes * Add events to resync UI to actual config This also provide fixup for UI no aligning to changed config when coming from cli up with config flags. * UI behavior conflicts relaxation UI sends full config snapshot with all values. It doesn't make sense to block it if the values are aligned with the values constrained by the MDM policy. It's just simplier to allow values that are compliant. (this goes for the CLI as well at this point) * Lock toggle Settngs * Advanced Settings locking * Fixup presharedkey * Apply MDM locks * Toggle gray in/out for Advanced Settings * Adds support for disabling of Profiles and UpdateSettings feature flags * Adds Gate Login as well when --disable-update-settings=true is given to service This commit tries to settle things with an old PR-4237 which had relaxed the case where the SetConfig returned an `Unavailable` code error. Under this circumnstance the PR allowed the upFunc to just emit a warning and progress further with the login gRPC. Since the login call is consuming the --management-url coming from the `up` command, it might be possible to abuse the "Unavailable" code to inject a management URL that is different from the configured one even though the --disable-update-settings is set to true (?) * Evaluate disable-update-settings errors only when there's an actual override * [UI] Fixup advanced Settings * [UI] Fixup for preshared key * [UI] Fixup for profile enable/disable toggle We need to align the initial state to evaluate the delta in case. The initial state has to be "true" since the profile starts visible. Then we receive MDM and transition the cache bool value to the actual MDM imposed state * Enforces disable networks * [UI] Aligns to "enable/disable once on change only" * Fixup: MDM wins. always * Removes --disable-advanced-settings It was a typo in our meetings. the actual thing is --disable-update-settings * [PROTO] Removes --disable-advanced-settings * [UI] Removes --disable-advanced-settings * Pins feat profile retrieval to notif event * [UI] Fix for "hide" not working when propagating to parent with children * Adds dep for reading plist files * Introduces support for darwing plist loading * Tests MDM config reload via ticker * [PROVISIONING] ADMX/ADML/PS/bash scripts/templates * CI fixes - Add docstrings to `mdm_integration` - refactor for cognitive complexity - mod tidy * Linting * Add docstrings to `mdm_integration` * nil,nil is no policy and no error. Allow it * nil,nil is no policy and no error. Allow it * exclude MDM profile adminstrated keys data from debug bundle * Fixes Rosenpass left disable after MDM unlock * Partial revert coderabbit added docstrings * Renaming fix * Avoid locking on clientRunning bool when the connection is aborted for whatever reason We want to just signal this through the giveUpChan, we will manage the signal from the waiter side and in case set it to false there. THis way we avoid locking, which should allow the MDM down+wait_for_term_chan_signal_+up procedure clientRunning is used to signal two different conditions here: 1. the initialization procedure is over (we have an engine) 2. the connection being up (or being attempted) Probably these two functionalities should not alias, and the failure of the second condition (because of any error) should just drive a reconnection (currently it's not happening, and we silently go idle). OR, mor probably, the two things are the SAME and there should not exist a case where we did the "Up" initialization and connection attempt but we are not still attempting it. * Moves test helper at te very bottom * Addresses github comments * No lock no copy * Prevents engine not stopping within 10 secs from being paired by another instance We instead juts SKIP updating the policy, so 1. the MDM ticker will kick in 1 minute time, 2. find the policy misaligned, 3. enter the onMDMPolicyChange, 4. find the s.clientRunning == true (because it is set to false only in server cleanupConnection, and not by s.actCancel()) 5. call s.actCancel() again if not nil 6. immediately return from <-s.clientGiveUpChan 7. finally call s.restartEngineForMDMLocked() * Since we ARE running there should be a config If the config was cancelled midflight, connect will abort later on * DisableAutoConnect should not stop a running connection. DisableAutoConnect should just avoid the connection attempts *when the service starts*. If we are started and we are up and running, DisableAutoConnect should not kick in. Another PR will follow about this topic * Removes unused vars * Moves callback into Run method arg * align comment to removal of DisableAutoConnect DisableAutoConnect should just avoid the connection attempts *when the service starts*. If we are started and we are up and running, DisableAutoConnect should not kick in * Removes unused managed_fields data. This was initially used to drive the UI but approach changed to reload config/features upon notifications which makes this data redundant. * Reorder stuff * Unexport unrequired vars/functions PoliciesEqual → policiesEqual AllKeys → allKeys * Adds list of MDM managed fields in the debug bundle
959 lines
31 KiB
Go
959 lines
31 KiB
Go
package debug
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bytes"
|
|
"encoding/json"
|
|
"net"
|
|
"net/netip"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/netbirdio/netbird/client/anonymize"
|
|
"github.com/netbirdio/netbird/client/configs"
|
|
"github.com/netbirdio/netbird/client/internal/profilemanager"
|
|
"github.com/netbirdio/netbird/shared/management/domain"
|
|
mgmProto "github.com/netbirdio/netbird/shared/management/proto"
|
|
"github.com/netbirdio/netbird/shared/netiputil"
|
|
)
|
|
|
|
func mustEncodePrefix(t *testing.T, p netip.Prefix) []byte {
|
|
t.Helper()
|
|
b, err := netiputil.EncodePrefix(p)
|
|
require.NoError(t, err)
|
|
return b
|
|
}
|
|
|
|
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: mustEncodePrefix(t, 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")
|
|
}
|
|
|
|
// TestAddConfig_AllFieldsCovered uses reflection to ensure every field in
|
|
// profilemanager.Config is either rendered in the debug bundle or explicitly
|
|
// excluded. When a new field is added to Config, this test fails until the
|
|
// developer either dumps it in addConfig/addCommonConfigFields or adds it to
|
|
// the excluded set with a justification.
|
|
func TestAddConfig_AllFieldsCovered(t *testing.T) {
|
|
excluded := map[string]string{
|
|
"PrivateKey": "sensitive: WireGuard private key",
|
|
"PreSharedKey": "sensitive: WireGuard pre-shared key",
|
|
"SSHKey": "sensitive: SSH private key",
|
|
"ClientCertKeyPair": "non-config: parsed cert pair, not serialized",
|
|
"policy": "non-config: in-memory MDM policy snapshot, surfaced via Config.Policy() / GetConfigResponse.MDMManagedFields",
|
|
}
|
|
|
|
mURL, _ := url.Parse("https://api.example.com:443")
|
|
aURL, _ := url.Parse("https://admin.example.com:443")
|
|
bTrue := true
|
|
iVal := 42
|
|
cfg := &profilemanager.Config{
|
|
PrivateKey: "priv",
|
|
PreSharedKey: "psk",
|
|
ManagementURL: mURL,
|
|
AdminURL: aURL,
|
|
WgIface: "wt0",
|
|
WgPort: 51820,
|
|
NetworkMonitor: &bTrue,
|
|
IFaceBlackList: []string{"eth0"},
|
|
DisableIPv6Discovery: true,
|
|
RosenpassEnabled: true,
|
|
RosenpassPermissive: true,
|
|
ServerSSHAllowed: &bTrue,
|
|
EnableSSHRoot: &bTrue,
|
|
EnableSSHSFTP: &bTrue,
|
|
EnableSSHLocalPortForwarding: &bTrue,
|
|
EnableSSHRemotePortForwarding: &bTrue,
|
|
DisableSSHAuth: &bTrue,
|
|
SSHJWTCacheTTL: &iVal,
|
|
DisableClientRoutes: true,
|
|
DisableServerRoutes: true,
|
|
DisableDNS: true,
|
|
DisableFirewall: true,
|
|
BlockLANAccess: true,
|
|
BlockInbound: true,
|
|
DisableNotifications: &bTrue,
|
|
DNSLabels: domain.List{},
|
|
SSHKey: "sshkey",
|
|
NATExternalIPs: []string{"1.2.3.4"},
|
|
CustomDNSAddress: "1.1.1.1:53",
|
|
DisableAutoConnect: true,
|
|
DNSRouteInterval: 5 * time.Second,
|
|
ClientCertPath: "/tmp/cert",
|
|
ClientCertKeyPath: "/tmp/key",
|
|
LazyConnectionEnabled: true,
|
|
MTU: 1280,
|
|
}
|
|
|
|
for _, anonymize := range []bool{false, true} {
|
|
t.Run("anonymize="+map[bool]string{true: "true", false: "false"}[anonymize], func(t *testing.T) {
|
|
g := &BundleGenerator{
|
|
anonymizer: newAnonymizerForTest(),
|
|
internalConfig: cfg,
|
|
anonymize: anonymize,
|
|
}
|
|
|
|
var sb strings.Builder
|
|
g.addCommonConfigFields(&sb)
|
|
rendered := sb.String() + renderAddConfigSpecific(g)
|
|
|
|
val := reflect.ValueOf(cfg).Elem()
|
|
typ := val.Type()
|
|
var missing []string
|
|
for i := 0; i < typ.NumField(); i++ {
|
|
name := typ.Field(i).Name
|
|
if _, ok := excluded[name]; ok {
|
|
continue
|
|
}
|
|
if !strings.Contains(rendered, name+":") {
|
|
missing = append(missing, name)
|
|
}
|
|
}
|
|
if len(missing) > 0 {
|
|
t.Fatalf("Config field(s) not present in debug bundle output: %v\n"+
|
|
"Either render the field in addCommonConfigFields/addConfig, "+
|
|
"or add it to the excluded map with a justification.", missing)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// renderAddConfigSpecific renders the fields handled by the anonymize/non-anonymize
|
|
// branches in addConfig (ManagementURL, AdminURL, NATExternalIPs, CustomDNSAddress).
|
|
// addCommonConfigFields covers the rest. Keeping this in the test mirrors the
|
|
// production shape without needing to write an actual zip.
|
|
func renderAddConfigSpecific(g *BundleGenerator) string {
|
|
var sb strings.Builder
|
|
if g.anonymize {
|
|
if g.internalConfig.ManagementURL != nil {
|
|
sb.WriteString("ManagementURL: " + g.anonymizer.AnonymizeURI(g.internalConfig.ManagementURL.String()) + "\n")
|
|
}
|
|
if g.internalConfig.AdminURL != nil {
|
|
sb.WriteString("AdminURL: " + g.anonymizer.AnonymizeURI(g.internalConfig.AdminURL.String()) + "\n")
|
|
}
|
|
sb.WriteString("NATExternalIPs: x\n")
|
|
if g.internalConfig.CustomDNSAddress != "" {
|
|
sb.WriteString("CustomDNSAddress: " + g.anonymizer.AnonymizeString(g.internalConfig.CustomDNSAddress) + "\n")
|
|
}
|
|
} else {
|
|
if g.internalConfig.ManagementURL != nil {
|
|
sb.WriteString("ManagementURL: " + g.internalConfig.ManagementURL.String() + "\n")
|
|
}
|
|
if g.internalConfig.AdminURL != nil {
|
|
sb.WriteString("AdminURL: " + g.internalConfig.AdminURL.String() + "\n")
|
|
}
|
|
sb.WriteString("NATExternalIPs: x\n")
|
|
if g.internalConfig.CustomDNSAddress != "" {
|
|
sb.WriteString("CustomDNSAddress: " + g.internalConfig.CustomDNSAddress + "\n")
|
|
}
|
|
}
|
|
return sb.String()
|
|
}
|
|
|
|
func newAnonymizerForTest() *anonymize.Anonymizer {
|
|
return anonymize.NewAnonymizer(anonymize.DefaultAddresses())
|
|
}
|