mirror of
https://github.com/netbirdio/netbird.git
synced 2026-08-29 11:01:29 +02:00
381 lines
9.9 KiB
Go
381 lines
9.9 KiB
Go
package nmaptest
|
|
|
|
import (
|
|
"bytes"
|
|
"cmp"
|
|
"fmt"
|
|
"slices"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/netbirdio/netbird/shared/management/proto"
|
|
)
|
|
|
|
// normalizeIDSpace replaces policy and route identifiers with positional
|
|
// placeholders so a comparison can reach everything else.
|
|
//
|
|
// This exists only because the envelope round-trip currently substitutes each
|
|
// internal xid with the object's public id, which is a tracked defect and not a
|
|
// licence to differ: those identifiers reach the server again inside flow
|
|
// events, which resolve them by internal id, so the substitution silently
|
|
// breaks flow attribution for component-format peers. TestIDSpaceMatches
|
|
// asserts the equality that must eventually hold; this erasure keeps the other
|
|
// 40-odd cases reporting on semantics meanwhile. When the id space is unified,
|
|
// delete this and the calls to it — every case should still pass.
|
|
//
|
|
// Cardinality and cross-references survive the erasure: two rules under one
|
|
// policy still share a token and a route firewall rule still points at its
|
|
// route, so a path that drops a policy, merges two policies, or misattributes a
|
|
// rule to the wrong route still fails.
|
|
func normalizeIDSpace(nm *proto.NetworkMap) {
|
|
if nm == nil {
|
|
return
|
|
}
|
|
policies := newTokenizer("policy")
|
|
routes := newTokenizer("route")
|
|
|
|
for _, i := range orderBy(nm.Routes, routeKeyWithoutID) {
|
|
nm.Routes[i].ID = routes.get(nm.Routes[i].ID)
|
|
}
|
|
for _, i := range orderBy(nm.FirewallRules, firewallKeyWithoutPolicy) {
|
|
r := nm.FirewallRules[i]
|
|
if len(r.PolicyID) > 0 {
|
|
r.PolicyID = []byte(policies.get(string(r.PolicyID)))
|
|
}
|
|
}
|
|
for _, i := range orderBy(nm.RoutesFirewallRules, routeFirewallKeyWithoutIDs) {
|
|
r := nm.RoutesFirewallRules[i]
|
|
if len(r.PolicyID) > 0 {
|
|
r.PolicyID = []byte(policies.get(string(r.PolicyID)))
|
|
}
|
|
r.RouteID = routes.get(r.RouteID)
|
|
}
|
|
}
|
|
|
|
// tokenizer maps identifiers to positional placeholders in order of first use.
|
|
type tokenizer struct {
|
|
prefix string
|
|
seen map[string]string
|
|
}
|
|
|
|
func newTokenizer(prefix string) *tokenizer {
|
|
return &tokenizer{prefix: prefix, seen: make(map[string]string)}
|
|
}
|
|
|
|
func (t *tokenizer) get(id string) string {
|
|
if id == "" {
|
|
return ""
|
|
}
|
|
if tok, ok := t.seen[id]; ok {
|
|
return tok
|
|
}
|
|
tok := fmt.Sprintf("%s#%d", t.prefix, len(t.seen))
|
|
t.seen[id] = tok
|
|
return tok
|
|
}
|
|
|
|
// orderBy returns indices sorted by key, so placeholder numbering does not
|
|
// depend on the identifiers being erased.
|
|
func orderBy[T any](items []T, key func(T) string) []int {
|
|
idx := make([]int, len(items))
|
|
for i := range idx {
|
|
idx[i] = i
|
|
}
|
|
sort.SliceStable(idx, func(a, b int) bool { return key(items[idx[a]]) < key(items[idx[b]]) })
|
|
return idx
|
|
}
|
|
|
|
func routeKeyWithoutID(r *proto.Route) string {
|
|
if r == nil {
|
|
return ""
|
|
}
|
|
return fmt.Sprintf("%s|%s|%s|%d|%d|%t|%t|%v",
|
|
r.Network, r.NetID, r.Peer, r.Metric, r.NetworkType, r.Masquerade, r.KeepRoute, r.Domains)
|
|
}
|
|
|
|
func firewallKeyWithoutPolicy(r *proto.FirewallRule) string {
|
|
if r == nil {
|
|
return ""
|
|
}
|
|
return fmt.Sprintf("%s|%d|%d|%d|%s|%s|%v",
|
|
r.PeerIP, r.Direction, r.Action, r.Protocol, r.Port, portInfoKey(r.PortInfo), r.SourcePrefixes) //nolint:staticcheck
|
|
}
|
|
|
|
func routeFirewallKeyWithoutIDs(r *proto.RouteFirewallRule) string {
|
|
if r == nil {
|
|
return ""
|
|
}
|
|
return fmt.Sprintf("%s|%d|%d|%s|%v|%v|%t|%d",
|
|
r.Destination, r.Protocol, r.Action, portInfoKey(r.PortInfo), r.Domains, r.SourceRanges, r.IsDynamic, r.CustomProtocol)
|
|
}
|
|
|
|
// canonicalize sorts every repeated field of the NetworkMap by a stable key.
|
|
// The producing paths iterate Go maps while building these slices, so order
|
|
// can differ between runs even when the content is identical; comparing
|
|
// without this reports noise.
|
|
func canonicalize(nm *proto.NetworkMap) {
|
|
if nm == nil {
|
|
return
|
|
}
|
|
slices.SortFunc(nm.RemotePeers, cmpRemotePeer)
|
|
slices.SortFunc(nm.OfflinePeers, cmpRemotePeer)
|
|
slices.SortFunc(nm.Routes, cmpRoute)
|
|
slices.SortFunc(nm.FirewallRules, cmpFirewallRule)
|
|
slices.SortFunc(nm.RoutesFirewallRules, cmpRouteFirewallRule)
|
|
slices.SortFunc(nm.ForwardingRules, cmpForwardingRule)
|
|
|
|
for _, r := range nm.FirewallRules {
|
|
slices.SortFunc(r.SourcePrefixes, bytes.Compare)
|
|
}
|
|
for _, r := range nm.RoutesFirewallRules {
|
|
slices.Sort(r.SourceRanges)
|
|
}
|
|
canonicalizeDNSConfig(nm.DNSConfig)
|
|
canonicalizeSSHAuth(nm.SshAuth)
|
|
}
|
|
|
|
func canonicalizeDNSConfig(d *proto.DNSConfig) {
|
|
if d == nil {
|
|
return
|
|
}
|
|
for _, g := range d.NameServerGroups {
|
|
if g == nil {
|
|
continue
|
|
}
|
|
slices.Sort(g.Domains)
|
|
slices.SortFunc(g.NameServers, func(a, b *proto.NameServer) int {
|
|
if a == nil || b == nil {
|
|
return boolCmp(a == nil, b == nil)
|
|
}
|
|
if c := cmp.Compare(a.IP, b.IP); c != 0 {
|
|
return c
|
|
}
|
|
if c := cmp.Compare(a.Port, b.Port); c != 0 {
|
|
return c
|
|
}
|
|
return cmp.Compare(a.NSType, b.NSType)
|
|
})
|
|
}
|
|
slices.SortFunc(d.NameServerGroups, func(a, b *proto.NameServerGroup) int {
|
|
return cmp.Compare(nsgKey(a), nsgKey(b))
|
|
})
|
|
for _, z := range d.CustomZones {
|
|
if z == nil {
|
|
continue
|
|
}
|
|
slices.SortFunc(z.Records, cmpSimpleRecord)
|
|
}
|
|
slices.SortFunc(d.CustomZones, func(a, b *proto.CustomZone) int {
|
|
if a == nil || b == nil {
|
|
return boolCmp(a == nil, b == nil)
|
|
}
|
|
return cmp.Compare(a.Domain, b.Domain)
|
|
})
|
|
}
|
|
|
|
// canonicalizeSSHAuth sorts AuthorizedUsers and re-keys MachineUsers.Indexes
|
|
// against the new ordering, preserving which machine user maps to which hashes.
|
|
func canonicalizeSSHAuth(s *proto.SSHAuth) {
|
|
if s == nil || len(s.AuthorizedUsers) == 0 {
|
|
return
|
|
}
|
|
type hashed struct {
|
|
bytes []byte
|
|
old uint32
|
|
}
|
|
entries := make([]hashed, len(s.AuthorizedUsers))
|
|
for i, b := range s.AuthorizedUsers {
|
|
entries[i] = hashed{bytes: b, old: uint32(i)}
|
|
}
|
|
slices.SortFunc(entries, func(a, b hashed) int { return bytes.Compare(a.bytes, b.bytes) })
|
|
|
|
remap := make(map[uint32]uint32, len(entries))
|
|
sorted := make([][]byte, len(entries))
|
|
for newIdx, e := range entries {
|
|
remap[e.old] = uint32(newIdx)
|
|
sorted[newIdx] = e.bytes
|
|
}
|
|
s.AuthorizedUsers = sorted
|
|
|
|
for _, mu := range s.MachineUsers {
|
|
if mu == nil {
|
|
continue
|
|
}
|
|
for i, oldIdx := range mu.Indexes {
|
|
if newIdx, ok := remap[oldIdx]; ok {
|
|
mu.Indexes[i] = newIdx
|
|
}
|
|
}
|
|
slices.Sort(mu.Indexes)
|
|
}
|
|
}
|
|
|
|
func boolCmp(a, b bool) int {
|
|
if a == b {
|
|
return 0
|
|
}
|
|
if a {
|
|
return 1
|
|
}
|
|
return -1
|
|
}
|
|
|
|
func nsgKey(g *proto.NameServerGroup) string {
|
|
if g == nil {
|
|
return ""
|
|
}
|
|
var parts []string
|
|
for _, ns := range g.NameServers {
|
|
if ns == nil {
|
|
continue
|
|
}
|
|
parts = append(parts, ns.IP+":"+strconv.FormatInt(ns.Port, 10)+":"+strconv.FormatInt(ns.NSType, 10))
|
|
}
|
|
slices.Sort(parts)
|
|
key := strings.Join(parts, ",")
|
|
domains := append([]string(nil), g.Domains...)
|
|
slices.Sort(domains)
|
|
key += "|" + strings.Join(domains, "|")
|
|
if g.Primary {
|
|
key += "|P"
|
|
}
|
|
if g.SearchDomainsEnabled {
|
|
key += "|S"
|
|
}
|
|
return key
|
|
}
|
|
|
|
func cmpSimpleRecord(a, b *proto.SimpleRecord) int {
|
|
if a == nil || b == nil {
|
|
return boolCmp(a == nil, b == nil)
|
|
}
|
|
if c := cmp.Compare(a.Name, b.Name); c != 0 {
|
|
return c
|
|
}
|
|
if c := cmp.Compare(a.Type, b.Type); c != 0 {
|
|
return c
|
|
}
|
|
if c := cmp.Compare(a.Class, b.Class); c != 0 {
|
|
return c
|
|
}
|
|
if c := cmp.Compare(a.RData, b.RData); c != 0 {
|
|
return c
|
|
}
|
|
return cmp.Compare(a.TTL, b.TTL)
|
|
}
|
|
|
|
func cmpRemotePeer(a, b *proto.RemotePeerConfig) int {
|
|
if a == nil || b == nil {
|
|
return boolCmp(a == nil, b == nil)
|
|
}
|
|
return cmp.Compare(a.WgPubKey, b.WgPubKey)
|
|
}
|
|
|
|
func cmpRoute(a, b *proto.Route) int {
|
|
if a == nil || b == nil {
|
|
return boolCmp(a == nil, b == nil)
|
|
}
|
|
if c := cmp.Compare(a.ID, b.ID); c != 0 {
|
|
return c
|
|
}
|
|
if c := cmp.Compare(a.NetID, b.NetID); c != 0 {
|
|
return c
|
|
}
|
|
if c := cmp.Compare(a.Network, b.Network); c != 0 {
|
|
return c
|
|
}
|
|
if c := cmp.Compare(a.Peer, b.Peer); c != 0 {
|
|
return c
|
|
}
|
|
if c := cmp.Compare(a.Metric, b.Metric); c != 0 {
|
|
return c
|
|
}
|
|
return slices.Compare(a.Domains, b.Domains)
|
|
}
|
|
|
|
func cmpFirewallRule(a, b *proto.FirewallRule) int {
|
|
if a == nil || b == nil {
|
|
return boolCmp(a == nil, b == nil)
|
|
}
|
|
if c := bytes.Compare(a.PolicyID, b.PolicyID); c != 0 {
|
|
return c
|
|
}
|
|
if c := cmp.Compare(a.PeerIP, b.PeerIP); c != 0 { //nolint:staticcheck
|
|
return c
|
|
}
|
|
if c := cmp.Compare(int32(a.Direction), int32(b.Direction)); c != 0 {
|
|
return c
|
|
}
|
|
if c := cmp.Compare(int32(a.Action), int32(b.Action)); c != 0 {
|
|
return c
|
|
}
|
|
if c := cmp.Compare(int32(a.Protocol), int32(b.Protocol)); c != 0 {
|
|
return c
|
|
}
|
|
if c := cmp.Compare(a.Port, b.Port); c != 0 {
|
|
return c
|
|
}
|
|
return cmp.Compare(portInfoKey(a.PortInfo), portInfoKey(b.PortInfo))
|
|
}
|
|
|
|
func cmpRouteFirewallRule(a, b *proto.RouteFirewallRule) int {
|
|
if a == nil || b == nil {
|
|
return boolCmp(a == nil, b == nil)
|
|
}
|
|
if c := bytes.Compare(a.PolicyID, b.PolicyID); c != 0 {
|
|
return c
|
|
}
|
|
if c := cmp.Compare(a.RouteID, b.RouteID); c != 0 {
|
|
return c
|
|
}
|
|
if c := cmp.Compare(a.Destination, b.Destination); c != 0 {
|
|
return c
|
|
}
|
|
if c := cmp.Compare(int32(a.Protocol), int32(b.Protocol)); c != 0 {
|
|
return c
|
|
}
|
|
if c := cmp.Compare(portInfoKey(a.PortInfo), portInfoKey(b.PortInfo)); c != 0 {
|
|
return c
|
|
}
|
|
if c := cmp.Compare(int32(a.Action), int32(b.Action)); c != 0 {
|
|
return c
|
|
}
|
|
if c := slices.Compare(a.Domains, b.Domains); c != 0 {
|
|
return c
|
|
}
|
|
if c := slices.Compare(a.SourceRanges, b.SourceRanges); c != 0 {
|
|
return c
|
|
}
|
|
if c := cmp.Compare(a.CustomProtocol, b.CustomProtocol); c != 0 {
|
|
return c
|
|
}
|
|
return boolCmp(a.IsDynamic, b.IsDynamic)
|
|
}
|
|
|
|
func cmpForwardingRule(a, b *proto.ForwardingRule) int {
|
|
if a == nil || b == nil {
|
|
return boolCmp(a == nil, b == nil)
|
|
}
|
|
if c := cmp.Compare(int32(a.Protocol), int32(b.Protocol)); c != 0 {
|
|
return c
|
|
}
|
|
return bytes.Compare(a.TranslatedAddress, b.TranslatedAddress)
|
|
}
|
|
|
|
func portInfoKey(pi *proto.PortInfo) string {
|
|
if pi == nil {
|
|
return ""
|
|
}
|
|
switch sel := pi.PortSelection.(type) {
|
|
case *proto.PortInfo_Port:
|
|
return "P" + strconv.FormatUint(uint64(sel.Port), 10)
|
|
case *proto.PortInfo_Range_:
|
|
if sel.Range == nil {
|
|
return "R"
|
|
}
|
|
return "R" + strconv.FormatUint(uint64(sel.Range.Start), 10) + "-" + strconv.FormatUint(uint64(sel.Range.End), 10)
|
|
}
|
|
return ""
|
|
}
|