mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-27 06:49:07 +01:00
[management] optimize posture cache (#8366)
This commit is contained in:
@@ -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
|
||||
// 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
|
||||
// policies carry no posture checks, so precomputing after the injection yields
|
||||
// the same validation as precomputing before it.
|
||||
// it is handed out: the twin the callers see is read-only. Posture checks are
|
||||
// evaluated lazily by the components calculation.
|
||||
func (c *Controller) fetchNetworkMapData(ctx context.Context, accountID string) (*networkmap.NetworkMapData, error) {
|
||||
nmData, err := c.nmdataStore.GetNetworkMapData(ctx, accountID)
|
||||
if err != nil {
|
||||
@@ -501,7 +500,6 @@ func (c *Controller) fetchNetworkMapData(ctx context.Context, accountID string)
|
||||
nmData.Services = c.proxyServicesFromRepo(ctx, accountID)
|
||||
nmData.BuildPrivateServiceCandidates()
|
||||
nmData.InjectProxyPolicies()
|
||||
nmData.PrecomputePostureValidation()
|
||||
|
||||
return nmData, nil
|
||||
}
|
||||
|
||||
@@ -184,7 +184,6 @@ func RunCase(t *testing.T, c Case) {
|
||||
nmData := c.Data
|
||||
applyFixtureDefaults(nmData)
|
||||
nmData.BuildPrivateServiceCandidates()
|
||||
nmData.PrecomputePostureValidation()
|
||||
|
||||
dnsDomain := c.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))
|
||||
}
|
||||
|
||||
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) {
|
||||
ca := certtest.NewCA(t, "root")
|
||||
leaf := ca.Issue(t, certtest.ECDSAKey(t), "device")
|
||||
|
||||
@@ -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
|
||||
// is anchored in one of the configured CAs.
|
||||
func AnyChainMatchesCAs(chainPEMs, caPEMs []string, now time.Time) bool {
|
||||
if len(chainPEMs) == 0 {
|
||||
return false
|
||||
}
|
||||
roots, err := ParseCAs(caPEMs)
|
||||
if err != nil {
|
||||
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 {
|
||||
if chainMatchesPool(chainPEM, roots, now) {
|
||||
return true
|
||||
|
||||
@@ -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{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
package networkmap
|
||||
|
||||
import (
|
||||
"slices"
|
||||
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||
"github.com/netbirdio/netbird/shared/management/types"
|
||||
)
|
||||
@@ -57,7 +55,7 @@ func (nmd *NetworkMapData) GetPeerNetworkMapComponents(peerID string, peersCusto
|
||||
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 {
|
||||
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 {
|
||||
if resource == nil || !resource.Enabled {
|
||||
continue
|
||||
@@ -111,16 +111,11 @@ func (nmd *NetworkMapData) GetPeerNetworkMapComponents(peerID string, peersCusto
|
||||
continue
|
||||
}
|
||||
if addSourcePeers {
|
||||
var peers []string
|
||||
if policy.Rules[0].SourceResource.Type == string(types.ResourceTypePeer) && policy.Rules[0].SourceResource.ID != "" {
|
||||
peers = []string{policy.Rules[0].SourceResource.ID}
|
||||
} else {
|
||||
peers = nmd.getUniquePeerIDsFromGroupsIDs(policy.SourceGroups())
|
||||
}
|
||||
for _, pID := range nmd.getPostureValidPeersSaveFailed(peers, policy.SourcePostureChecks, &components.PostureFailedPeers) {
|
||||
if _, exists := components.Peers[pID]; !exists {
|
||||
components.Peers[pID] = nmd.Peers[pID]
|
||||
}
|
||||
if _, seen := seenSourcePolicies[policy]; !seen {
|
||||
seenSourcePolicies[policy] = struct{}{}
|
||||
sources := nmd.resourcePolicySources(policy)
|
||||
nmd.mergeSourcePeers(components, sources)
|
||||
sourceFailures = append(sourceFailures, sources.failed)
|
||||
}
|
||||
} else {
|
||||
peerInSources := false
|
||||
@@ -128,7 +123,7 @@ func (nmd *NetworkMapData) GetPeerNetworkMapComponents(peerID string, peersCusto
|
||||
peerInSources = policy.Rules[0].SourceResource.ID == peerID
|
||||
} else {
|
||||
for _, groupID := range policy.SourceGroups() {
|
||||
if group := nmd.Groups[groupID]; group != nil && slices.Contains(group.Peers, peerID) {
|
||||
if _, ok := peerGroups[groupID]; ok {
|
||||
peerInSources = true
|
||||
break
|
||||
}
|
||||
@@ -195,7 +190,7 @@ func (nmd *NetworkMapData) GetPeerNetworkMapComponents(peerID string, peersCusto
|
||||
}
|
||||
|
||||
filterGroupPeers(&components.Groups, components.Peers)
|
||||
filterPostureFailedPeers(&components.PostureFailedPeers, components.Policies, components.ResourcePoliciesMap, components.Peers)
|
||||
nmd.collectPostureFailedPeers(components, sourceFailures)
|
||||
|
||||
return components
|
||||
}
|
||||
@@ -203,7 +198,6 @@ func (nmd *NetworkMapData) GetPeerNetworkMapComponents(peerID string, peersCusto
|
||||
func (nmd *NetworkMapData) getPeersGroupsPoliciesRoutes(
|
||||
peerID string,
|
||||
peerSSHEnabled bool,
|
||||
postureFailedPeers *map[string]map[string]struct{},
|
||||
) (map[string]*nmdata.Peer, map[string]*nmdata.Group, []*nmdata.Policy, []*nmdata.Route, sshRequirements) {
|
||||
relevantPeerIDs := make(map[string]*nmdata.Peer, len(nmd.Peers)/4)
|
||||
relevantGroupIDs := make(map[string]*nmdata.Group, len(nmd.Groups)/4)
|
||||
@@ -296,7 +290,7 @@ func (nmd *NetworkMapData) getPeersGroupsPoliciesRoutes(
|
||||
}
|
||||
|
||||
policyRelevant := false
|
||||
for _, rule := range policy.Rules {
|
||||
for i, rule := range policy.Rules {
|
||||
if rule == nil || !rule.Enabled {
|
||||
continue
|
||||
}
|
||||
@@ -320,24 +314,13 @@ func (nmd *NetworkMapData) getPeersGroupsPoliciesRoutes(
|
||||
}
|
||||
}
|
||||
|
||||
var sourcePeers, destinationPeers []string
|
||||
var peerInSources, peerInDestinations bool
|
||||
|
||||
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)
|
||||
}
|
||||
sources, destinations := nmd.ruleAdmissions(policy, i)
|
||||
peerInSources := nmd.sideAdmitsPeer(rule.Sources, rule.SourceResource, policy.SourcePostureChecks, peerID, peerGroupSet)
|
||||
peerInDestinations := nmd.sideAdmitsPeer(rule.Destinations, rule.DestinationResource, nil, peerID, peerGroupSet)
|
||||
|
||||
if peerInSources {
|
||||
policyRelevant = true
|
||||
for _, pid := range destinationPeers {
|
||||
for _, pid := range destinations.peers {
|
||||
relevantPeerIDs[pid] = nmd.Peers[pid]
|
||||
}
|
||||
for _, dstGroupID := range rule.Destinations {
|
||||
@@ -349,7 +332,7 @@ func (nmd *NetworkMapData) getPeersGroupsPoliciesRoutes(
|
||||
|
||||
if peerInDestinations {
|
||||
policyRelevant = true
|
||||
for _, pid := range sourcePeers {
|
||||
for _, pid := range sources.peers {
|
||||
relevantPeerIDs[pid] = nmd.Peers[pid]
|
||||
}
|
||||
for _, srcGroupID := range rule.Sources {
|
||||
@@ -381,9 +364,11 @@ func (nmd *NetworkMapData) getPeersGroupsPoliciesRoutes(
|
||||
return relevantPeerIDs, relevantGroupIDs, relevantPolicies, relevantRoutes, sshReqs
|
||||
}
|
||||
|
||||
func (nmd *NetworkMapData) getPeersFromGroups(groups []string, peerID string, sourcePostureChecksIDs []string,
|
||||
postureFailedPeers *map[string]map[string]struct{}) ([]string, bool) {
|
||||
peerInGroups := false
|
||||
// getPeersFromGroups returns the admitted members of a rule side given by groups.
|
||||
// The All group, or a single group, alone decides the side. Posture failures are
|
||||
// recorded in postureFailedPeers.
|
||||
func (nmd *NetworkMapData) getPeersFromGroups(groups []string, sourcePostureChecksIDs []string,
|
||||
postureFailedPeers *map[string]map[string]struct{}) []string {
|
||||
filteredPeerIDs := make([]string, 0, 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 {
|
||||
filteredPeerIDs = make([]string, 0, len(group.Peers))
|
||||
peerInGroups = false
|
||||
for _, pid := range group.Peers {
|
||||
if !nmd.admitPolicyPeer(pid, sourcePostureChecksIDs, postureFailedPeers) {
|
||||
continue
|
||||
if nmd.admitPolicyPeer(pid, sourcePostureChecksIDs, postureFailedPeers) {
|
||||
filteredPeerIDs = append(filteredPeerIDs, pid)
|
||||
}
|
||||
|
||||
if pid == peerID {
|
||||
peerInGroups = true
|
||||
continue
|
||||
}
|
||||
|
||||
filteredPeerIDs = append(filteredPeerIDs, pid)
|
||||
}
|
||||
return filteredPeerIDs, peerInGroups
|
||||
return filteredPeerIDs
|
||||
}
|
||||
|
||||
for _, pid := range group.Peers {
|
||||
@@ -416,57 +393,55 @@ func (nmd *NetworkMapData) getPeersFromGroups(groups []string, peerID string, so
|
||||
continue
|
||||
}
|
||||
seenPeerIds[pid] = struct{}{}
|
||||
if !nmd.admitPolicyPeer(pid, sourcePostureChecksIDs, postureFailedPeers) {
|
||||
continue
|
||||
if nmd.admitPolicyPeer(pid, sourcePostureChecksIDs, postureFailedPeers) {
|
||||
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
|
||||
// like a member of a group holding only that peer.
|
||||
func (nmd *NetworkMapData) getPeerFromResource(resource nmdata.Resource, peerID string, sourcePostureChecksIDs []string,
|
||||
postureFailedPeers *map[string]map[string]struct{}) ([]string, bool) {
|
||||
func (nmd *NetworkMapData) getPeerFromResource(resource nmdata.Resource, sourcePostureChecksIDs []string,
|
||||
postureFailedPeers *map[string]map[string]struct{}) []string {
|
||||
if !nmd.admitPolicyPeer(resource.ID, sourcePostureChecksIDs, postureFailedPeers) {
|
||||
return nil, false
|
||||
return nil
|
||||
}
|
||||
if resource.ID == peerID {
|
||||
return nil, true
|
||||
}
|
||||
return []string{resource.ID}, false
|
||||
return []string{resource.ID}
|
||||
}
|
||||
|
||||
// admitPolicyPeer applies the per-peer admission of a rule side: the peer must exist,
|
||||
// be validated and pass the rule's posture checks. A failed check is recorded in
|
||||
// postureFailedPeers.
|
||||
// admitPolicyPeer is admitsPeer that records a failed posture check in postureFailedPeers.
|
||||
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]
|
||||
if !ok || peer == nil {
|
||||
return false
|
||||
return false, ""
|
||||
}
|
||||
|
||||
if _, ok := nmd.ValidatedPeers[pid]; !ok {
|
||||
return false
|
||||
return false, ""
|
||||
}
|
||||
|
||||
isValid, pname := nmd.validatePostureChecksOnPeerGetFailed(sourcePostureChecksIDs, pid)
|
||||
if !isValid && len(pname) > 0 {
|
||||
if _, ok := (*postureFailedPeers)[pname]; !ok {
|
||||
(*postureFailedPeers)[pname] = make(map[string]struct{})
|
||||
}
|
||||
(*postureFailedPeers)[pname][pid] = struct{}{}
|
||||
return false
|
||||
return false, pname
|
||||
}
|
||||
return true
|
||||
return true, ""
|
||||
}
|
||||
|
||||
func (nmd *NetworkMapData) validatePostureChecksOnPeerGetFailed(sourcePostureChecksID []string, peerID string) (bool, string) {
|
||||
@@ -476,7 +451,7 @@ func (nmd *NetworkMapData) validatePostureChecksOnPeerGetFailed(sourcePostureChe
|
||||
}
|
||||
|
||||
for _, postureChecksID := range sourcePostureChecksID {
|
||||
if valid, cached := nmd.cachedPostureCheckResult(postureChecksID, peerID); cached {
|
||||
if valid, cached := nmd.postureVerdict(postureChecksID, peerID); cached {
|
||||
if !valid {
|
||||
return false, postureChecksID
|
||||
}
|
||||
@@ -494,43 +469,25 @@ func (nmd *NetworkMapData) validatePostureChecksOnPeerGetFailed(sourcePostureChe
|
||||
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() {
|
||||
if len(nmd.PostureChecks) == 0 {
|
||||
nmd.PostureValidation = nil
|
||||
return
|
||||
}
|
||||
|
||||
checkPeerIDs := make(map[string]map[string]struct{})
|
||||
results := make(map[string]map[string]bool)
|
||||
for _, policy := range nmd.Policies {
|
||||
if policy == nil || !policy.Enabled || len(policy.SourcePostureChecks) == 0 {
|
||||
if policy == nil || !policy.Enabled {
|
||||
continue
|
||||
}
|
||||
|
||||
groupPeerIDs := nmd.getUniquePeerIDsFromGroupsIDs(policy.SourceGroups())
|
||||
for _, postureChecksID := range policy.SourcePostureChecks {
|
||||
set := checkPeerIDs[postureChecksID]
|
||||
if set == nil {
|
||||
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{}{}
|
||||
}
|
||||
if _, done := results[postureChecksID]; !done {
|
||||
results[postureChecksID] = nmd.evaluatePostureChecksForPeers(postureChecksID, nmd.postureCheckPeerIDs(postureChecksID))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
results := make(map[string]map[string]bool, len(checkPeerIDs))
|
||||
for postureChecksID, peerIDs := range checkPeerIDs {
|
||||
results[postureChecksID] = nmd.evaluatePostureChecksForPeers(postureChecksID, peerIDs)
|
||||
}
|
||||
nmd.PostureValidation = results
|
||||
}
|
||||
|
||||
@@ -552,38 +509,77 @@ func (nmd *NetworkMapData) evaluatePostureChecksForPeers(postureChecksID string,
|
||||
return results
|
||||
}
|
||||
|
||||
func (nmd *NetworkMapData) cachedPostureCheckResult(postureChecksID, peerID string) (bool, bool) {
|
||||
results, ok := nmd.PostureValidation[postureChecksID]
|
||||
if !ok {
|
||||
return false, false
|
||||
// mergeSourcePeers adds the admitted source peers of a resource policy to the components.
|
||||
func (nmd *NetworkMapData) mergeSourcePeers(components *types.NetworkMapComponents, sources *admittedPeers) {
|
||||
for _, pID := range sources.peers {
|
||||
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 {
|
||||
var dest []string
|
||||
for _, peerID := range inputPeers {
|
||||
if _, validated := nmd.ValidatedPeers[peerID]; !validated {
|
||||
continue
|
||||
// collectPostureFailedPeers sets the components' posture failures to those of every
|
||||
// enabled rule side, of the given resource-policy sources and the ones already
|
||||
// recorded, limited to the checks the components' policies reference and to the peers
|
||||
// in the components. Each failure set is intersected with the components' peers from
|
||||
// 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)
|
||||
if valid {
|
||||
dest = append(dest, peerID)
|
||||
continue
|
||||
for _, checkID := range policy.SourcePostureChecks {
|
||||
referenced[checkID] = struct{}{}
|
||||
}
|
||||
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
|
||||
@@ -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 {
|
||||
if len(records) == 0 || len(peers) == 0 {
|
||||
return nil
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"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.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
|
||||
|
||||
// PostureValidation holds the precomputed posture-check results, keyed by
|
||||
// posture check ID then peer ID. Filled by PrecomputePostureValidation; a
|
||||
// PostureValidation optionally seeds the posture-check results, keyed by
|
||||
// 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
|
||||
// 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
|
||||
|
||||
AllowedUserIDs map[string]struct{}
|
||||
@@ -62,6 +63,9 @@ type NetworkMapData struct { //nolint:revive // established name across the code
|
||||
peerGroupsIdx map[string]map[string]struct{}
|
||||
|
||||
proxyPoliciesOnce sync.Once
|
||||
|
||||
admissionsOnce sync.Once
|
||||
admissionsIdx *admissionCache
|
||||
}
|
||||
|
||||
// AppliedZoneCandidate is an account-level custom DNS zone reduced to the
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package nmdata
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/netbirdio/netbird/shared/management/certposture"
|
||||
@@ -9,8 +11,21 @@ import (
|
||||
// CertificateCheck is the slim twin of posture.CertificateCheck.
|
||||
type CertificateCheck struct {
|
||||
CACertificates []string
|
||||
|
||||
rootsOnce sync.Once
|
||||
roots *x509.CertPool
|
||||
rootsErr 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}}})
|
||||
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 (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/hashicorp/go-version"
|
||||
)
|
||||
@@ -9,27 +10,26 @@ import (
|
||||
// NBVersionCheck is the slim twin of posture.NBVersionCheck.
|
||||
type NBVersionCheck struct {
|
||||
MinVersion string
|
||||
|
||||
constraintOnce sync.Once
|
||||
constraint version.Constraints
|
||||
constraintErr error
|
||||
}
|
||||
|
||||
func (n *NBVersionCheck) check(peer *Peer) (bool, error) {
|
||||
return meetsMinVersion(n.MinVersion, peer.Meta.WtVersion)
|
||||
}
|
||||
|
||||
func meetsMinVersion(minVer, peerVer string) (bool, error) {
|
||||
peerVer = sanitizeVersion(peerVer)
|
||||
minVer = sanitizeVersion(minVer)
|
||||
|
||||
peerNBVer, err := version.NewVersion(peerVer)
|
||||
peerNBVer, err := version.NewVersion(sanitizeVersion(peer.Meta.WtVersion))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
constraints, err := version.NewConstraint(">= " + minVer)
|
||||
if err != nil {
|
||||
return false, err
|
||||
n.constraintOnce.Do(func() {
|
||||
n.constraint, n.constraintErr = version.NewConstraint(">= " + sanitizeVersion(n.MinVersion))
|
||||
})
|
||||
if n.constraintErr != nil {
|
||||
return false, n.constraintErr
|
||||
}
|
||||
|
||||
return constraints.Check(peerNBVer), nil
|
||||
return n.constraint.Check(peerNBVer), nil
|
||||
}
|
||||
|
||||
func sanitizeVersion(v string) string {
|
||||
|
||||
Reference in New Issue
Block a user