[management] optimize posture cache (#8366)

This commit is contained in:
Pascal Fischer
2026-10-26 16:03:33 +01:00
committed by GitHub
parent afa6eec6a7
commit 2c68a84a97
11 changed files with 690 additions and 182 deletions
@@ -489,9 +489,8 @@ func (c *Controller) getNetworkMapData(ctx context.Context, accountID string) *n
// fetchNetworkMapData reads the twin once per buffer window. Its result is // fetchNetworkMapData reads the twin once per buffer window. Its result is
// shared by every waiter of that window, so the mutating steps run here, before // shared by every waiter of that window, so the mutating steps run here, before
// it is handed out: the twin the callers see is read-only. Injected proxy // it is handed out: the twin the callers see is read-only. Posture checks are
// policies carry no posture checks, so precomputing after the injection yields // evaluated lazily by the components calculation.
// the same validation as precomputing before it.
func (c *Controller) fetchNetworkMapData(ctx context.Context, accountID string) (*networkmap.NetworkMapData, error) { func (c *Controller) fetchNetworkMapData(ctx context.Context, accountID string) (*networkmap.NetworkMapData, error) {
nmData, err := c.nmdataStore.GetNetworkMapData(ctx, accountID) nmData, err := c.nmdataStore.GetNetworkMapData(ctx, accountID)
if err != nil { if err != nil {
@@ -501,7 +500,6 @@ func (c *Controller) fetchNetworkMapData(ctx context.Context, accountID string)
nmData.Services = c.proxyServicesFromRepo(ctx, accountID) nmData.Services = c.proxyServicesFromRepo(ctx, accountID)
nmData.BuildPrivateServiceCandidates() nmData.BuildPrivateServiceCandidates()
nmData.InjectProxyPolicies() nmData.InjectProxyPolicies()
nmData.PrecomputePostureValidation()
return nmData, nil return nmData, nil
} }
@@ -184,7 +184,6 @@ func RunCase(t *testing.T, c Case) {
nmData := c.Data nmData := c.Data
applyFixtureDefaults(nmData) applyFixtureDefaults(nmData)
nmData.BuildPrivateServiceCandidates() nmData.BuildPrivateServiceCandidates()
nmData.PrecomputePostureValidation()
dnsDomain := c.DNSDomain dnsDomain := c.DNSDomain
if nmData.AccountSettings.DNSDomain != "" { if nmData.AccountSettings.DNSDomain != "" {
@@ -175,6 +175,24 @@ func TestChainMatchesCAs(t *testing.T) {
assert.False(t, ChainMatchesCAs("not a pem", []string{ca.PEM}, now)) assert.False(t, ChainMatchesCAs("not a pem", []string{ca.PEM}, now))
} }
func TestAnyChainMatchesCAs(t *testing.T) {
ca := certtest.NewCA(t, "root")
otherCA := certtest.NewCA(t, "other-root")
chainPEM := EncodeChainPEM([]*x509.Certificate{ca.Issue(t, certtest.ECDSAKey(t), "device")})
otherPEM := EncodeChainPEM([]*x509.Certificate{otherCA.Issue(t, certtest.ECDSAKey(t), "device")})
assert.True(t, AnyChainMatchesCAs([]string{otherPEM, chainPEM}, []string{ca.PEM}, now))
assert.False(t, AnyChainMatchesCAs([]string{otherPEM}, []string{ca.PEM}, now))
assert.False(t, AnyChainMatchesCAs(nil, []string{ca.PEM}, now))
assert.False(t, AnyChainMatchesCAs([]string{chainPEM}, []string{"not a pem"}, now))
roots, err := ParseCAs([]string{ca.PEM})
require.NoError(t, err)
assert.True(t, AnyChainMatchesPool([]string{otherPEM, chainPEM}, roots, now))
assert.False(t, AnyChainMatchesPool([]string{otherPEM}, roots, now))
assert.False(t, AnyChainMatchesPool(nil, roots, now))
}
func TestChainPEM_RoundTrip(t *testing.T) { func TestChainPEM_RoundTrip(t *testing.T) {
ca := certtest.NewCA(t, "root") ca := certtest.NewCA(t, "root")
leaf := ca.Issue(t, certtest.ECDSAKey(t), "device") leaf := ca.Issue(t, certtest.ECDSAKey(t), "device")
+9
View File
@@ -68,10 +68,19 @@ func ChainMatchesCAs(chainPEM string, caPEMs []string, now time.Time) bool {
// AnyChainMatchesCAs reports whether at least one of the peer's verified chains // AnyChainMatchesCAs reports whether at least one of the peer's verified chains
// is anchored in one of the configured CAs. // is anchored in one of the configured CAs.
func AnyChainMatchesCAs(chainPEMs, caPEMs []string, now time.Time) bool { func AnyChainMatchesCAs(chainPEMs, caPEMs []string, now time.Time) bool {
if len(chainPEMs) == 0 {
return false
}
roots, err := ParseCAs(caPEMs) roots, err := ParseCAs(caPEMs)
if err != nil { if err != nil {
return false return false
} }
return AnyChainMatchesPool(chainPEMs, roots, now)
}
// AnyChainMatchesPool is AnyChainMatchesCAs over an already parsed root pool, for
// callers that verify many peers against the same CAs.
func AnyChainMatchesPool(chainPEMs []string, roots *x509.CertPool, now time.Time) bool {
for _, chainPEM := range chainPEMs { for _, chainPEM := range chainPEMs {
if chainMatchesPool(chainPEM, roots, now) { if chainMatchesPool(chainPEM, roots, now) {
return true return true
+239
View File
@@ -0,0 +1,239 @@
package networkmap
import (
"slices"
"sync"
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
"github.com/netbirdio/netbird/shared/management/types"
)
// admissionCache memoises the peer-independent parts of the components calculation:
// the posture-check verdicts and the admitted peers of every policy side. Its index is
// built once per NetworkMapData and every entry is filled on first use, so lookups
// take no lock and nothing is computed for checks or policies no peer needs. The
// NetworkMapData must not change once the index is built.
type admissionCache struct {
policies map[*nmdata.Policy]*policyAdmission
verdicts map[string]*postureVerdicts
ruleFailuresOnce sync.Once
ruleFailures map[string]map[string]struct{}
}
// policyAdmission holds the admissions of one policy: the union of its source sides,
// used by the network resource loop, and the sides of each rule, in rule order.
type policyAdmission struct {
sources admittedPeers
rules []ruleAdmission
}
type ruleAdmission struct {
sources admittedPeers
destinations admittedPeers
}
// admittedPeers is the admission of one policy side: the peers that exist, are
// validated and pass the side's posture checks and, keyed by posture check ID, the
// peers a check rejected. Both are shared and must not be mutated.
type admittedPeers struct {
once sync.Once
peers []string
failed map[string]map[string]struct{}
}
// postureVerdicts holds the results of one posture check keyed by peer ID. A nil
// map marks a check ID that resolves to no posture check, which passes.
type postureVerdicts struct {
once sync.Once
results map[string]bool
}
func (nmd *NetworkMapData) admissions() *admissionCache {
nmd.admissionsOnce.Do(func() {
nmd.InjectProxyPolicies()
c := &admissionCache{
policies: make(map[*nmdata.Policy]*policyAdmission, len(nmd.Policies)),
verdicts: make(map[string]*postureVerdicts, len(nmd.PostureChecks)),
}
for _, policy := range nmd.Policies {
c.addPolicy(policy)
}
for _, policies := range nmd.ResourcePolicies {
for _, policy := range policies {
c.addPolicy(policy)
}
}
for id := range nmd.PostureChecks {
c.verdicts[id] = &postureVerdicts{}
}
nmd.admissionsIdx = c
})
return nmd.admissionsIdx
}
func (c *admissionCache) addPolicy(policy *nmdata.Policy) {
if policy == nil {
return
}
if _, ok := c.policies[policy]; ok {
return
}
c.policies[policy] = &policyAdmission{rules: make([]ruleAdmission, len(policy.Rules))}
}
// ruleAdmissions returns the admitted source and destination peers of the policy's
// rule at index i.
func (nmd *NetworkMapData) ruleAdmissions(policy *nmdata.Policy, i int) (*admittedPeers, *admittedPeers) {
rule := policy.Rules[i]
ra := &nmd.admissions().policies[policy].rules[i]
src := ra.sources.fill(func(failed *map[string]map[string]struct{}) []string {
return nmd.admitSide(rule.Sources, rule.SourceResource, policy.SourcePostureChecks, failed)
})
dst := ra.destinations.fill(func(failed *map[string]map[string]struct{}) []string {
return nmd.admitSide(rule.Destinations, rule.DestinationResource, nil, failed)
})
return src, dst
}
// resourcePolicySources returns the admitted source peers of a resource policy across
// all its rules, shared by every routing peer of the policy's resources.
func (nmd *NetworkMapData) resourcePolicySources(policy *nmdata.Policy) *admittedPeers {
pa := nmd.admissions().policies[policy]
return pa.sources.fill(func(failed *map[string]map[string]struct{}) []string {
if resource := policy.Rules[0].SourceResource; isPeerResource(resource) {
return nmd.getPeerFromResource(resource, policy.SourcePostureChecks, failed)
}
var admitted []string
for _, pid := range nmd.getUniquePeerIDsFromGroupsIDs(policy.SourceGroups()) {
if nmd.admitPolicyPeer(pid, policy.SourcePostureChecks, failed) {
admitted = append(admitted, pid)
}
}
return admitted
})
}
func (a *admittedPeers) fill(admit func(failed *map[string]map[string]struct{}) []string) *admittedPeers {
a.once.Do(func() {
a.failed = make(map[string]map[string]struct{})
a.peers = admit(&a.failed)
})
return a
}
func (nmd *NetworkMapData) admitSide(groups []string, resource nmdata.Resource, postureChecksIDs []string, failed *map[string]map[string]struct{}) []string {
if isPeerResource(resource) {
return nmd.getPeerFromResource(resource, postureChecksIDs, failed)
}
return nmd.getPeersFromGroups(groups, postureChecksIDs, failed)
}
// ruleFailures returns the posture failures of every enabled rule side. Every peer's
// rule loop walks all of them, so they are merged once per NetworkMapData.
func (nmd *NetworkMapData) ruleFailures() map[string]map[string]struct{} {
c := nmd.admissions()
c.ruleFailuresOnce.Do(func() {
failures := make(map[string]map[string]struct{})
for _, policy := range nmd.Policies {
if policy == nil || !policy.Enabled {
continue
}
for i, rule := range policy.Rules {
if rule == nil || !rule.Enabled {
continue
}
sources, destinations := nmd.ruleAdmissions(policy, i)
mergePostureFailures(&failures, sources.failed)
mergePostureFailures(&failures, destinations.failed)
}
}
c.ruleFailures = failures
})
return c.ruleFailures
}
// sideAdmitsPeer reports whether the peer is an admitted member of a rule side,
// resolving membership like getPeersFromGroups without walking the group members.
func (nmd *NetworkMapData) sideAdmitsPeer(groups []string, resource nmdata.Resource, postureChecksIDs []string, peerID string, peerGroups map[string]struct{}) bool {
member := false
if isPeerResource(resource) {
member = resource.ID == peerID
} else {
for _, gid := range groups {
group := nmd.Groups[gid]
if group == nil {
continue
}
_, inGroup := peerGroups[gid]
if group.IsGroupAll() || len(groups) == 1 {
member = inGroup
break
}
member = member || inGroup
}
}
if !member {
return false
}
admitted, _ := nmd.admitsPeer(peerID, postureChecksIDs)
return admitted
}
// postureVerdict returns the cached result of a posture check for the peer and
// whether one is cached. The results of a check are evaluated on first use for the
// source peers of every enabled policy using it, or taken from PostureValidation
// when it was precomputed.
func (nmd *NetworkMapData) postureVerdict(postureChecksID, peerID string) (bool, bool) {
v, ok := nmd.admissions().verdicts[postureChecksID]
if !ok {
return false, false
}
v.once.Do(func() {
if results, seeded := nmd.PostureValidation[postureChecksID]; seeded {
v.results = results
return
}
v.results = nmd.evaluatePostureChecksForPeers(postureChecksID, nmd.postureCheckPeerIDs(postureChecksID))
})
if v.results == nil {
return true, true
}
valid, found := v.results[peerID]
return valid, found
}
// postureCheckPeerIDs collects the source peers of every enabled policy using the
// posture check.
func (nmd *NetworkMapData) postureCheckPeerIDs(postureChecksID string) map[string]struct{} {
peerIDs := make(map[string]struct{})
for _, policy := range nmd.Policies {
if policy == nil || !policy.Enabled || !slices.Contains(policy.SourcePostureChecks, postureChecksID) {
continue
}
for _, pid := range nmd.getUniquePeerIDsFromGroupsIDs(policy.SourceGroups()) {
peerIDs[pid] = struct{}{}
}
for _, rule := range policy.Rules {
if rule != nil && isPeerResource(rule.SourceResource) {
peerIDs[rule.SourceResource.ID] = struct{}{}
}
}
}
return peerIDs
}
func isPeerResource(resource nmdata.Resource) bool {
return resource.Type == string(types.ResourceTypePeer) && resource.ID != ""
}
func mergePostureFailures(dst *map[string]map[string]struct{}, failed map[string]map[string]struct{}) {
for checkID, peerIDs := range failed {
if _, ok := (*dst)[checkID]; !ok {
(*dst)[checkID] = make(map[string]struct{}, len(peerIDs))
}
for pid := range peerIDs {
(*dst)[checkID][pid] = struct{}{}
}
}
}
+122 -161
View File
@@ -1,8 +1,6 @@
package networkmap package networkmap
import ( import (
"slices"
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata" "github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
"github.com/netbirdio/netbird/shared/management/types" "github.com/netbirdio/netbird/shared/management/types"
) )
@@ -57,7 +55,7 @@ func (nmd *NetworkMapData) GetPeerNetworkMapComponents(peerID string, peersCusto
ForceRoutingPeerDNSResolution: forceRoutingPeerDNS, ForceRoutingPeerDNSResolution: forceRoutingPeerDNS,
} }
relevantPeers, relevantGroups, relevantPolicies, relevantRoutes, sshReqs := nmd.getPeersGroupsPoliciesRoutes(peerID, peer.SSHEnabled, &components.PostureFailedPeers) relevantPeers, relevantGroups, relevantPolicies, relevantRoutes, sshReqs := nmd.getPeersGroupsPoliciesRoutes(peerID, peer.SSHEnabled)
if len(sshReqs.neededGroupIDs) > 0 { if len(sshReqs.neededGroupIDs) > 0 {
components.GroupIDToUserIDs = filterGroupIDToUserIDs(nmd.GroupIDToUserIDs, sshReqs.neededGroupIDs) components.GroupIDToUserIDs = filterGroupIDToUserIDs(nmd.GroupIDToUserIDs, sshReqs.neededGroupIDs)
@@ -87,6 +85,8 @@ func (nmd *NetworkMapData) GetPeerNetworkMapComponents(peerID string, peersCusto
} }
} }
seenSourcePolicies := make(map[*nmdata.Policy]struct{})
var sourceFailures []map[string]map[string]struct{}
for _, resource := range nmd.NetworkResources { for _, resource := range nmd.NetworkResources {
if resource == nil || !resource.Enabled { if resource == nil || !resource.Enabled {
continue continue
@@ -111,16 +111,11 @@ func (nmd *NetworkMapData) GetPeerNetworkMapComponents(peerID string, peersCusto
continue continue
} }
if addSourcePeers { if addSourcePeers {
var peers []string if _, seen := seenSourcePolicies[policy]; !seen {
if policy.Rules[0].SourceResource.Type == string(types.ResourceTypePeer) && policy.Rules[0].SourceResource.ID != "" { seenSourcePolicies[policy] = struct{}{}
peers = []string{policy.Rules[0].SourceResource.ID} sources := nmd.resourcePolicySources(policy)
} else { nmd.mergeSourcePeers(components, sources)
peers = nmd.getUniquePeerIDsFromGroupsIDs(policy.SourceGroups()) sourceFailures = append(sourceFailures, sources.failed)
}
for _, pID := range nmd.getPostureValidPeersSaveFailed(peers, policy.SourcePostureChecks, &components.PostureFailedPeers) {
if _, exists := components.Peers[pID]; !exists {
components.Peers[pID] = nmd.Peers[pID]
}
} }
} else { } else {
peerInSources := false peerInSources := false
@@ -128,7 +123,7 @@ func (nmd *NetworkMapData) GetPeerNetworkMapComponents(peerID string, peersCusto
peerInSources = policy.Rules[0].SourceResource.ID == peerID peerInSources = policy.Rules[0].SourceResource.ID == peerID
} else { } else {
for _, groupID := range policy.SourceGroups() { for _, groupID := range policy.SourceGroups() {
if group := nmd.Groups[groupID]; group != nil && slices.Contains(group.Peers, peerID) { if _, ok := peerGroups[groupID]; ok {
peerInSources = true peerInSources = true
break break
} }
@@ -195,7 +190,7 @@ func (nmd *NetworkMapData) GetPeerNetworkMapComponents(peerID string, peersCusto
} }
filterGroupPeers(&components.Groups, components.Peers) filterGroupPeers(&components.Groups, components.Peers)
filterPostureFailedPeers(&components.PostureFailedPeers, components.Policies, components.ResourcePoliciesMap, components.Peers) nmd.collectPostureFailedPeers(components, sourceFailures)
return components return components
} }
@@ -203,7 +198,6 @@ func (nmd *NetworkMapData) GetPeerNetworkMapComponents(peerID string, peersCusto
func (nmd *NetworkMapData) getPeersGroupsPoliciesRoutes( func (nmd *NetworkMapData) getPeersGroupsPoliciesRoutes(
peerID string, peerID string,
peerSSHEnabled bool, peerSSHEnabled bool,
postureFailedPeers *map[string]map[string]struct{},
) (map[string]*nmdata.Peer, map[string]*nmdata.Group, []*nmdata.Policy, []*nmdata.Route, sshRequirements) { ) (map[string]*nmdata.Peer, map[string]*nmdata.Group, []*nmdata.Policy, []*nmdata.Route, sshRequirements) {
relevantPeerIDs := make(map[string]*nmdata.Peer, len(nmd.Peers)/4) relevantPeerIDs := make(map[string]*nmdata.Peer, len(nmd.Peers)/4)
relevantGroupIDs := make(map[string]*nmdata.Group, len(nmd.Groups)/4) relevantGroupIDs := make(map[string]*nmdata.Group, len(nmd.Groups)/4)
@@ -296,7 +290,7 @@ func (nmd *NetworkMapData) getPeersGroupsPoliciesRoutes(
} }
policyRelevant := false policyRelevant := false
for _, rule := range policy.Rules { for i, rule := range policy.Rules {
if rule == nil || !rule.Enabled { if rule == nil || !rule.Enabled {
continue continue
} }
@@ -320,24 +314,13 @@ func (nmd *NetworkMapData) getPeersGroupsPoliciesRoutes(
} }
} }
var sourcePeers, destinationPeers []string sources, destinations := nmd.ruleAdmissions(policy, i)
var peerInSources, peerInDestinations bool peerInSources := nmd.sideAdmitsPeer(rule.Sources, rule.SourceResource, policy.SourcePostureChecks, peerID, peerGroupSet)
peerInDestinations := nmd.sideAdmitsPeer(rule.Destinations, rule.DestinationResource, nil, peerID, peerGroupSet)
if rule.SourceResource.Type == string(types.ResourceTypePeer) && rule.SourceResource.ID != "" {
sourcePeers, peerInSources = nmd.getPeerFromResource(rule.SourceResource, peerID, policy.SourcePostureChecks, postureFailedPeers)
} else {
sourcePeers, peerInSources = nmd.getPeersFromGroups(rule.Sources, peerID, policy.SourcePostureChecks, postureFailedPeers)
}
if rule.DestinationResource.Type == string(types.ResourceTypePeer) && rule.DestinationResource.ID != "" {
destinationPeers, peerInDestinations = nmd.getPeerFromResource(rule.DestinationResource, peerID, nil, postureFailedPeers)
} else {
destinationPeers, peerInDestinations = nmd.getPeersFromGroups(rule.Destinations, peerID, nil, postureFailedPeers)
}
if peerInSources { if peerInSources {
policyRelevant = true policyRelevant = true
for _, pid := range destinationPeers { for _, pid := range destinations.peers {
relevantPeerIDs[pid] = nmd.Peers[pid] relevantPeerIDs[pid] = nmd.Peers[pid]
} }
for _, dstGroupID := range rule.Destinations { for _, dstGroupID := range rule.Destinations {
@@ -349,7 +332,7 @@ func (nmd *NetworkMapData) getPeersGroupsPoliciesRoutes(
if peerInDestinations { if peerInDestinations {
policyRelevant = true policyRelevant = true
for _, pid := range sourcePeers { for _, pid := range sources.peers {
relevantPeerIDs[pid] = nmd.Peers[pid] relevantPeerIDs[pid] = nmd.Peers[pid]
} }
for _, srcGroupID := range rule.Sources { for _, srcGroupID := range rule.Sources {
@@ -381,9 +364,11 @@ func (nmd *NetworkMapData) getPeersGroupsPoliciesRoutes(
return relevantPeerIDs, relevantGroupIDs, relevantPolicies, relevantRoutes, sshReqs return relevantPeerIDs, relevantGroupIDs, relevantPolicies, relevantRoutes, sshReqs
} }
func (nmd *NetworkMapData) getPeersFromGroups(groups []string, peerID string, sourcePostureChecksIDs []string, // getPeersFromGroups returns the admitted members of a rule side given by groups.
postureFailedPeers *map[string]map[string]struct{}) ([]string, bool) { // The All group, or a single group, alone decides the side. Posture failures are
peerInGroups := false // recorded in postureFailedPeers.
func (nmd *NetworkMapData) getPeersFromGroups(groups []string, sourcePostureChecksIDs []string,
postureFailedPeers *map[string]map[string]struct{}) []string {
filteredPeerIDs := make([]string, 0, len(groups)) filteredPeerIDs := make([]string, 0, len(groups))
seenPeerIds := make(map[string]struct{}, len(groups)) seenPeerIds := make(map[string]struct{}, len(groups))
@@ -395,20 +380,12 @@ func (nmd *NetworkMapData) getPeersFromGroups(groups []string, peerID string, so
if group.IsGroupAll() || len(groups) == 1 { if group.IsGroupAll() || len(groups) == 1 {
filteredPeerIDs = make([]string, 0, len(group.Peers)) filteredPeerIDs = make([]string, 0, len(group.Peers))
peerInGroups = false
for _, pid := range group.Peers { for _, pid := range group.Peers {
if !nmd.admitPolicyPeer(pid, sourcePostureChecksIDs, postureFailedPeers) { if nmd.admitPolicyPeer(pid, sourcePostureChecksIDs, postureFailedPeers) {
continue filteredPeerIDs = append(filteredPeerIDs, pid)
} }
if pid == peerID {
peerInGroups = true
continue
}
filteredPeerIDs = append(filteredPeerIDs, pid)
} }
return filteredPeerIDs, peerInGroups return filteredPeerIDs
} }
for _, pid := range group.Peers { for _, pid := range group.Peers {
@@ -416,57 +393,55 @@ func (nmd *NetworkMapData) getPeersFromGroups(groups []string, peerID string, so
continue continue
} }
seenPeerIds[pid] = struct{}{} seenPeerIds[pid] = struct{}{}
if !nmd.admitPolicyPeer(pid, sourcePostureChecksIDs, postureFailedPeers) { if nmd.admitPolicyPeer(pid, sourcePostureChecksIDs, postureFailedPeers) {
continue filteredPeerIDs = append(filteredPeerIDs, pid)
} }
if pid == peerID {
peerInGroups = true
continue
}
filteredPeerIDs = append(filteredPeerIDs, pid)
} }
} }
return filteredPeerIDs, peerInGroups return filteredPeerIDs
} }
// getPeerFromResource resolves a rule side that names a peer directly, admitting it // getPeerFromResource resolves a rule side that names a peer directly, admitting it
// like a member of a group holding only that peer. // like a member of a group holding only that peer.
func (nmd *NetworkMapData) getPeerFromResource(resource nmdata.Resource, peerID string, sourcePostureChecksIDs []string, func (nmd *NetworkMapData) getPeerFromResource(resource nmdata.Resource, sourcePostureChecksIDs []string,
postureFailedPeers *map[string]map[string]struct{}) ([]string, bool) { postureFailedPeers *map[string]map[string]struct{}) []string {
if !nmd.admitPolicyPeer(resource.ID, sourcePostureChecksIDs, postureFailedPeers) { if !nmd.admitPolicyPeer(resource.ID, sourcePostureChecksIDs, postureFailedPeers) {
return nil, false return nil
} }
if resource.ID == peerID { return []string{resource.ID}
return nil, true
}
return []string{resource.ID}, false
} }
// admitPolicyPeer applies the per-peer admission of a rule side: the peer must exist, // admitPolicyPeer is admitsPeer that records a failed posture check in postureFailedPeers.
// be validated and pass the rule's posture checks. A failed check is recorded in
// postureFailedPeers.
func (nmd *NetworkMapData) admitPolicyPeer(pid string, sourcePostureChecksIDs []string, postureFailedPeers *map[string]map[string]struct{}) bool { func (nmd *NetworkMapData) admitPolicyPeer(pid string, sourcePostureChecksIDs []string, postureFailedPeers *map[string]map[string]struct{}) bool {
admitted, failedCheck := nmd.admitsPeer(pid, sourcePostureChecksIDs)
if failedCheck != "" {
if _, ok := (*postureFailedPeers)[failedCheck]; !ok {
(*postureFailedPeers)[failedCheck] = make(map[string]struct{})
}
(*postureFailedPeers)[failedCheck][pid] = struct{}{}
}
return admitted
}
// admitsPeer applies the per-peer admission of a rule side: the peer must exist, be
// validated and pass the rule's posture checks. It returns the first failed posture
// check ID, if any.
func (nmd *NetworkMapData) admitsPeer(pid string, sourcePostureChecksIDs []string) (bool, string) {
peer, ok := nmd.Peers[pid] peer, ok := nmd.Peers[pid]
if !ok || peer == nil { if !ok || peer == nil {
return false return false, ""
} }
if _, ok := nmd.ValidatedPeers[pid]; !ok { if _, ok := nmd.ValidatedPeers[pid]; !ok {
return false return false, ""
} }
isValid, pname := nmd.validatePostureChecksOnPeerGetFailed(sourcePostureChecksIDs, pid) isValid, pname := nmd.validatePostureChecksOnPeerGetFailed(sourcePostureChecksIDs, pid)
if !isValid && len(pname) > 0 { if !isValid && len(pname) > 0 {
if _, ok := (*postureFailedPeers)[pname]; !ok { return false, pname
(*postureFailedPeers)[pname] = make(map[string]struct{})
}
(*postureFailedPeers)[pname][pid] = struct{}{}
return false
} }
return true return true, ""
} }
func (nmd *NetworkMapData) validatePostureChecksOnPeerGetFailed(sourcePostureChecksID []string, peerID string) (bool, string) { func (nmd *NetworkMapData) validatePostureChecksOnPeerGetFailed(sourcePostureChecksID []string, peerID string) (bool, string) {
@@ -476,7 +451,7 @@ func (nmd *NetworkMapData) validatePostureChecksOnPeerGetFailed(sourcePostureChe
} }
for _, postureChecksID := range sourcePostureChecksID { for _, postureChecksID := range sourcePostureChecksID {
if valid, cached := nmd.cachedPostureCheckResult(postureChecksID, peerID); cached { if valid, cached := nmd.postureVerdict(postureChecksID, peerID); cached {
if !valid { if !valid {
return false, postureChecksID return false, postureChecksID
} }
@@ -494,43 +469,25 @@ func (nmd *NetworkMapData) validatePostureChecksOnPeerGetFailed(sourcePostureChe
return true, "" return true, ""
} }
// PrecomputePostureValidation evaluates every posture check used by an enabled policy
// for the policy's source peers and stores the results in PostureValidation.
func (nmd *NetworkMapData) PrecomputePostureValidation() { func (nmd *NetworkMapData) PrecomputePostureValidation() {
if len(nmd.PostureChecks) == 0 { if len(nmd.PostureChecks) == 0 {
nmd.PostureValidation = nil nmd.PostureValidation = nil
return return
} }
checkPeerIDs := make(map[string]map[string]struct{}) results := make(map[string]map[string]bool)
for _, policy := range nmd.Policies { for _, policy := range nmd.Policies {
if policy == nil || !policy.Enabled || len(policy.SourcePostureChecks) == 0 { if policy == nil || !policy.Enabled {
continue continue
} }
groupPeerIDs := nmd.getUniquePeerIDsFromGroupsIDs(policy.SourceGroups())
for _, postureChecksID := range policy.SourcePostureChecks { for _, postureChecksID := range policy.SourcePostureChecks {
set := checkPeerIDs[postureChecksID] if _, done := results[postureChecksID]; !done {
if set == nil { results[postureChecksID] = nmd.evaluatePostureChecksForPeers(postureChecksID, nmd.postureCheckPeerIDs(postureChecksID))
set = make(map[string]struct{}, len(groupPeerIDs))
checkPeerIDs[postureChecksID] = set
}
for _, pid := range groupPeerIDs {
set[pid] = struct{}{}
}
for _, rule := range policy.Rules {
if rule == nil {
continue
}
if rule.SourceResource.Type == string(types.ResourceTypePeer) && rule.SourceResource.ID != "" {
set[rule.SourceResource.ID] = struct{}{}
}
} }
} }
} }
results := make(map[string]map[string]bool, len(checkPeerIDs))
for postureChecksID, peerIDs := range checkPeerIDs {
results[postureChecksID] = nmd.evaluatePostureChecksForPeers(postureChecksID, peerIDs)
}
nmd.PostureValidation = results nmd.PostureValidation = results
} }
@@ -552,38 +509,77 @@ func (nmd *NetworkMapData) evaluatePostureChecksForPeers(postureChecksID string,
return results return results
} }
func (nmd *NetworkMapData) cachedPostureCheckResult(postureChecksID, peerID string) (bool, bool) { // mergeSourcePeers adds the admitted source peers of a resource policy to the components.
results, ok := nmd.PostureValidation[postureChecksID] func (nmd *NetworkMapData) mergeSourcePeers(components *types.NetworkMapComponents, sources *admittedPeers) {
if !ok { for _, pID := range sources.peers {
return false, false if _, exists := components.Peers[pID]; !exists {
components.Peers[pID] = nmd.Peers[pID]
}
} }
if results == nil {
return true, true
}
valid, found := results[peerID]
return valid, found
} }
func (nmd *NetworkMapData) getPostureValidPeersSaveFailed(inputPeers []string, postureChecksIDs []string, postureFailedPeers *map[string]map[string]struct{}) []string { // collectPostureFailedPeers sets the components' posture failures to those of every
var dest []string // enabled rule side, of the given resource-policy sources and the ones already
for _, peerID := range inputPeers { // recorded, limited to the checks the components' policies reference and to the peers
if _, validated := nmd.ValidatedPeers[peerID]; !validated { // in the components. Each failure set is intersected with the components' peers from
continue // its smaller side, so the cost follows the peer's own map, not the account size.
func (nmd *NetworkMapData) collectPostureFailedPeers(components *types.NetworkMapComponents, sourceFailures []map[string]map[string]struct{}) {
failures := append([]map[string]map[string]struct{}{nmd.ruleFailures(), components.PostureFailedPeers}, sourceFailures...)
referenced := make(map[string]struct{})
addReferenced := func(policy *nmdata.Policy) {
if policy == nil {
return
} }
valid, pname := nmd.validatePostureChecksOnPeerGetFailed(postureChecksIDs, peerID) for _, checkID := range policy.SourcePostureChecks {
if valid { referenced[checkID] = struct{}{}
dest = append(dest, peerID)
continue
} }
if pname == "" {
continue
}
if _, ok := (*postureFailedPeers)[pname]; !ok {
(*postureFailedPeers)[pname] = make(map[string]struct{})
}
(*postureFailedPeers)[pname][peerID] = struct{}{}
} }
return dest for _, policy := range components.Policies {
addReferenced(policy)
}
for _, policies := range components.ResourcePoliciesMap {
for _, policy := range policies {
addReferenced(policy)
}
}
result := make(map[string]map[string]struct{})
for checkID := range referenced {
var kept map[string]struct{}
for _, failed := range failures {
kept = addFailedPeersInMap(kept, failed[checkID], components.Peers)
}
if len(kept) > 0 {
result[checkID] = kept
}
}
components.PostureFailedPeers = result
}
// addFailedPeersInMap adds the failed peers that are in peers to kept, allocating kept
// on the first match, and returns it.
func addFailedPeersInMap(kept, failed map[string]struct{}, peers map[string]*nmdata.Peer) map[string]struct{} {
add := func(pid string) {
if kept == nil {
kept = make(map[string]struct{})
}
kept[pid] = struct{}{}
}
if len(failed) <= len(peers) {
for pid := range failed {
if _, ok := peers[pid]; ok {
add(pid)
}
}
return kept
}
for pid := range peers {
if _, ok := failed[pid]; ok {
add(pid)
}
}
return kept
} }
// forcesRoutingPeerDNSResolution reports whether the given peer must run // forcesRoutingPeerDNSResolution reports whether the given peer must run
@@ -739,41 +735,6 @@ func filterGroupPeers(groups *map[string]*nmdata.Group, peers map[string]*nmdata
} }
} }
func filterPostureFailedPeers(postureFailedPeers *map[string]map[string]struct{}, policies []*nmdata.Policy, resourcePoliciesMap map[string][]*nmdata.Policy, peers map[string]*nmdata.Peer) {
if len(*postureFailedPeers) == 0 {
return
}
referencedPostureChecks := make(map[string]struct{})
for _, policy := range policies {
for _, checkID := range policy.SourcePostureChecks {
referencedPostureChecks[checkID] = struct{}{}
}
}
for _, resPolicies := range resourcePoliciesMap {
for _, policy := range resPolicies {
for _, checkID := range policy.SourcePostureChecks {
referencedPostureChecks[checkID] = struct{}{}
}
}
}
for checkID, failedPeers := range *postureFailedPeers {
if _, referenced := referencedPostureChecks[checkID]; !referenced {
delete(*postureFailedPeers, checkID)
continue
}
for peerID := range failedPeers {
if _, exists := peers[peerID]; !exists {
delete(failedPeers, peerID)
}
}
if len(failedPeers) == 0 {
delete(*postureFailedPeers, checkID)
}
}
}
func filterDNSRecordsByPeers(records []nmdata.SimpleRecord, peers map[string]*nmdata.Peer, includeIPv6 bool) []nmdata.SimpleRecord { func filterDNSRecordsByPeers(records []nmdata.SimpleRecord, peers map[string]*nmdata.Peer, includeIPv6 bool) []nmdata.SimpleRecord {
if len(records) == 0 || len(peers) == 0 { if len(records) == 0 || len(peers) == 0 {
return nil return nil
@@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"net/netip" "net/netip"
"sync"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -1662,3 +1663,243 @@ func TestNetworkMapData_GetPeerGroups(t *testing.T) {
assert.Equal(t, map[string]struct{}{"g-1": {}, "g-2": {}}, nmd.GetPeerGroups(targetID)) assert.Equal(t, map[string]struct{}{"g-1": {}, "g-2": {}}, nmd.GetPeerGroups(targetID))
assert.Empty(t, nmd.GetPeerGroups("missing")) assert.Empty(t, nmd.GetPeerGroups("missing"))
} }
func TestGetPeerNetworkMapComponents_NetworkResources_SharedPolicyAcrossResourcesAndRouters(t *testing.T) {
target := newPeer(targetID, 1)
router2 := newPeer("peer-router-2", 2)
clientOK := newPeer("peer-client-ok", 3)
clientFail := newPeer("peer-client-fail", 4)
clientFail.Meta.WtVersion = failingVersion
nmd := newNMD(target, router2, clientOK, clientFail)
addVersionCheck(nmd, "pc-1", postureMinVersion)
addGroup(nmd, "g-clients-a", clientOK.ID)
addGroup(nmd, "g-clients-b", clientFail.ID)
addGroup(nmd, "g-routers", targetID, router2.ID)
rp := newPolicy("rp-1", newRule([]string{"g-clients-a", "g-clients-b"}, []string{"g-resource"}))
rp.SourcePostureChecks = []string{"pc-1"}
nmd.Policies = []*nmdata.Policy{newPolicy("p-acl", newRule([]string{"g-clients-b"}, []string{"g-routers"}))}
nmd.ResourcePolicies = map[string][]*nmdata.Policy{}
for i := range 3 {
res := &nmdata.NetworkResource{ID: fmt.Sprintf("res-%d", i), NetworkID: "net-1", Enabled: true}
nmd.NetworkResources = append(nmd.NetworkResources, res)
nmd.ResourcePolicies[res.ID] = []*nmdata.Policy{rp}
}
nmd.Routers = map[string]map[string]*nmdata.NetworkRouter{"net-1": {targetID: {}, router2.ID: {}}}
nmd.PrecomputePostureValidation()
first := compute(nmd, targetID)
first.PostureFailedPeers["pc-1"]["mutated"] = struct{}{}
second := compute(nmd, router2.ID)
for _, c := range []*nbtypes.NetworkMapComponents{first, second} {
assert.Contains(t, peerIDSet(c.Peers), clientOK.ID)
assert.Len(t, c.ResourcePoliciesMap, 3)
}
assert.Equal(t, map[string]map[string]struct{}{"pc-1": {clientFail.ID: {}}}, second.PostureFailedPeers,
"a component's failure set must not alias the shared per-policy result")
}
func BenchmarkGetPeerNetworkMapComponents_RouterManyResources(b *testing.B) {
const (
peerCount = 2000
groupCount = 4
resourceCount = 200
routerCount = 10
)
peers := make([]*nmdata.Peer, 0, peerCount)
for i := range peerCount {
p := newPeer(fmt.Sprintf("peer-%d", i), byte(i))
if i%3 == 0 {
p.Meta.WtVersion = failingVersion
}
peers = append(peers, p)
}
nmd := newNMD(peers...)
addVersionCheck(nmd, "pc-1", postureMinVersion)
sourceGroups := make([]string, 0, groupCount)
for g := range groupCount {
id := fmt.Sprintf("g-src-%d", g)
var members []string
for i := g; i < peerCount; i += groupCount {
members = append(members, peers[i].ID)
}
addGroup(nmd, id, members...)
sourceGroups = append(sourceGroups, id)
}
rp := newPolicy("rp-1", newRule(sourceGroups, []string{"g-resource"}))
rp.SourcePostureChecks = []string{"pc-1"}
nmd.Policies = []*nmdata.Policy{rp}
nmd.ResourcePolicies = map[string][]*nmdata.Policy{}
for i := range resourceCount {
res := &nmdata.NetworkResource{ID: fmt.Sprintf("res-%d", i), NetworkID: "net-1", Enabled: true}
nmd.NetworkResources = append(nmd.NetworkResources, res)
nmd.ResourcePolicies[res.ID] = []*nmdata.Policy{rp}
}
routers := make(map[string]*nmdata.NetworkRouter, routerCount)
for i := range routerCount {
routers[peers[i].ID] = &nmdata.NetworkRouter{}
}
nmd.Routers = map[string]map[string]*nmdata.NetworkRouter{"net-1": routers}
nmd.PrecomputePostureValidation()
b.ReportAllocs()
for b.Loop() {
for routerID := range routers {
compute(nmd, routerID)
}
}
}
// newPolicyFixture builds an account with ACL policies over overlapping groups, the
// All group, peer resources and posture checks, plus a routed resource policy.
func newPolicyFixture(peerCount, policyCount int) *networkmap.NetworkMapData {
peers := make([]*nmdata.Peer, 0, peerCount)
allIDs := make([]string, 0, peerCount)
for i := range peerCount {
p := newPeer(fmt.Sprintf("peer-%d", i), byte(i))
if i%5 == 0 {
p.Meta.WtVersion = failingVersion
}
peers = append(peers, p)
allIDs = append(allIDs, p.ID)
}
nmd := newNMD(peers...)
delete(nmd.ValidatedPeers, peers[peerCount-1].ID)
addVersionCheck(nmd, "pc-1", postureMinVersion)
addVersionCheck(nmd, "pc-2", "0.0.1")
addGroup(nmd, "g-all", allIDs...).Name = nmdata.GroupAllName
for g := range 8 {
var members []string
for i := g; i < peerCount; i += 3 + g {
members = append(members, peers[i].ID)
}
addGroup(nmd, fmt.Sprintf("g-%d", g), members...)
}
for i := range policyCount {
rule := newRule([]string{fmt.Sprintf("g-%d", i%8), fmt.Sprintf("g-%d", (i+3)%8)}, []string{fmt.Sprintf("g-%d", (i+1)%8)})
switch i % 4 {
case 1:
rule.Sources = []string{fmt.Sprintf("g-%d", i%8), "g-all"}
case 2:
rule.SourceResource = nmdata.Resource{ID: peers[i%peerCount].ID, Type: string(nbtypes.ResourceTypePeer)}
}
policy := newPolicy(fmt.Sprintf("p-%d", i), rule)
if i%2 == 0 {
policy.SourcePostureChecks = []string{"pc-2", "pc-1"}
}
nmd.Policies = append(nmd.Policies, policy)
}
rp := newPolicy("rp-1", newRule([]string{"g-1", "g-2"}, []string{"g-resource"}))
rp.SourcePostureChecks = []string{"pc-1"}
nmd.Policies = append(nmd.Policies, rp)
nmd.NetworkResources = []*nmdata.NetworkResource{{ID: "res-1", NetworkID: "net-1", Enabled: true}}
nmd.ResourcePolicies = map[string][]*nmdata.Policy{"res-1": {rp}}
nmd.Routers = map[string]map[string]*nmdata.NetworkRouter{"net-1": {peers[0].ID: {}, peers[1].ID: {}}}
return nmd
}
func TestGetPeerNetworkMapComponents_SharedAdmissionsMatchFreshData(t *testing.T) {
const peerCount = 60
expected := make(map[string]*nbtypes.NetworkMapComponents, peerCount)
for i := range peerCount {
peerID := fmt.Sprintf("peer-%d", i)
expected[peerID] = compute(newPolicyFixture(peerCount, 24), peerID)
}
shared := newPolicyFixture(peerCount, 24)
got := make([]*nbtypes.NetworkMapComponents, peerCount)
var wg sync.WaitGroup
for i := peerCount - 1; i >= 0; i-- {
wg.Go(func() {
got[i] = compute(shared, fmt.Sprintf("peer-%d", i))
})
}
wg.Wait()
for i, c := range got {
peerID := fmt.Sprintf("peer-%d", i)
want := expected[peerID]
assert.ElementsMatch(t, peerIDSet(want.Peers), peerIDSet(c.Peers), peerID)
assert.ElementsMatch(t, groupIDSet(want.Groups), groupIDSet(c.Groups), peerID)
assert.ElementsMatch(t, policyIDs(want.Policies), policyIDs(c.Policies), peerID)
assert.Equal(t, want.PostureFailedPeers, c.PostureFailedPeers, peerID)
}
}
func BenchmarkGetPeerNetworkMapComponents_AllPeersPosturePolicies(b *testing.B) {
const peerCount = 2000
b.ReportAllocs()
for b.Loop() {
b.StopTimer()
nmd := newPolicyFixture(peerCount, 50)
b.StartTimer()
for i := range peerCount {
compute(nmd, fmt.Sprintf("peer-%d", i))
}
}
}
func TestGetPeerNetworkMapComponents_SeededPostureValidation(t *testing.T) {
target := newPeer(targetID, 1)
src := newPeer("peer-src", 2)
src.Meta.WtVersion = failingVersion
nmd := newNMD(target, src)
addVersionCheck(nmd, "pc-1", postureMinVersion)
addGroup(nmd, "g-src", src.ID)
addGroup(nmd, "g-dst", targetID)
policy := newPolicy("p-1", newRule([]string{"g-src"}, []string{"g-dst"}))
policy.SourcePostureChecks = []string{"pc-1"}
nmd.Policies = []*nmdata.Policy{policy}
nmd.PostureValidation = map[string]map[string]bool{"pc-1": {src.ID: true}}
c := compute(nmd, targetID)
assert.Contains(t, peerIDSet(c.Peers), src.ID, "the seeded verdict wins over evaluating the check")
assert.Empty(t, c.PostureFailedPeers)
}
// newClientsBehindRouterFixture builds clients that reach a routed network resource
// and the routing peer through policies with posture checks a fifth of them fail.
func newClientsBehindRouterFixture(clientCount int) (*networkmap.NetworkMapData, []string) {
router := newPeer("peer-router", 1)
peers := []*nmdata.Peer{router}
clientIDs := make([]string, 0, clientCount)
for i := range clientCount {
p := newPeer(fmt.Sprintf("peer-client-%d", i), byte(i))
if i%5 == 0 {
p.Meta.WtVersion = failingVersion
}
peers = append(peers, p)
clientIDs = append(clientIDs, p.ID)
}
nmd := newNMD(peers...)
addVersionCheck(nmd, "pc-1", postureMinVersion)
addGroup(nmd, "g-clients", clientIDs...)
addGroup(nmd, "g-routers", router.ID)
addGroup(nmd, "g-resource")
acl := newPolicy("p-acl", newRule([]string{"g-clients"}, []string{"g-routers"}))
acl.SourcePostureChecks = []string{"pc-1"}
rp := newPolicy("rp-1", newRule([]string{"g-clients"}, []string{"g-resource"}))
rp.SourcePostureChecks = []string{"pc-1"}
nmd.Policies = []*nmdata.Policy{acl, rp}
nmd.ResourcePolicies = map[string][]*nmdata.Policy{}
for i := range 5 {
res := &nmdata.NetworkResource{ID: fmt.Sprintf("res-%d", i), NetworkID: "net-1", Enabled: true}
nmd.NetworkResources = append(nmd.NetworkResources, res)
nmd.ResourcePolicies[res.ID] = []*nmdata.Policy{rp}
}
nmd.Routers = map[string]map[string]*nmdata.NetworkRouter{"net-1": {router.ID: {}}}
return nmd, append(clientIDs, router.ID)
}
func BenchmarkGetPeerNetworkMapComponents_ClientsBehindOneRouter(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
b.StopTimer()
nmd, peerIDs := newClientsBehindRouterFixture(10000)
b.StartTimer()
for _, peerID := range peerIDs {
compute(nmd, peerID)
}
}
}
@@ -25,10 +25,11 @@ type NetworkMapData struct { //nolint:revive // established name across the code
PostureChecks map[string]*nmdata.PostureChecks PostureChecks map[string]*nmdata.PostureChecks
// PostureValidation holds the precomputed posture-check results, keyed by // PostureValidation optionally seeds the posture-check results, keyed by
// posture check ID then peer ID. Filled by PrecomputePostureValidation; a // posture check ID then peer ID. May be filled by PrecomputePostureValidation; a
// present but nil inner map marks a check ID that resolves to no posture // present but nil inner map marks a check ID that resolves to no posture
// check, which the calc treats as passing. // check, which the calc treats as passing. Checks it lacks are evaluated
// on first use.
PostureValidation map[string]map[string]bool PostureValidation map[string]map[string]bool
AllowedUserIDs map[string]struct{} AllowedUserIDs map[string]struct{}
@@ -62,6 +63,9 @@ type NetworkMapData struct { //nolint:revive // established name across the code
peerGroupsIdx map[string]map[string]struct{} peerGroupsIdx map[string]map[string]struct{}
proxyPoliciesOnce sync.Once proxyPoliciesOnce sync.Once
admissionsOnce sync.Once
admissionsIdx *admissionCache
} }
// AppliedZoneCandidate is an account-level custom DNS zone reduced to the // AppliedZoneCandidate is an account-level custom DNS zone reduced to the
@@ -1,6 +1,8 @@
package nmdata package nmdata
import ( import (
"crypto/x509"
"sync"
"time" "time"
"github.com/netbirdio/netbird/shared/management/certposture" "github.com/netbirdio/netbird/shared/management/certposture"
@@ -9,8 +11,21 @@ import (
// CertificateCheck is the slim twin of posture.CertificateCheck. // CertificateCheck is the slim twin of posture.CertificateCheck.
type CertificateCheck struct { type CertificateCheck struct {
CACertificates []string CACertificates []string
rootsOnce sync.Once
roots *x509.CertPool
rootsErr error
} }
func (c *CertificateCheck) check(peer *Peer) (bool, error) { func (c *CertificateCheck) check(peer *Peer) (bool, error) {
return certposture.AnyChainMatchesCAs(peer.Meta.Certificates, c.CACertificates, time.Now()), nil if len(peer.Meta.Certificates) == 0 {
return false, nil
}
c.rootsOnce.Do(func() {
c.roots, c.rootsErr = certposture.ParseCAs(c.CACertificates)
})
if c.rootsErr != nil {
return false, c.rootsErr
}
return certposture.AnyChainMatchesPool(peer.Meta.Certificates, c.roots, time.Now()), nil
} }
@@ -26,3 +26,27 @@ func TestCertificateCheck_Check(t *testing.T) {
otherOnly := bundle(ChecksDefinition{CertificateCheck: &CertificateCheck{CACertificates: []string{otherCA.PEM}}}) otherOnly := bundle(ChecksDefinition{CertificateCheck: &CertificateCheck{CACertificates: []string{otherCA.PEM}}})
assert.False(t, otherOnly[0].Passes(with)) assert.False(t, otherOnly[0].Passes(with))
} }
func TestCertificateCheck_ReusesParsedCAs(t *testing.T) {
ca := certtest.NewCA(t, "corp-root")
chain := certposture.EncodeChainPEM([]*x509.Certificate{ca.Issue(t, certtest.ECDSAKey(t), "device")})
with := &Peer{Meta: PeerSystemMeta{Certificates: []string{chain}}}
check := &CertificateCheck{CACertificates: []string{ca.PEM}}
for range 3 {
valid, err := check.check(with)
assert.NoError(t, err)
assert.True(t, valid)
}
invalid := &CertificateCheck{CACertificates: []string{"not a pem"}}
for range 2 {
valid, err := invalid.check(with)
assert.ErrorIs(t, err, certposture.ErrNoCertificateInPEM)
assert.False(t, valid)
}
valid, err := invalid.check(&Peer{})
assert.NoError(t, err)
assert.False(t, valid, "a peer without certificates fails before the CAs are parsed")
}
@@ -2,6 +2,7 @@ package nmdata
import ( import (
"strings" "strings"
"sync"
"github.com/hashicorp/go-version" "github.com/hashicorp/go-version"
) )
@@ -9,27 +10,26 @@ import (
// NBVersionCheck is the slim twin of posture.NBVersionCheck. // NBVersionCheck is the slim twin of posture.NBVersionCheck.
type NBVersionCheck struct { type NBVersionCheck struct {
MinVersion string MinVersion string
constraintOnce sync.Once
constraint version.Constraints
constraintErr error
} }
func (n *NBVersionCheck) check(peer *Peer) (bool, error) { func (n *NBVersionCheck) check(peer *Peer) (bool, error) {
return meetsMinVersion(n.MinVersion, peer.Meta.WtVersion) peerNBVer, err := version.NewVersion(sanitizeVersion(peer.Meta.WtVersion))
}
func meetsMinVersion(minVer, peerVer string) (bool, error) {
peerVer = sanitizeVersion(peerVer)
minVer = sanitizeVersion(minVer)
peerNBVer, err := version.NewVersion(peerVer)
if err != nil { if err != nil {
return false, err return false, err
} }
constraints, err := version.NewConstraint(">= " + minVer) n.constraintOnce.Do(func() {
if err != nil { n.constraint, n.constraintErr = version.NewConstraint(">= " + sanitizeVersion(n.MinVersion))
return false, err })
if n.constraintErr != nil {
return false, n.constraintErr
} }
return constraints.Check(peerNBVer), nil return n.constraint.Check(peerNBVer), nil
} }
func sanitizeVersion(v string) string { func sanitizeVersion(v string) string {