mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-27 14:59:09 +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
|
// 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")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user