mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-27 23:09:08 +01:00
240 lines
7.8 KiB
Go
240 lines
7.8 KiB
Go
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{}{}
|
|
}
|
|
}
|
|
}
|