Files
netbird/management/internals/controllers/network_map/nmaptest/legacyaccount.go
Pascal Fischer e06c17cf59 [management] network map from nmap data type (#6919)
Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>
Co-authored-by: Dmitri Dolguikh <dmitri.external@netbird.io>
2026-08-27 11:28:05 +02:00

544 lines
19 KiB
Go

package nmaptest
import (
"context"
"strings"
"testing"
"github.com/miekg/dns"
"github.com/stretchr/testify/require"
nbdns "github.com/netbirdio/netbird/dns"
"github.com/netbirdio/netbird/management/internals/controllers/network_map/controller/cache"
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
"github.com/netbirdio/netbird/management/internals/modules/zones"
"github.com/netbirdio/netbird/management/internals/modules/zones/records"
resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types"
routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types"
networkTypes "github.com/netbirdio/netbird/management/server/networks/types"
nbpeer "github.com/netbirdio/netbird/management/server/peer"
"github.com/netbirdio/netbird/management/server/posture"
"github.com/netbirdio/netbird/management/server/types"
"github.com/netbirdio/netbird/management/server/types/legacynmap"
nbroute "github.com/netbirdio/netbird/route"
"github.com/netbirdio/netbird/shared/management/networkmap"
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
"github.com/netbirdio/netbird/shared/management/proto"
sharedtypes "github.com/netbirdio/netbird/shared/management/types"
)
// legacyInput is the account and the four derived arguments main's computation
// took alongside it. The controller resolved them from the account before
// calling; the twin carries them as fields, so the fixture is the source for
// both halves.
type legacyInput struct {
account *types.Account
accountZones []*zones.Zone
validatedPeers map[string]struct{}
resourcePolicies map[string][]*types.Policy
routers map[string]map[string]*routerTypes.NetworkRouter
groupIDToUserIDs map[string][]string
}
// legacyInputFromData rebuilds the Account the fixture stands for. A fixture is
// the value the store returns, and the store's twins carry exactly the state
// the computation reads, so inverting them reproduces the account main would
// have loaded — which is what lets one expectation measure all three paths.
//
// The inverse is only defined for what a twin carries: fields the builders drop
// (peer names, policy descriptions, user records behind AllowedUserIDs) come
// back as the zero value or a minimal stand-in, because no path reads them.
func legacyInputFromData(accountID string, nmData *networkmap.NetworkMapData) legacyInput {
account := &types.Account{
Id: accountID,
Network: accountNetwork(nmData.Network),
Settings: accountSettings(nmData.AccountSettings),
DNSSettings: types.DNSSettings{DisabledManagementGroups: nmData.DNSSettings.DisabledManagementGroups},
Peers: make(map[string]*nbpeer.Peer, len(nmData.Peers)),
Groups: make(map[string]*types.Group, len(nmData.Groups)),
Policies: make([]*types.Policy, 0, len(nmData.Policies)),
Routes: make(map[nbroute.ID]*nbroute.Route, len(nmData.Routes)),
NameServerGroups: make(map[string]*nbdns.NameServerGroup, len(nmData.NameServerGroups)),
NetworkResources: make([]*resourceTypes.NetworkResource, 0, len(nmData.NetworkResources)),
PostureChecks: make([]*posture.Checks, 0, len(nmData.PostureChecks)),
Users: make(map[string]*types.User, len(nmData.AllowedUserIDs)),
Services: accountServices(nmData.Services),
}
for id, p := range nmData.Peers {
account.Peers[id] = accountPeer(id, p)
}
for id, g := range nmData.Groups {
account.Groups[id] = accountGroup(id, g)
}
policiesByID := make(map[string]*types.Policy, len(nmData.Policies))
for _, p := range nmData.Policies {
policy := accountPolicy(p)
if policy == nil {
continue
}
account.Policies = append(account.Policies, policy)
policiesByID[policy.ID] = policy
}
for _, r := range nmData.Routes {
route := accountRoute(r)
if route != nil {
account.Routes[route.ID] = route
}
}
for _, nsg := range nmData.NameServerGroups {
group := accountNSG(nsg)
if group != nil {
account.NameServerGroups[group.ID] = group
}
}
for _, res := range nmData.NetworkResources {
if resource := accountNetworkResource(res); resource != nil {
account.NetworkResources = append(account.NetworkResources, resource)
}
}
for id, pc := range nmData.PostureChecks {
if check := accountPostureChecks(id, pc, nmData.PostureCheckXIDToPublicID[id]); check != nil {
account.PostureChecks = append(account.PostureChecks, check)
}
}
for xid, publicID := range nmData.NetworkXIDToPublicID {
account.Networks = append(account.Networks, &networkTypes.Network{ID: xid, PublicID: publicID})
}
// The twin keeps only the ids of the users a peer may be shared with; the
// legacy side derives the same set from the account's user records, so a
// bare non-blocked regular user per id is enough.
for userID := range nmData.AllowedUserIDs {
account.Users[userID] = &types.User{Id: userID}
}
// Main's network-map controller synthesised the reverse-proxy ACLs onto the
// account and only then derived the resource-policy map, so the frozen copy
// has to be fed in that order to stand for what main produced.
account.Policies = append(account.Policies, legacynmap.SynthesizeProxyPolicies(account)...)
return legacyInput{
account: account,
accountZones: accountZones(nmData.AppliedZoneCandidates),
validatedPeers: nmData.ValidatedPeers,
resourcePolicies: account.GetResourcePoliciesMap(),
routers: accountRouters(nmData.Routers),
groupIDToUserIDs: nmData.GroupIDToUserIDs,
}
}
// computeLegacy runs the fixture through main's frozen path and its own proto
// encoder, the one comparison surface the three modes share.
func computeLegacy(t *testing.T, ctx context.Context, legacy legacyInput, peerID string, zone nmdata.CustomZone, dnsDomain string, dnsFwdPort int64) *proto.NetworkMap {
t.Helper()
require.NotNil(t, legacy.account, "legacy mode needs an account rebuilt from the fixture")
peer := legacy.account.Peers[peerID]
require.NotNil(t, peer, "target peer %q not in rebuilt account", peerID)
nm := legacynmap.GetPeerNetworkMapFromComponents(
legacy.account, ctx, peerID, legacyCustomZone(zone), legacy.accountZones, legacy.validatedPeers,
legacy.resourcePolicies, legacy.routers, nil, legacy.groupIDToUserIDs,
)
require.NotNil(t, nm, "legacy path returned no network map for peer %q", peerID)
return legacynmap.ToProtoNetworkMap(
ctx, peer, nm, dnsDomain, legacy.account.Settings, nil, &cache.DNSConfigCache{}, dnsFwdPort,
)
}
// legacyCustomZone converts the peers custom zone the runner computes once for
// every mode into the shape main's path took.
func legacyCustomZone(z nmdata.CustomZone) nbdns.CustomZone {
zoneRecords := make([]nbdns.SimpleRecord, 0, len(z.Records))
for _, r := range z.Records {
zoneRecords = append(zoneRecords, nbdns.SimpleRecord{
Name: r.Name,
Type: r.Type,
Class: r.Class,
TTL: r.TTL,
RData: r.RData,
})
}
return nbdns.CustomZone{
Domain: z.Domain,
Records: zoneRecords,
SearchDomainDisabled: z.SearchDomainDisabled,
NonAuthoritative: z.NonAuthoritative,
}
}
func accountNetwork(n *nmdata.Network) *types.Network {
if n == nil {
return nil
}
return &types.Network{
Identifier: n.Identifier,
Net: n.Net,
NetV6: n.NetV6,
Dns: n.Dns,
Serial: uint64(n.Serial),
}
}
func accountSettings(s *nmdata.AccountSettingsInfo) *types.Settings {
if s == nil {
return nil
}
return &types.Settings{
PeerLoginExpirationEnabled: s.PeerLoginExpirationEnabled,
PeerLoginExpiration: s.PeerLoginExpiration,
PeerInactivityExpirationEnabled: s.PeerInactivityExpirationEnabled,
PeerInactivityExpiration: s.PeerInactivityExpiration,
DNSDomain: s.DNSDomain,
IPv6EnabledGroups: s.IPv6EnabledGroups,
RoutingPeerDNSResolutionEnabled: s.RoutingPeerDNSResolutionEnabled,
LazyConnectionEnabled: s.LazyConnectionEnabled,
AutoUpdateVersion: s.AutoUpdateVersion,
AutoUpdateAlways: s.AutoUpdateAlways,
MetricsPushEnabled: s.MetricsPushEnabled,
}
}
func accountPeer(id string, p *nmdata.Peer) *nbpeer.Peer {
if p == nil {
return nil
}
networkAddresses := make([]nbpeer.NetworkAddress, 0, len(p.Meta.NetworkAddresses))
for _, na := range p.Meta.NetworkAddresses {
networkAddresses = append(networkAddresses, nbpeer.NetworkAddress{NetIP: na.NetIP})
}
files := make([]nbpeer.File, 0, len(p.Meta.Files))
for _, f := range p.Meta.Files {
files = append(files, nbpeer.File{Path: f.Path, ProcessIsRunning: f.ProcessIsRunning})
}
return &nbpeer.Peer{
ID: id,
Key: p.Key,
SSHKey: p.SSHKey,
DNSLabel: p.DNSLabel,
UserID: p.UserID,
SSHEnabled: p.SSHEnabled,
LoginExpirationEnabled: p.LoginExpirationEnabled,
LastLogin: p.LastLogin,
IP: p.IP,
IPv6: p.IPv6,
ExtraDNSLabels: p.ExtraDNSLabels,
ProxyMeta: nbpeer.ProxyMeta{Embedded: p.ProxyMeta.Embedded, Cluster: p.ProxyMeta.Cluster},
// Connected is what SynthesizePrivateServiceZones gates its records on,
// and a fixture peer stands for a peer the store returned, so it is one
// the account would have reported connected.
Status: &nbpeer.PeerStatus{RequiresApproval: p.RequiresApproval, Connected: true},
Meta: nbpeer.PeerSystemMeta{
WtVersion: p.Meta.WtVersion,
GoOS: p.Meta.GoOS,
OSVersion: p.Meta.OSVersion,
KernelVersion: p.Meta.KernelVersion,
NetworkAddresses: networkAddresses,
Files: files,
Capabilities: p.Meta.Capabilities,
SyncMessageVersion: p.Meta.SyncMessageVersion,
Flags: nbpeer.Flags{
ServerSSHAllowed: p.Meta.Flags.ServerSSHAllowed,
DisableIPv6: p.Meta.Flags.DisableIPv6,
},
},
Location: nbpeer.Location{
CountryCode: p.Location.CountryCode,
CityName: p.Location.CityName,
ConnectionIP: p.Location.ConnectionIP,
},
}
}
func accountGroup(id string, g *nmdata.Group) *types.Group {
if g == nil {
return nil
}
return &types.Group{
ID: id,
Name: g.Name,
PublicID: g.PublicID,
Peers: g.Peers,
}
}
func accountPolicy(p *nmdata.Policy) *types.Policy {
if p == nil {
return nil
}
rules := make([]*types.PolicyRule, 0, len(p.Rules))
for _, r := range p.Rules {
if r == nil {
continue
}
var portRanges []sharedtypes.RulePortRange
if r.PortRanges != nil {
portRanges = make([]sharedtypes.RulePortRange, len(r.PortRanges))
for i, pr := range r.PortRanges {
portRanges[i] = sharedtypes.RulePortRange{Start: pr.Start, End: pr.End}
}
}
rules = append(rules, &types.PolicyRule{
ID: r.ID,
PolicyID: r.PolicyID,
Enabled: r.Enabled,
Action: sharedtypes.PolicyTrafficActionType(r.Action),
Protocol: sharedtypes.PolicyRuleProtocolType(r.Protocol),
Bidirectional: r.Bidirectional,
Sources: r.Sources,
Destinations: r.Destinations,
SourceResource: types.Resource{ID: r.SourceResource.ID, Type: sharedtypes.ResourceType(r.SourceResource.Type)},
DestinationResource: types.Resource{ID: r.DestinationResource.ID, Type: sharedtypes.ResourceType(r.DestinationResource.Type)},
Ports: r.Ports,
PortRanges: portRanges,
AuthorizedGroups: r.AuthorizedGroups,
AuthorizedUser: r.AuthorizedUser,
})
}
return &types.Policy{
ID: p.ID,
PublicID: p.PublicID,
Enabled: p.Enabled,
SourcePostureChecks: p.SourcePostureChecks,
Rules: rules,
}
}
func accountRoute(r *nmdata.Route) *nbroute.Route {
if r == nil {
return nil
}
return &nbroute.Route{
ID: nbroute.ID(r.ID),
AccountID: r.AccountID,
PublicID: r.PublicID,
Network: r.Network,
Domains: r.Domains,
KeepRoute: r.KeepRoute,
NetID: nbroute.NetID(r.NetID),
Description: r.Description,
Peer: r.Peer,
PeerID: r.PeerID,
PeerGroups: r.PeerGroups,
NetworkType: nbroute.NetworkType(r.NetworkType),
Masquerade: r.Masquerade,
Metric: r.Metric,
Enabled: r.Enabled,
Groups: r.Groups,
AccessControlGroups: r.AccessControlGroups,
SkipAutoApply: r.SkipAutoApply,
}
}
func accountNSG(n *nmdata.NameServerGroup) *nbdns.NameServerGroup {
if n == nil {
return nil
}
nameServers := make([]nbdns.NameServer, 0, len(n.NameServers))
for _, ns := range n.NameServers {
nameServers = append(nameServers, nbdns.NameServer{
IP: ns.IP,
NSType: nbdns.NameServerType(ns.NSType),
Port: ns.Port,
})
}
return &nbdns.NameServerGroup{
ID: n.ID,
PublicID: n.PublicID,
Name: n.Name,
Description: n.Description,
NameServers: nameServers,
Groups: n.Groups,
Primary: n.Primary,
Domains: n.Domains,
Enabled: n.Enabled,
SearchDomainsEnabled: n.SearchDomainsEnabled,
}
}
func accountNetworkResource(r *nmdata.NetworkResource) *resourceTypes.NetworkResource {
if r == nil {
return nil
}
return &resourceTypes.NetworkResource{
ID: r.ID,
NetworkID: r.NetworkID,
AccountID: r.AccountID,
PublicID: r.PublicID,
Name: r.Name,
Description: r.Description,
Type: resourceTypes.NetworkResourceType(r.Type),
Address: r.Address,
Domain: r.Domain,
Prefix: r.Prefix,
Enabled: r.Enabled,
}
}
func accountPostureChecks(id string, pc *nmdata.PostureChecks, publicID string) *posture.Checks {
if pc == nil {
return nil
}
out := &posture.Checks{ID: id, PublicID: publicID}
def := pc.Checks
if def.NBVersionCheck != nil {
out.Checks.NBVersionCheck = &posture.NBVersionCheck{MinVersion: def.NBVersionCheck.MinVersion}
}
if def.OSVersionCheck != nil {
oc := &posture.OSVersionCheck{}
if def.OSVersionCheck.Android != nil {
oc.Android = &posture.MinVersionCheck{MinVersion: def.OSVersionCheck.Android.MinVersion}
}
if def.OSVersionCheck.Darwin != nil {
oc.Darwin = &posture.MinVersionCheck{MinVersion: def.OSVersionCheck.Darwin.MinVersion}
}
if def.OSVersionCheck.Ios != nil {
oc.Ios = &posture.MinVersionCheck{MinVersion: def.OSVersionCheck.Ios.MinVersion}
}
if def.OSVersionCheck.Linux != nil {
oc.Linux = &posture.MinKernelVersionCheck{MinKernelVersion: def.OSVersionCheck.Linux.MinKernelVersion}
}
if def.OSVersionCheck.Windows != nil {
oc.Windows = &posture.MinKernelVersionCheck{MinKernelVersion: def.OSVersionCheck.Windows.MinKernelVersion}
}
out.Checks.OSVersionCheck = oc
}
if def.GeoLocationCheck != nil {
gc := &posture.GeoLocationCheck{Action: def.GeoLocationCheck.Action}
for _, loc := range def.GeoLocationCheck.Locations {
gc.Locations = append(gc.Locations, posture.Location{CountryCode: loc.CountryCode, CityName: loc.CityName})
}
out.Checks.GeoLocationCheck = gc
}
if def.PeerNetworkRangeCheck != nil {
out.Checks.PeerNetworkRangeCheck = &posture.PeerNetworkRangeCheck{
Action: def.PeerNetworkRangeCheck.Action,
Ranges: def.PeerNetworkRangeCheck.Ranges,
}
}
if def.ProcessCheck != nil {
procs := make([]posture.Process, 0, len(def.ProcessCheck.Processes))
for _, p := range def.ProcessCheck.Processes {
procs = append(procs, posture.Process{LinuxPath: p.LinuxPath, MacPath: p.MacPath, WindowsPath: p.WindowsPath})
}
out.Checks.ProcessCheck = &posture.ProcessCheck{Processes: procs}
}
return out
}
func accountServices(services []*nmdata.Service) []*service.Service {
if len(services) == 0 {
return nil
}
out := make([]*service.Service, 0, len(services))
for _, svc := range services {
if svc == nil {
continue
}
targets := make([]*service.Target, 0, len(svc.Targets))
for _, t := range svc.Targets {
if t == nil {
continue
}
target := &service.Target{
Enabled: t.Enabled,
Port: t.Port,
Protocol: t.Protocol,
TargetId: t.TargetID,
TargetType: service.TargetType(t.TargetType),
}
if t.Path != "" {
path := t.Path
target.Path = &path
}
targets = append(targets, target)
}
out = append(out, &service.Service{
ID: svc.ID,
Enabled: svc.Enabled,
Private: svc.Private,
Mode: svc.Mode,
ProxyCluster: svc.ProxyCluster,
AccessGroups: svc.AccessGroups,
Targets: targets,
})
}
return out
}
// accountZones inverts buildAppliedZoneCandidates. Records come back with the
// record type the builder mapped them from; a candidate only ever carries the
// three types it converts.
func accountZones(candidates []networkmap.AppliedZoneCandidate) []*zones.Zone {
if len(candidates) == 0 {
return nil
}
out := make([]*zones.Zone, 0, len(candidates))
for _, candidate := range candidates {
zoneRecords := make([]*records.Record, 0, len(candidate.Zone.Records))
for _, r := range candidate.Zone.Records {
recordType, ok := zoneRecordType(r.Type)
if !ok {
continue
}
zoneRecords = append(zoneRecords, &records.Record{
Name: strings.TrimSuffix(r.Name, "."),
Type: recordType,
Content: r.RData,
TTL: r.TTL,
})
}
out = append(out, &zones.Zone{
ID: candidate.Zone.Domain,
Domain: strings.TrimSuffix(candidate.Zone.Domain, "."),
Enabled: true,
EnableSearchDomain: !candidate.Zone.SearchDomainDisabled,
DistributionGroups: candidate.DistributionGroups,
Records: zoneRecords,
})
}
return out
}
func zoneRecordType(recordType int) (records.RecordType, bool) {
switch uint16(recordType) {
case dns.TypeA:
return records.RecordTypeA, true
case dns.TypeAAAA:
return records.RecordTypeAAAA, true
case dns.TypeCNAME:
return records.RecordTypeCNAME, true
default:
return "", false
}
}
func accountRouters(routers map[string]map[string]*nmdata.NetworkRouter) map[string]map[string]*routerTypes.NetworkRouter {
if len(routers) == 0 {
return nil
}
out := make(map[string]map[string]*routerTypes.NetworkRouter, len(routers))
for networkID, inner := range routers {
converted := make(map[string]*routerTypes.NetworkRouter, len(inner))
for peerID, router := range inner {
if router == nil {
continue
}
converted[peerID] = &routerTypes.NetworkRouter{
NetworkID: networkID,
PublicID: router.PublicID,
Peer: peerID,
PeerGroups: router.PeerGroups,
Masquerade: router.Masquerade,
Metric: router.Metric,
Enabled: router.Enabled,
}
}
out[networkID] = converted
}
return out
}