mirror of
https://github.com/netbirdio/netbird.git
synced 2026-06-30 03:39:55 +00:00
Compare commits
11 Commits
client-jso
...
docs/agent
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ee43bd8fc | ||
|
|
3b52259a2b | ||
|
|
390c4fe2fb | ||
|
|
615631567a | ||
|
|
f4daf59bcd | ||
|
|
ff2787e184 | ||
|
|
e20b62ad65 | ||
|
|
18b38943aa | ||
|
|
a400828b89 | ||
|
|
e2bb328a34 | ||
|
|
221b9c012c |
6
.github/workflows/golang-test-linux.yml
vendored
6
.github/workflows/golang-test-linux.yml
vendored
@@ -579,10 +579,11 @@ jobs:
|
|||||||
CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \
|
CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \
|
||||||
NETBIRD_STORE_ENGINE=${{ matrix.store }} \
|
NETBIRD_STORE_ENGINE=${{ matrix.store }} \
|
||||||
CI=true \
|
CI=true \
|
||||||
GIT_BRANCH=${{ github.ref_name }} \
|
|
||||||
go test -tags devcert -run=^$ -bench=. \
|
go test -tags devcert -run=^$ -bench=. \
|
||||||
-exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE,GIT_BRANCH,GITHUB_RUN_ID' \
|
-exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE,GIT_BRANCH,GITHUB_RUN_ID' \
|
||||||
-timeout 20m ./management/... ./shared/management/... $(go list ./management/... ./shared/management/... | grep -v -e /management/server/http)
|
-timeout 20m ./management/... ./shared/management/... $(go list ./management/... ./shared/management/... | grep -v -e /management/server/http)
|
||||||
|
env:
|
||||||
|
GIT_BRANCH: ${{ github.ref_name }}
|
||||||
|
|
||||||
api_benchmark:
|
api_benchmark:
|
||||||
name: "Management / Benchmark (API)"
|
name: "Management / Benchmark (API)"
|
||||||
@@ -673,12 +674,13 @@ jobs:
|
|||||||
CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \
|
CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \
|
||||||
NETBIRD_STORE_ENGINE=${{ matrix.store }} \
|
NETBIRD_STORE_ENGINE=${{ matrix.store }} \
|
||||||
CI=true \
|
CI=true \
|
||||||
GIT_BRANCH=${{ github.ref_name }} \
|
|
||||||
go test -tags=benchmark \
|
go test -tags=benchmark \
|
||||||
-run=^$ \
|
-run=^$ \
|
||||||
-bench=. \
|
-bench=. \
|
||||||
-exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE,GIT_BRANCH,GITHUB_RUN_ID' \
|
-exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE,GIT_BRANCH,GITHUB_RUN_ID' \
|
||||||
-timeout 20m ./management/server/http/...
|
-timeout 20m ./management/server/http/...
|
||||||
|
env:
|
||||||
|
GIT_BRANCH: ${{ github.ref_name }}
|
||||||
|
|
||||||
api_integration_test:
|
api_integration_test:
|
||||||
name: "Management / Integration"
|
name: "Management / Integration"
|
||||||
|
|||||||
@@ -37,6 +37,11 @@
|
|||||||
</strong>
|
</strong>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
> ### 🤖 NetBird Agent Network (Beta)
|
||||||
|
> Identity-aware access control for AI agents — keyless access to LLM APIs and private
|
||||||
|
> resources over the encrypted NetBird tunnel. See [`agent-network/`](agent-network/) or
|
||||||
|
> learn more at **[netbird.ai](https://netbird.ai)**.
|
||||||
|
|
||||||
**NetBird combines a configuration-free peer-to-peer private network and a centralized access control system in a single platform, making it easy to create secure private networks for your organization or home.**
|
**NetBird combines a configuration-free peer-to-peer private network and a centralized access control system in a single platform, making it easy to create secure private networks for your organization or home.**
|
||||||
|
|
||||||
**Connect.** NetBird creates a WireGuard-based overlay network that automatically connects your machines over an encrypted tunnel, leaving behind the hassle of opening ports, complex firewall rules, VPN gateways, and so forth.
|
**Connect.** NetBird creates a WireGuard-based overlay network that automatically connects your machines over an encrypted tunnel, leaving behind the hassle of opening ports, complex firewall rules, VPN gateways, and so forth.
|
||||||
|
|||||||
39
agent-network/README.md
Normal file
39
agent-network/README.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# NetBird Agent Network
|
||||||
|
|
||||||
|
Agent Network is NetBird's access control layer for AI agents and the people who run
|
||||||
|
them. It gives every agent a real identity, tied to your identity provider (IdP), and
|
||||||
|
governs what it can reach — the LLM APIs and AI gateways it can call, and the internal
|
||||||
|
resources it can access. Traffic flows only over the encrypted NetBird tunnel, scoped by
|
||||||
|
policy, with no API keys to leak.
|
||||||
|
|
||||||
|
> **Beta.** Agent Network is open source and can be self-hosted on your own
|
||||||
|
> infrastructure.
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
Agent Network is built on two existing NetBird capabilities:
|
||||||
|
|
||||||
|
- **Overlay network** — the encrypted WireGuard mesh between peers.
|
||||||
|
- **Reverse proxy** — a NetBird peer that terminates LLM requests, establishes the
|
||||||
|
caller's identity, evaluates policies/limits/guardrails, injects the upstream provider
|
||||||
|
key server-side, forwards to the API or gateway, and records usage.
|
||||||
|
|
||||||
|
LLM traffic is routed through the proxy's identity-aware pipeline, while internal
|
||||||
|
resources (databases, internal APIs, self-hosted models) are reached directly over
|
||||||
|
peer-to-peer WireGuard tunnels, governed by the same identities and access policies.
|
||||||
|
|
||||||
|
## Where the code lives
|
||||||
|
|
||||||
|
There is no separate "agent-network" service — it reuses the reverse-proxy and management
|
||||||
|
components:
|
||||||
|
|
||||||
|
- [`proxy/`](../proxy) — the NetBird reverse proxy that serves the agent network endpoint
|
||||||
|
and runs the per-request middleware pipeline.
|
||||||
|
- [`management/internals/modules/reverseproxy/`](../management/internals/modules/reverseproxy)
|
||||||
|
— the management-side control plane: providers, policies, guardrails, limits, routing,
|
||||||
|
and usage/access logs.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
Full documentation, architecture, and quickstart:
|
||||||
|
**https://netbird.ai**
|
||||||
@@ -418,7 +418,14 @@ func newServiceClient(args *newServiceClientArgs) *serviceClient {
|
|||||||
case args.showProfiles:
|
case args.showProfiles:
|
||||||
s.showProfilesUI()
|
s.showProfilesUI()
|
||||||
case args.showQuickActions:
|
case args.showQuickActions:
|
||||||
s.showQuickActionsUI()
|
// Suppress the on-boot Quick Actions popup when the daemon
|
||||||
|
// reports DisableAutoConnect=true — that flag carries both the
|
||||||
|
// user's "Connect on Startup = off" preference AND any MDM-
|
||||||
|
// enforced override (applyMDMPolicy writes the policy value
|
||||||
|
// into the same Config field). See netbirdio/netbird#5744.
|
||||||
|
if !s.disableAutoConnectFromDaemon() {
|
||||||
|
s.showQuickActionsUI()
|
||||||
|
}
|
||||||
case args.showUpdate:
|
case args.showUpdate:
|
||||||
s.showUpdateProgress(ctx, args.showUpdateVersion)
|
s.showUpdateProgress(ctx, args.showUpdateVersion)
|
||||||
}
|
}
|
||||||
@@ -1338,6 +1345,40 @@ func (s *serviceClient) getFeatures() (*proto.GetFeaturesResponse, error) {
|
|||||||
return features, nil
|
return features, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// disableAutoConnectFromDaemon returns true when the daemon reports
|
||||||
|
// the active profile has DisableAutoConnect=true. Used by the
|
||||||
|
// --quick-actions startup path to suppress the on-boot popup when the
|
||||||
|
// user (or an MDM admin) opted out of auto-connecting; both cases
|
||||||
|
// converge on the same Config field because applyMDMPolicy writes the
|
||||||
|
// policy value into it. Returns false on any RPC / lookup failure so a
|
||||||
|
// daemon hiccup does not silently swallow the popup.
|
||||||
|
func (s *serviceClient) disableAutoConnectFromDaemon() bool {
|
||||||
|
activeProf, err := s.profileManager.GetActiveProfile()
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("disableAutoConnectFromDaemon: get active profile: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
currUser, err := user.Current()
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("disableAutoConnectFromDaemon: get current user: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
conn, err := s.getSrvClient(failFastTimeout)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("disableAutoConnectFromDaemon: get daemon client: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
srvCfg, err := conn.GetConfig(s.ctx, &proto.GetConfigRequest{
|
||||||
|
ProfileName: activeProf.ID.String(),
|
||||||
|
Username: currUser.Username,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("disableAutoConnectFromDaemon: GetConfig RPC: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return srvCfg.GetDisableAutoConnect()
|
||||||
|
}
|
||||||
|
|
||||||
// getSrvConfig from the service to show it in the settings window.
|
// getSrvConfig from the service to show it in the settings window.
|
||||||
func (s *serviceClient) getSrvConfig() {
|
func (s *serviceClient) getSrvConfig() {
|
||||||
s.managementURL = profilemanager.DefaultManagementURL
|
s.managementURL = profilemanager.DefaultManagementURL
|
||||||
|
|||||||
@@ -497,7 +497,7 @@ func (c *Controller) BufferUpdateAffectedPeers(ctx context.Context, accountID st
|
|||||||
c.accountManagerMetrics.CountUpdateAccountPeersTriggered(string(reason.Resource), string(reason.Operation))
|
c.accountManagerMetrics.CountUpdateAccountPeersTriggered(string(reason.Resource), string(reason.Operation))
|
||||||
}
|
}
|
||||||
|
|
||||||
log.WithContext(ctx).Tracef("buffer updating %d affected peers for account %s from %s", len(peerIDs), accountID, util.GetCallerName())
|
log.WithContext(ctx).Tracef("buffer updating %d affected peers for account %s from %s with reason %s/%s", len(peerIDs), accountID, util.GetCallerName(), reason.Operation, reason.Resource)
|
||||||
|
|
||||||
bufUpd, _ := c.affectedPeerUpdateLocks.LoadOrStore(accountID, &bufferAffectedUpdate{
|
bufUpd, _ := c.affectedPeerUpdateLocks.LoadOrStore(accountID, &bufferAffectedUpdate{
|
||||||
peerIDs: make(map[string]struct{}),
|
peerIDs: make(map[string]struct{}),
|
||||||
@@ -610,12 +610,10 @@ func (c *Controller) GetValidatedPeerWithMap(ctx context.Context, isRequiresAppr
|
|||||||
return nil, nil, 0, err
|
return nil, nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
startPosture := time.Now()
|
|
||||||
postureChecks, err := c.getPeerPostureChecks(account, peerID)
|
postureChecks, err := c.getPeerPostureChecks(account, peerID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, 0, err
|
return nil, nil, 0, err
|
||||||
}
|
}
|
||||||
log.WithContext(ctx).Debugf("getPeerPostureChecks took %s", time.Since(startPosture))
|
|
||||||
|
|
||||||
accountZones, err := c.repo.GetAccountZones(ctx, account.Id)
|
accountZones, err := c.repo.GetAccountZones(ctx, account.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const (
|
|||||||
reconnThreshold = 5 * time.Minute
|
reconnThreshold = 5 * time.Minute
|
||||||
baseBlockDuration = 10 * time.Minute // Duration for which a peer is banned after exceeding the reconnection limit
|
baseBlockDuration = 10 * time.Minute // Duration for which a peer is banned after exceeding the reconnection limit
|
||||||
reconnLimitForBan = 30 // Number of reconnections within the reconnTreshold that triggers a ban
|
reconnLimitForBan = 30 // Number of reconnections within the reconnTreshold that triggers a ban
|
||||||
metaChangeLimit = 3 // Number of reconnections with different metadata that triggers a ban of one peer
|
metaChangeLimit = 5 // Number of reconnections with different metadata that triggers a ban of one peer
|
||||||
)
|
)
|
||||||
|
|
||||||
type lfConfig struct {
|
type lfConfig struct {
|
||||||
@@ -139,7 +139,7 @@ func (l *loginFilter) addLogin(wgPubKey string, metaHash uint64) {
|
|||||||
state.lastSeen = now
|
state.lastSeen = now
|
||||||
}
|
}
|
||||||
|
|
||||||
func metaHash(meta nbpeer.PeerSystemMeta, pubip string) uint64 {
|
func metaHash(meta nbpeer.PeerSystemMeta) uint64 {
|
||||||
h := fnv.New64a()
|
h := fnv.New64a()
|
||||||
|
|
||||||
h.Write([]byte(meta.WtVersion))
|
h.Write([]byte(meta.WtVersion))
|
||||||
@@ -147,14 +147,6 @@ func metaHash(meta nbpeer.PeerSystemMeta, pubip string) uint64 {
|
|||||||
h.Write([]byte(meta.KernelVersion))
|
h.Write([]byte(meta.KernelVersion))
|
||||||
h.Write([]byte(meta.Hostname))
|
h.Write([]byte(meta.Hostname))
|
||||||
h.Write([]byte(meta.SystemSerialNumber))
|
h.Write([]byte(meta.SystemSerialNumber))
|
||||||
h.Write([]byte(pubip))
|
|
||||||
|
|
||||||
macs := uint64(0)
|
return h.Sum64()
|
||||||
for _, na := range meta.NetworkAddresses {
|
|
||||||
for _, r := range na.Mac {
|
|
||||||
macs += uint64(r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return h.Sum64() + macs
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -164,9 +164,7 @@ func BenchmarkHashingMethods(b *testing.B) {
|
|||||||
KernelVersion: "5.15.0-76-generic",
|
KernelVersion: "5.15.0-76-generic",
|
||||||
Hostname: "prod-server-database-01",
|
Hostname: "prod-server-database-01",
|
||||||
SystemSerialNumber: "PC-1234567890",
|
SystemSerialNumber: "PC-1234567890",
|
||||||
NetworkAddresses: []nbpeer.NetworkAddress{{Mac: "00:1B:44:11:3A:B7"}, {Mac: "00:1B:44:11:3A:B8"}},
|
|
||||||
}
|
}
|
||||||
pubip := "8.8.8.8"
|
|
||||||
|
|
||||||
var resultString string
|
var resultString string
|
||||||
var resultUint uint64
|
var resultUint uint64
|
||||||
@@ -175,7 +173,7 @@ func BenchmarkHashingMethods(b *testing.B) {
|
|||||||
b.ReportAllocs()
|
b.ReportAllocs()
|
||||||
b.ResetTimer()
|
b.ResetTimer()
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
resultString = builderString(meta, pubip)
|
resultString = builderString(meta)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -183,7 +181,7 @@ func BenchmarkHashingMethods(b *testing.B) {
|
|||||||
b.ReportAllocs()
|
b.ReportAllocs()
|
||||||
b.ResetTimer()
|
b.ResetTimer()
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
resultString = fnvHashToString(meta, pubip)
|
resultString = fnvHashToString(meta)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -191,7 +189,7 @@ func BenchmarkHashingMethods(b *testing.B) {
|
|||||||
b.ReportAllocs()
|
b.ReportAllocs()
|
||||||
b.ResetTimer()
|
b.ResetTimer()
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
resultUint = metaHash(meta, pubip)
|
resultUint = metaHash(meta)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -199,29 +197,20 @@ func BenchmarkHashingMethods(b *testing.B) {
|
|||||||
_ = resultUint
|
_ = resultUint
|
||||||
}
|
}
|
||||||
|
|
||||||
func fnvHashToString(meta nbpeer.PeerSystemMeta, pubip string) string {
|
func fnvHashToString(meta nbpeer.PeerSystemMeta) string {
|
||||||
h := fnv.New64a()
|
h := fnv.New64a()
|
||||||
|
|
||||||
if len(meta.NetworkAddresses) != 0 {
|
|
||||||
for _, na := range meta.NetworkAddresses {
|
|
||||||
h.Write([]byte(na.Mac))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
h.Write([]byte(meta.WtVersion))
|
h.Write([]byte(meta.WtVersion))
|
||||||
h.Write([]byte(meta.OSVersion))
|
h.Write([]byte(meta.OSVersion))
|
||||||
h.Write([]byte(meta.KernelVersion))
|
h.Write([]byte(meta.KernelVersion))
|
||||||
h.Write([]byte(meta.Hostname))
|
h.Write([]byte(meta.Hostname))
|
||||||
h.Write([]byte(meta.SystemSerialNumber))
|
h.Write([]byte(meta.SystemSerialNumber))
|
||||||
h.Write([]byte(pubip))
|
|
||||||
|
|
||||||
return strconv.FormatUint(h.Sum64(), 16)
|
return strconv.FormatUint(h.Sum64(), 16)
|
||||||
}
|
}
|
||||||
|
|
||||||
func builderString(meta nbpeer.PeerSystemMeta, pubip string) string {
|
func builderString(meta nbpeer.PeerSystemMeta) string {
|
||||||
mac := getMacAddress(meta.NetworkAddresses)
|
estimatedSize := len(meta.WtVersion) + len(meta.OSVersion) + len(meta.KernelVersion) + len(meta.Hostname) + len(meta.SystemSerialNumber) + 4
|
||||||
estimatedSize := len(meta.WtVersion) + len(meta.OSVersion) + len(meta.KernelVersion) + len(meta.Hostname) + len(meta.SystemSerialNumber) +
|
|
||||||
len(pubip) + len(mac) + 6
|
|
||||||
|
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
b.Grow(estimatedSize)
|
b.Grow(estimatedSize)
|
||||||
@@ -235,23 +224,10 @@ func builderString(meta nbpeer.PeerSystemMeta, pubip string) string {
|
|||||||
b.WriteString(meta.Hostname)
|
b.WriteString(meta.Hostname)
|
||||||
b.WriteByte('|')
|
b.WriteByte('|')
|
||||||
b.WriteString(meta.SystemSerialNumber)
|
b.WriteString(meta.SystemSerialNumber)
|
||||||
b.WriteByte('|')
|
|
||||||
b.WriteString(pubip)
|
|
||||||
|
|
||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func getMacAddress(nas []nbpeer.NetworkAddress) string {
|
|
||||||
if len(nas) == 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
macs := make([]string, 0, len(nas))
|
|
||||||
for _, na := range nas {
|
|
||||||
macs = append(macs, na.Mac)
|
|
||||||
}
|
|
||||||
return strings.Join(macs, "/")
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkLoginFilter_ParallelLoad(b *testing.B) {
|
func BenchmarkLoginFilter_ParallelLoad(b *testing.B) {
|
||||||
filter := newLoginFilterWithCfg(testAdvancedCfg())
|
filter := newLoginFilterWithCfg(testAdvancedCfg())
|
||||||
numKeys := 100000
|
numKeys := 100000
|
||||||
|
|||||||
@@ -254,7 +254,7 @@ func (s *Server) Sync(req *proto.EncryptedMessage, srv proto.ManagementService_S
|
|||||||
return mapError(ctx, err)
|
return mapError(ctx, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
metahashed := metaHash(peerMeta, sRealIP)
|
metahashed := metaHash(peerMeta)
|
||||||
if userID == "" && !s.loginFilter.allowLogin(peerKey.String(), metahashed) {
|
if userID == "" && !s.loginFilter.allowLogin(peerKey.String(), metahashed) {
|
||||||
if s.appMetrics != nil {
|
if s.appMetrics != nil {
|
||||||
s.appMetrics.GRPCMetrics().CountSyncRequestBlocked()
|
s.appMetrics.GRPCMetrics().CountSyncRequestBlocked()
|
||||||
@@ -306,7 +306,7 @@ func (s *Server) Sync(req *proto.EncryptedMessage, srv proto.ManagementService_S
|
|||||||
log.WithContext(ctx).Tracef("peer system meta has to be provided on sync. Peer %s, remote addr %s", peerKey.String(), realIP)
|
log.WithContext(ctx).Tracef("peer system meta has to be provided on sync. Peer %s, remote addr %s", peerKey.String(), realIP)
|
||||||
}
|
}
|
||||||
|
|
||||||
metahash := metaHash(peerMeta, realIP.String())
|
metahash := metaHash(peerMeta)
|
||||||
s.loginFilter.addLogin(peerKey.String(), metahash)
|
s.loginFilter.addLogin(peerKey.String(), metahash)
|
||||||
|
|
||||||
peer, netMap, postureChecks, dnsFwdPort, err := s.accountManager.SyncAndMarkPeer(ctx, accountID, peerKey.String(), peerMeta, realIP, syncStart)
|
peer, netMap, postureChecks, dnsFwdPort, err := s.accountManager.SyncAndMarkPeer(ctx, accountID, peerKey.String(), peerMeta, realIP, syncStart)
|
||||||
@@ -732,7 +732,7 @@ func (s *Server) Login(ctx context.Context, req *proto.EncryptedMessage) (*proto
|
|||||||
}
|
}
|
||||||
|
|
||||||
peerMeta := extractPeerMeta(ctx, loginReq.GetMeta())
|
peerMeta := extractPeerMeta(ctx, loginReq.GetMeta())
|
||||||
metahashed := metaHash(peerMeta, sRealIP)
|
metahashed := metaHash(peerMeta)
|
||||||
if !s.loginFilter.allowLogin(peerKey.String(), metahashed) {
|
if !s.loginFilter.allowLogin(peerKey.String(), metahashed) {
|
||||||
if s.logBlockedPeers {
|
if s.logBlockedPeers {
|
||||||
log.WithContext(ctx).Tracef("peer %s with meta hash %d is blocked from login", peerKey.String(), metahashed)
|
log.WithContext(ctx).Tracef("peer %s with meta hash %d is blocked from login", peerKey.String(), metahashed)
|
||||||
@@ -788,7 +788,11 @@ func (s *Server) Login(ctx context.Context, req *proto.EncryptedMessage) (*proto
|
|||||||
ExtraDNSLabels: loginReq.GetDnsLabels(),
|
ExtraDNSLabels: loginReq.GetDnsLabels(),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithContext(ctx).Warnf("failed logging in peer %s: %s", peerKey, err)
|
if errors.Is(err, internalStatus.ErrNoAuthMethodProvided) {
|
||||||
|
log.WithContext(ctx).Tracef("failed logging in peer %s: %s", peerKey, err)
|
||||||
|
} else {
|
||||||
|
log.WithContext(ctx).Warnf("failed logging in peer %s: %s", peerKey, err)
|
||||||
|
}
|
||||||
return nil, mapError(ctx, err)
|
return nil, mapError(ctx, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ func TestAffectedPeers_DependencyCoverageMatrix(t *testing.T) {
|
|||||||
_, err := s.manager.SavePolicy(ctx, s.accountID, userID, peerToResourcePolicyByGroup(s.sourceGroupID, s.resourceGroupID), true)
|
_, err := s.manager.SavePolicy(ctx, s.accountID, userID, peerToResourcePolicyByGroup(s.sourceGroupID, s.resourceGroupID), true)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
return affectedpeers.Change{ChangedPeerIDs: []string{s.routerPeerID}},
|
return affectedpeers.Change{ChangedPeerIDs: []string{s.routerPeerID}},
|
||||||
[]string{s.sourcePeerID}, []string{s.unrelatedPeerID}
|
[]string{s.sourcePeerID, s.routerPeerID}, []string{s.unrelatedPeerID}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -106,11 +106,9 @@ func TestAffectedPeers_DependencyCoverageMatrix(t *testing.T) {
|
|||||||
change, mustContain, mustExclude := r.build(t, s, ctx)
|
change, mustContain, mustExclude := r.build(t, s, ctx)
|
||||||
affected := resolveAffected(t, s.manager.Store, s.accountID, change)
|
affected := resolveAffected(t, s.manager.Store, s.accountID, change)
|
||||||
|
|
||||||
for _, id := range mustContain {
|
assert.ElementsMatch(t, affected, mustContain, "expected peer to be affected")
|
||||||
assert.Contains(t, affected, id, "expected peer to be affected")
|
for _, peerID := range mustExclude {
|
||||||
}
|
assert.NotContains(t, affected, peerID, "peer must not be affected")
|
||||||
for _, id := range mustExclude {
|
|
||||||
assert.NotContains(t, affected, id, "peer must not be affected")
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -251,7 +251,9 @@ func TestAffectedPeers_E2E_UpdateResource_DestinationResourcePolicy_RefreshesSou
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAffectedPeers_E2E_UpdateResource_DisabledSiblingRouter_StillBridged(t *testing.T) {
|
// A disabled sibling router routes to nobody, so updating a resource on its network
|
||||||
|
// must NOT refresh its peer (the enabled router carries the bridge instead).
|
||||||
|
func TestAffectedPeers_E2E_UpdateResource_DisabledSiblingRouterNotBridged(t *testing.T) {
|
||||||
s := setupRouterScenario(t, true)
|
s := setupRouterScenario(t, true)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
@@ -274,13 +276,18 @@ func TestAffectedPeers_E2E_UpdateResource_DisabledSiblingRouter_StillBridged(t *
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
disabledCh := s.updateManager.CreateChannel(ctx, disabledRouterPeer.ID)
|
disabledCh := s.updateManager.CreateChannel(ctx, disabledRouterPeer.ID)
|
||||||
t.Cleanup(func() { s.updateManager.CloseChannel(ctx, disabledRouterPeer.ID) })
|
enabledCh := s.updateManager.CreateChannel(ctx, s.routerPeerID)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
s.updateManager.CloseChannel(ctx, disabledRouterPeer.ID)
|
||||||
|
s.updateManager.CloseChannel(ctx, s.routerPeerID)
|
||||||
|
})
|
||||||
|
|
||||||
settleAffectedUpdates(disabledCh)
|
settleAffectedUpdates(disabledCh, enabledCh)
|
||||||
|
|
||||||
done := make(chan struct{})
|
done := make(chan struct{})
|
||||||
go func() {
|
go func() {
|
||||||
peerShouldReceiveUpdate(t, disabledCh)
|
peerShouldReceiveUpdate(t, enabledCh)
|
||||||
|
peerShouldNotReceiveUpdate(t, disabledCh)
|
||||||
close(done)
|
close(done)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -298,7 +305,7 @@ func TestAffectedPeers_E2E_UpdateResource_DisabledSiblingRouter_StillBridged(t *
|
|||||||
select {
|
select {
|
||||||
case <-done:
|
case <-done:
|
||||||
case <-time.After(peerUpdateTimeout):
|
case <-time.After(peerUpdateTimeout):
|
||||||
t.Error("timeout: resource update did not refresh the disabled sibling router's peer")
|
t.Error("timeout")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -682,6 +682,9 @@ func TestAffectedPeers_AllRoutingPeers_Network(t *testing.T) {
|
|||||||
assert.Contains(t, affected, secondRouterPeer.ID, "second routing peer on the same network must also be affected")
|
assert.Contains(t, affected, secondRouterPeer.ID, "second routing peer on the same network must also be affected")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A disabled router in the snapshot routes to nobody, so it is skipped when the
|
||||||
|
// walk scans existing account data: a policy edit still folds the literal source
|
||||||
|
// group, but not the disabled router's peer.
|
||||||
func TestAffectedPeers_DisabledRouter(t *testing.T) {
|
func TestAffectedPeers_DisabledRouter(t *testing.T) {
|
||||||
s := setupRouterScenario(t, true)
|
s := setupRouterScenario(t, true)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
@@ -694,11 +697,13 @@ func TestAffectedPeers_DisabledRouter(t *testing.T) {
|
|||||||
|
|
||||||
affected := s.resolvePolicyAffected(ctx, peerToResourcePolicyByGroup(s.sourceGroupID, s.resourceGroupID))
|
affected := s.resolvePolicyAffected(ctx, peerToResourcePolicyByGroup(s.sourceGroupID, s.resourceGroupID))
|
||||||
|
|
||||||
assert.Contains(t, affected, s.sourcePeerID, "source peer must be affected")
|
assert.Contains(t, affected, s.sourcePeerID, "source peer (literal policy source group) must be affected")
|
||||||
assert.Contains(t, affected, s.routerPeerID,
|
assert.NotContains(t, affected, s.routerPeerID,
|
||||||
"disabled router's peer must still be affected: Enabled must not gate affected-peers")
|
"a disabled router routes to nobody, so its peer must not be folded from snapshot data")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A disabled resource in the snapshot is skipped: the policy edit still folds the
|
||||||
|
// literal source group, but the resource no longer bridges to its network's router.
|
||||||
func TestAffectedPeers_DisabledResource(t *testing.T) {
|
func TestAffectedPeers_DisabledResource(t *testing.T) {
|
||||||
s := setupRouterScenario(t, true)
|
s := setupRouterScenario(t, true)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
@@ -710,9 +715,9 @@ func TestAffectedPeers_DisabledResource(t *testing.T) {
|
|||||||
|
|
||||||
affected := s.resolvePolicyAffected(ctx, peerToResourcePolicyByGroup(s.sourceGroupID, s.resourceGroupID))
|
affected := s.resolvePolicyAffected(ctx, peerToResourcePolicyByGroup(s.sourceGroupID, s.resourceGroupID))
|
||||||
|
|
||||||
assert.Contains(t, affected, s.sourcePeerID, "source peer must be affected")
|
assert.Contains(t, affected, s.sourcePeerID, "source peer (literal policy source group) must be affected")
|
||||||
assert.Contains(t, affected, s.routerPeerID,
|
assert.NotContains(t, affected, s.routerPeerID,
|
||||||
"disabled resource must still resolve the routing peer: Enabled must not gate affected-peers")
|
"a disabled resource routes to nobody, so its network's router must not be folded from snapshot data")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAffectedPeers_DisabledRule(t *testing.T) {
|
func TestAffectedPeers_DisabledRule(t *testing.T) {
|
||||||
|
|||||||
@@ -96,33 +96,54 @@ func affectedGroupID(i int) string { return fmt.Sprintf("affected-grp-%d", i)
|
|||||||
func affectedGroupName(i int) string { return fmt.Sprintf("AffectedGroup%d", i) }
|
func affectedGroupName(i int) string { return fmt.Sprintf("AffectedGroup%d", i) }
|
||||||
|
|
||||||
func TestCollectGroupChange_PolicyLinked(t *testing.T) {
|
func TestCollectGroupChange_PolicyLinked(t *testing.T) {
|
||||||
manager, s, accountID, _, groupIDs := setupAffectedPeersTest(t)
|
manager, s, accountID, peerIDs, groupIDs := setupAffectedPeersTest(t)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
_, err := manager.SavePolicy(ctx, accountID, userID, &types.Policy{
|
_, err := manager.SavePolicy(ctx, accountID, userID, &types.Policy{
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
Rules: []*types.PolicyRule{
|
Rules: []*types.PolicyRule{
|
||||||
{
|
{
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
Sources: []string{groupIDs[0]},
|
Sources: []string{groupIDs[0]},
|
||||||
Destinations: []string{groupIDs[1]},
|
Destinations: []string{groupIDs[1]},
|
||||||
Bidirectional: true,
|
SourceResource: types.Resource{ID: peerIDs[0], Type: types.ResourceTypePeer},
|
||||||
Action: types.PolicyTrafficActionAccept,
|
DestinationResource: types.Resource{ID: peerIDs[1], Type: types.ResourceTypePeer},
|
||||||
|
Bidirectional: true,
|
||||||
|
Action: types.PolicyTrafficActionAccept,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Enabled: true,
|
||||||
|
Sources: []string{groupIDs[0]},
|
||||||
|
Destinations: []string{groupIDs[1]},
|
||||||
|
SourceResource: types.Resource{ID: peerIDs[2], Type: types.ResourceTypeHost},
|
||||||
|
DestinationResource: types.Resource{ID: peerIDs[3], Type: types.ResourceTypeHost},
|
||||||
|
Bidirectional: true,
|
||||||
|
Action: types.PolicyTrafficActionAccept,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Enabled: true,
|
||||||
|
Sources: []string{groupIDs[0]},
|
||||||
|
Destinations: []string{groupIDs[1]},
|
||||||
|
SourceResource: types.Resource{ID: "", Type: types.ResourceTypePeer},
|
||||||
|
DestinationResource: types.Resource{ID: "", Type: types.ResourceTypePeer},
|
||||||
|
Bidirectional: true,
|
||||||
|
Action: types.PolicyTrafficActionAccept,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, true)
|
}, true)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
groups, _ := collectGroupChangeAffectedGroups(ctx, s, accountID, []string{groupIDs[0]})
|
groups, directPeers := collectGroupChangeAffectedGroups(ctx, s, accountID, []string{groupIDs[0]})
|
||||||
assert.Contains(t, groups, groupIDs[0])
|
assert.ElementsMatch(t, groups, []string{groupIDs[0], groupIDs[1]})
|
||||||
assert.Contains(t, groups, groupIDs[1])
|
assert.ElementsMatch(t, directPeers, []string{peerIDs[1]})
|
||||||
|
|
||||||
groups, _ = collectGroupChangeAffectedGroups(ctx, s, accountID, []string{groupIDs[1]})
|
groups, directPeers = collectGroupChangeAffectedGroups(ctx, s, accountID, []string{groupIDs[1]})
|
||||||
assert.Contains(t, groups, groupIDs[0])
|
assert.ElementsMatch(t, groups, []string{groupIDs[0], groupIDs[1]})
|
||||||
assert.Contains(t, groups, groupIDs[1])
|
assert.ElementsMatch(t, directPeers, []string{peerIDs[0]})
|
||||||
|
|
||||||
groups, _ = collectGroupChangeAffectedGroups(ctx, s, accountID, []string{groupIDs[2]})
|
groups, directPeers = collectGroupChangeAffectedGroups(ctx, s, accountID, []string{groupIDs[2]})
|
||||||
assert.Empty(t, groups)
|
assert.Empty(t, groups)
|
||||||
|
assert.Empty(t, directPeers)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCollectGroupChange_PolicyWithDirectPeerResource(t *testing.T) {
|
func TestCollectGroupChange_PolicyWithDirectPeerResource(t *testing.T) {
|
||||||
@@ -133,20 +154,44 @@ func TestCollectGroupChange_PolicyWithDirectPeerResource(t *testing.T) {
|
|||||||
Enabled: true,
|
Enabled: true,
|
||||||
Rules: []*types.PolicyRule{
|
Rules: []*types.PolicyRule{
|
||||||
{
|
{
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
Sources: []string{groupIDs[0]},
|
Sources: []string{groupIDs[0]},
|
||||||
SourceResource: types.Resource{ID: peerIDs[3], Type: types.ResourceTypePeer},
|
SourceResource: types.Resource{ID: peerIDs[3], Type: types.ResourceTypePeer},
|
||||||
Destinations: []string{groupIDs[1]},
|
DestinationResource: types.Resource{ID: peerIDs[4], Type: types.ResourceTypePeer},
|
||||||
Action: types.PolicyTrafficActionAccept,
|
Destinations: []string{groupIDs[1]},
|
||||||
|
Action: types.PolicyTrafficActionAccept,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Enabled: true,
|
||||||
|
Sources: []string{groupIDs[0]},
|
||||||
|
SourceResource: types.Resource{ID: peerIDs[1], Type: types.ResourceTypeHost},
|
||||||
|
DestinationResource: types.Resource{ID: peerIDs[2], Type: types.ResourceTypeHost},
|
||||||
|
Destinations: []string{groupIDs[1]},
|
||||||
|
Action: types.PolicyTrafficActionAccept,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Enabled: true,
|
||||||
|
Sources: []string{groupIDs[0]},
|
||||||
|
SourceResource: types.Resource{ID: "", Type: types.ResourceTypePeer},
|
||||||
|
DestinationResource: types.Resource{ID: "", Type: types.ResourceTypePeer},
|
||||||
|
Destinations: []string{groupIDs[1]},
|
||||||
|
Action: types.PolicyTrafficActionAccept,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, true)
|
}, true)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
groups, directPeers := collectGroupChangeAffectedGroups(ctx, s, accountID, []string{groupIDs[0]})
|
groups, directPeers := collectGroupChangeAffectedGroups(ctx, s, accountID, []string{groupIDs[0]})
|
||||||
assert.Contains(t, groups, groupIDs[0])
|
assert.ElementsMatch(t, groups, []string{groupIDs[0], groupIDs[1]})
|
||||||
assert.Contains(t, groups, groupIDs[1])
|
assert.ElementsMatch(t, directPeers, []string{peerIDs[4]})
|
||||||
assert.Contains(t, directPeers, peerIDs[3])
|
|
||||||
|
groups, directPeers = collectGroupChangeAffectedGroups(ctx, s, accountID, []string{groupIDs[1]})
|
||||||
|
assert.ElementsMatch(t, groups, []string{groupIDs[0], groupIDs[1]})
|
||||||
|
assert.ElementsMatch(t, directPeers, []string{peerIDs[3]})
|
||||||
|
|
||||||
|
groups, directPeers = collectGroupChangeAffectedGroups(ctx, s, accountID, []string{groupIDs[2]})
|
||||||
|
assert.Empty(t, groups)
|
||||||
|
assert.Empty(t, directPeers)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCollectGroupChange_PolicyWithNonPeerResource_NoDirectPeers(t *testing.T) {
|
func TestCollectGroupChange_PolicyWithNonPeerResource_NoDirectPeers(t *testing.T) {
|
||||||
@@ -168,8 +213,7 @@ func TestCollectGroupChange_PolicyWithNonPeerResource_NoDirectPeers(t *testing.T
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
groups, directPeers := collectGroupChangeAffectedGroups(ctx, s, accountID, []string{groupIDs[0]})
|
groups, directPeers := collectGroupChangeAffectedGroups(ctx, s, accountID, []string{groupIDs[0]})
|
||||||
assert.Contains(t, groups, groupIDs[0])
|
assert.ElementsMatch(t, groups, []string{groupIDs[0], groupIDs[1]})
|
||||||
assert.Contains(t, groups, groupIDs[1])
|
|
||||||
assert.Empty(t, directPeers, "non-peer resources should not produce direct peer IDs")
|
assert.Empty(t, directPeers, "non-peer resources should not produce direct peer IDs")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,6 +338,7 @@ func TestCollectGroupChange_NetworkRouterLinked(t *testing.T) {
|
|||||||
AccountID: accountID,
|
AccountID: accountID,
|
||||||
PeerGroups: []string{groupIDs[0]},
|
PeerGroups: []string{groupIDs[0]},
|
||||||
Peer: peerIDs[3],
|
Peer: peerIDs[3],
|
||||||
|
Enabled: true,
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -324,6 +369,7 @@ func TestCollectGroupChange_NetworkRouterPeerOnlyNoGroups(t *testing.T) {
|
|||||||
NetworkID: net1.ID,
|
NetworkID: net1.ID,
|
||||||
AccountID: accountID,
|
AccountID: accountID,
|
||||||
Peer: peerIDs[4],
|
Peer: peerIDs[4],
|
||||||
|
Enabled: true,
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -373,17 +419,11 @@ func TestCollectGroupChange_MultipleEntities(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
groups, directPeers := collectGroupChangeAffectedGroups(ctx, s, accountID, []string{groupIDs[0]})
|
groups, directPeers := collectGroupChangeAffectedGroups(ctx, s, accountID, []string{groupIDs[0]})
|
||||||
assert.Contains(t, groups, groupIDs[0])
|
assert.ElementsMatch(t, groups, []string{groupIDs[0], groupIDs[1]})
|
||||||
assert.Contains(t, groups, groupIDs[1])
|
|
||||||
assert.NotContains(t, groups, groupIDs[2])
|
|
||||||
assert.NotContains(t, groups, groupIDs[3])
|
|
||||||
assert.Empty(t, directPeers)
|
assert.Empty(t, directPeers)
|
||||||
|
|
||||||
groups, directPeers = collectGroupChangeAffectedGroups(ctx, s, accountID, []string{groupIDs[3]})
|
groups, directPeers = collectGroupChangeAffectedGroups(ctx, s, accountID, []string{groupIDs[3]})
|
||||||
assert.Contains(t, groups, groupIDs[2])
|
assert.ElementsMatch(t, groups, []string{groupIDs[2], groupIDs[3]})
|
||||||
assert.Contains(t, groups, groupIDs[3])
|
|
||||||
assert.NotContains(t, groups, groupIDs[0])
|
|
||||||
assert.NotContains(t, groups, groupIDs[1])
|
|
||||||
assert.Empty(t, directPeers)
|
assert.Empty(t, directPeers)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -452,8 +492,9 @@ func TestResolveAffectedPeers_PolicyBetweenTwoGroups(t *testing.T) {
|
|||||||
result = manager.resolveAffectedPeersForPeerChanges(ctx, s, accountID, []string{peerIDs[1]})
|
result = manager.resolveAffectedPeersForPeerChanges(ctx, s, accountID, []string{peerIDs[1]})
|
||||||
assert.ElementsMatch(t, []string{peerIDs[0], peerIDs[1]}, result)
|
assert.ElementsMatch(t, []string{peerIDs[0], peerIDs[1]}, result)
|
||||||
|
|
||||||
|
// peerIDs[2] is unrelated to the route; only its own map can change.
|
||||||
result = manager.resolveAffectedPeersForPeerChanges(ctx, s, accountID, []string{peerIDs[2]})
|
result = manager.resolveAffectedPeersForPeerChanges(ctx, s, accountID, []string{peerIDs[2]})
|
||||||
assert.Empty(t, result)
|
assert.ElementsMatch(t, []string{peerIDs[2]}, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestResolveAffectedPeers_PolicyThreeGroups(t *testing.T) {
|
func TestResolveAffectedPeers_PolicyThreeGroups(t *testing.T) {
|
||||||
@@ -474,7 +515,7 @@ func TestResolveAffectedPeers_PolicyThreeGroups(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
result := manager.resolveAffectedPeersForPeerChanges(ctx, s, accountID, []string{peerIDs[0]})
|
result := manager.resolveAffectedPeersForPeerChanges(ctx, s, accountID, []string{peerIDs[0]})
|
||||||
assert.ElementsMatch(t, []string{peerIDs[0], peerIDs[1], peerIDs[2]}, result)
|
assert.ElementsMatch(t, []string{peerIDs[0], peerIDs[2]}, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestResolveAffectedPeers_RoutePeerGroups(t *testing.T) {
|
func TestResolveAffectedPeers_RoutePeerGroups(t *testing.T) {
|
||||||
@@ -506,8 +547,9 @@ func TestResolveAffectedPeers_RoutePeerGroups(t *testing.T) {
|
|||||||
result = manager.resolveAffectedPeersForPeerChanges(ctx, s, accountID, []string{peerIDs[1]})
|
result = manager.resolveAffectedPeersForPeerChanges(ctx, s, accountID, []string{peerIDs[1]})
|
||||||
assert.ElementsMatch(t, []string{peerIDs[0], peerIDs[1]}, result)
|
assert.ElementsMatch(t, []string{peerIDs[0], peerIDs[1]}, result)
|
||||||
|
|
||||||
|
// peerIDs[2] is in no policy; only its own map can change, so it refreshes itself.
|
||||||
result = manager.resolveAffectedPeersForPeerChanges(ctx, s, accountID, []string{peerIDs[2]})
|
result = manager.resolveAffectedPeersForPeerChanges(ctx, s, accountID, []string{peerIDs[2]})
|
||||||
assert.Empty(t, result)
|
assert.ElementsMatch(t, []string{peerIDs[2]}, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestResolveAffectedPeers_RouteWithDirectPeer(t *testing.T) {
|
func TestResolveAffectedPeers_RouteWithDirectPeer(t *testing.T) {
|
||||||
@@ -564,9 +606,9 @@ func TestResolveAffectedPeers_RouteWithAccessControlGroups(t *testing.T) {
|
|||||||
result := manager.resolveAffectedPeersForPeerChanges(ctx, s, accountID, []string{peerIDs[2]})
|
result := manager.resolveAffectedPeersForPeerChanges(ctx, s, accountID, []string{peerIDs[2]})
|
||||||
assert.ElementsMatch(t, []string{peerIDs[0], peerIDs[1], peerIDs[2]}, result)
|
assert.ElementsMatch(t, []string{peerIDs[0], peerIDs[1], peerIDs[2]}, result)
|
||||||
|
|
||||||
// peer3 is unrelated
|
// peer3 is unrelated to the route; only its own map can change.
|
||||||
result = manager.resolveAffectedPeersForPeerChanges(ctx, s, accountID, []string{peerIDs[3]})
|
result = manager.resolveAffectedPeersForPeerChanges(ctx, s, accountID, []string{peerIDs[3]})
|
||||||
assert.Empty(t, result)
|
assert.ElementsMatch(t, []string{peerIDs[3]}, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestResolveAffectedPeers_NetworkRouter(t *testing.T) {
|
func TestResolveAffectedPeers_NetworkRouter(t *testing.T) {
|
||||||
@@ -587,6 +629,7 @@ func TestResolveAffectedPeers_NetworkRouter(t *testing.T) {
|
|||||||
AccountID: accountID,
|
AccountID: accountID,
|
||||||
PeerGroups: []string{groupIDs[0]},
|
PeerGroups: []string{groupIDs[0]},
|
||||||
Peer: peerIDs[3],
|
Peer: peerIDs[3],
|
||||||
|
Enabled: true,
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -659,9 +702,13 @@ func TestResolveAffectedPeers_PeerInMultipleGroups(t *testing.T) {
|
|||||||
}, true)
|
}, true)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// peer0 is in group0 AND group1, so both policies apply
|
// peer0 is in group0 AND group1, so both policies apply. A peer change folds
|
||||||
|
// only the changed peer plus the opposite side of each rule: group2 (peer2) via
|
||||||
|
// the group0 policy and group3 (peer3) via the group1 policy. peer1, a co-member
|
||||||
|
// of group1, is a sibling of the changed peer and must NOT refresh.
|
||||||
result := manager.resolveAffectedPeersForPeerChanges(ctx, s, accountID, []string{peerIDs[0]})
|
result := manager.resolveAffectedPeersForPeerChanges(ctx, s, accountID, []string{peerIDs[0]})
|
||||||
assert.ElementsMatch(t, []string{peerIDs[0], peerIDs[1], peerIDs[2], peerIDs[3]}, result)
|
assert.ElementsMatch(t, []string{peerIDs[0], peerIDs[2], peerIDs[3]}, result)
|
||||||
|
assert.NotContains(t, result, peerIDs[1], "co-member of the changed peer's group must not refresh")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestResolveAffectedPeers_MultipleChangedPeers(t *testing.T) {
|
func TestResolveAffectedPeers_MultipleChangedPeers(t *testing.T) {
|
||||||
@@ -697,7 +744,7 @@ func TestResolveAffectedPeers_MultipleChangedPeers(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
result := manager.resolveAffectedPeersForPeerChanges(ctx, s, accountID, []string{peerIDs[0], peerIDs[2]})
|
result := manager.resolveAffectedPeersForPeerChanges(ctx, s, accountID, []string{peerIDs[0], peerIDs[2]})
|
||||||
assert.ElementsMatch(t, []string{peerIDs[0], peerIDs[1], peerIDs[2], peerIDs[3]}, result)
|
assert.ElementsMatch(t, []string{peerIDs[0], peerIDs[2], peerIDs[1], peerIDs[3]}, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestResolveAffectedPeers_SharedGroupAcrossPolicyAndRoute(t *testing.T) {
|
func TestResolveAffectedPeers_SharedGroupAcrossPolicyAndRoute(t *testing.T) {
|
||||||
@@ -854,8 +901,9 @@ func TestAffectedPeers_IsolatedPolicies(t *testing.T) {
|
|||||||
assert.NotContains(t, result, peerIDs[0])
|
assert.NotContains(t, result, peerIDs[0])
|
||||||
assert.NotContains(t, result, peerIDs[1])
|
assert.NotContains(t, result, peerIDs[1])
|
||||||
|
|
||||||
|
// peerIDs[4] is in neither isolated policy; only its own map can change.
|
||||||
result = manager.resolveAffectedPeersForPeerChanges(ctx, s, accountID, []string{peerIDs[4]})
|
result = manager.resolveAffectedPeersForPeerChanges(ctx, s, accountID, []string{peerIDs[4]})
|
||||||
assert.Empty(t, result)
|
assert.ElementsMatch(t, []string{peerIDs[4]}, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAffectedPeers_IsolatedRouteAndPolicy(t *testing.T) {
|
func TestAffectedPeers_IsolatedRouteAndPolicy(t *testing.T) {
|
||||||
@@ -977,12 +1025,13 @@ func TestAffectedPeers_GroupUpdateOnlyAffectsLinkedPeers(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAffectedPeers_UnlinkedGroupChange_NoUpdates(t *testing.T) {
|
// A peer in no policy/route refreshes only itself — no other peer is affected.
|
||||||
|
func TestAffectedPeers_UnlinkedPeerChange_RefreshesSelfOnly(t *testing.T) {
|
||||||
manager, s, accountID, peerIDs, _ := setupAffectedPeersTest(t)
|
manager, s, accountID, peerIDs, _ := setupAffectedPeersTest(t)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
result := manager.resolveAffectedPeersForPeerChanges(ctx, s, accountID, []string{peerIDs[0]})
|
result := manager.resolveAffectedPeersForPeerChanges(ctx, s, accountID, []string{peerIDs[0]})
|
||||||
assert.Empty(t, result)
|
assert.ElementsMatch(t, []string{peerIDs[0]}, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestAffectedPeers_PolicyChange_UnrelatedPeerNoUpdate verifies that creating/deleting a
|
// TestAffectedPeers_PolicyChange_UnrelatedPeerNoUpdate verifies that creating/deleting a
|
||||||
@@ -1332,6 +1381,7 @@ func TestAffectedPeers_NetworkRouterUnlinkedPeerNoUpdate(t *testing.T) {
|
|||||||
NetworkID: net1.ID,
|
NetworkID: net1.ID,
|
||||||
AccountID: accountID,
|
AccountID: accountID,
|
||||||
PeerGroups: []string{"nr-grpA"},
|
PeerGroups: []string{"nr-grpA"},
|
||||||
|
Enabled: true,
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -1755,7 +1805,9 @@ func TestCollectAffectedFromProxyServices_GroupContainingTargetPeerChanged(t *te
|
|||||||
assert.Contains(t, directPeers, peerIDs[1], "target peer must be refreshed")
|
assert.Contains(t, directPeers, peerIDs[1], "target peer must be refreshed")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCollectAffectedFromProxyServices_DisabledServiceStillMatches(t *testing.T) {
|
// A disabled service in the snapshot proxies nothing, so it is skipped: a changed
|
||||||
|
// target peer does not pull in the service's proxy peer.
|
||||||
|
func TestCollectAffectedFromProxyServices_DisabledServiceSkipped(t *testing.T) {
|
||||||
manager, s, accountID, peerIDs, _ := setupAffectedPeersTest(t)
|
manager, s, accountID, peerIDs, _ := setupAffectedPeersTest(t)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
@@ -1781,8 +1833,7 @@ func TestCollectAffectedFromProxyServices_DisabledServiceStillMatches(t *testing
|
|||||||
require.NoError(t, s.CreateService(ctx, svc))
|
require.NoError(t, s.CreateService(ctx, svc))
|
||||||
|
|
||||||
_, directPeers := collectPeerChangeAffectedGroups(ctx, manager.Store, accountID, nil, []string{peerIDs[1]})
|
_, directPeers := collectPeerChangeAffectedGroups(ctx, manager.Store, accountID, nil, []string{peerIDs[1]})
|
||||||
assert.Contains(t, directPeers, peerIDs[0], "disabled service should still trigger a refresh so peers are ready when re-enabled")
|
assert.NotContains(t, directPeers, peerIDs[0], "a disabled service proxies nothing, so its proxy peer must not be folded")
|
||||||
assert.Contains(t, directPeers, peerIDs[1], "disabled target should still trigger a refresh")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCollectAffectedFromProxyServices_NonPeerTargetType(t *testing.T) {
|
func TestCollectAffectedFromProxyServices_NonPeerTargetType(t *testing.T) {
|
||||||
|
|||||||
@@ -6,7 +6,12 @@
|
|||||||
// and before a delete/removal severs the old state).
|
// and before a delete/removal severs the old state).
|
||||||
// - Snapshot.Expand: in-memory walk, no store access. Run AFTER the tx commits.
|
// - Snapshot.Expand: in-memory walk, no store access. Run AFTER the tx commits.
|
||||||
//
|
//
|
||||||
// Enabled is never consulted: toggling it is itself an observable change.
|
// Enabled handling differs by source. Disabled objects in the SNAPSHOT (existing
|
||||||
|
// account policies/resources/routers/routes/proxy services and their rules/targets)
|
||||||
|
// route to nobody and are skipped — they cannot affect any peer's map. Objects in
|
||||||
|
// the CHANGE itself are processed regardless of Enabled, so disabling one still
|
||||||
|
// refreshes the peers that lose access (the toggle is the observable change, and the
|
||||||
|
// update carries the old∪new state).
|
||||||
package affectedpeers
|
package affectedpeers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -61,7 +66,8 @@ func Load(ctx context.Context, s store.Store, accountID string, c Change) (*Snap
|
|||||||
// loadCollections reads the policy/route/nameserver/dns/router/resource/proxy
|
// loadCollections reads the policy/route/nameserver/dns/router/resource/proxy
|
||||||
// collections a Change can touch, gated to what the walk needs.
|
// collections a Change can touch, gated to what the walk needs.
|
||||||
func (snap *Snapshot) loadCollections(ctx context.Context, s store.Store, accountID string, c Change) error {
|
func (snap *Snapshot) loadCollections(ctx context.Context, s store.Store, accountID string, c Change) error {
|
||||||
hasGroupOrPeerChange := len(c.ChangedGroupIDs) > 0 || len(c.ChangedPeerIDs) > 0 || len(c.Resources) > 0
|
// LinkGroups drive the same policy/route/dns walk as a changed group or peer.
|
||||||
|
hasGroupOrPeerChange := len(c.ChangedGroupIDs) > 0 || len(c.ChangedPeerIDs) > 0 || len(c.LinkGroups) > 0 || len(c.Resources) > 0
|
||||||
hasNetworkObject := len(c.Routers) > 0 || len(c.Resources) > 0 || len(c.Networks) > 0
|
hasNetworkObject := len(c.Routers) > 0 || len(c.Resources) > 0 || len(c.Networks) > 0
|
||||||
// the resource<->router bridge can fire for any of these
|
// the resource<->router bridge can fire for any of these
|
||||||
needsRoutersResources := hasGroupOrPeerChange || len(c.PostureCheckIDs) > 0 || len(c.Policies) > 0 || hasNetworkObject
|
needsRoutersResources := hasGroupOrPeerChange || len(c.PostureCheckIDs) > 0 || len(c.Policies) > 0 || hasNetworkObject
|
||||||
@@ -76,7 +82,7 @@ func (snap *Snapshot) loadCollections(ctx context.Context, s store.Store, accoun
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(c.ChangedGroupIDs) > 0 || len(c.ChangedPeerIDs) > 0 {
|
if len(c.ChangedGroupIDs) > 0 || len(c.ChangedPeerIDs) > 0 || len(c.LinkGroups) > 0 {
|
||||||
if err := snap.loadDNS(ctx, s, accountID); err != nil {
|
if err := snap.loadDNS(ctx, s, accountID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -174,6 +180,24 @@ type Change struct {
|
|||||||
// folded in — but only when the group is linked (an unlinked group has no map
|
// folded in — but only when the group is linked (an unlinked group has no map
|
||||||
// impact), matching how current members are handled.
|
// impact), matching how current members are handled.
|
||||||
RemovedPeersByGroup map[string][]string
|
RemovedPeersByGroup map[string][]string
|
||||||
|
|
||||||
|
// OutputPeerIDs are peers folded straight into the result without seeding their
|
||||||
|
// group memberships into the walk. Use for the peer whose group membership changed:
|
||||||
|
// the peer itself must refresh, but its OTHER groups did not change, so they must
|
||||||
|
// not be walked. Contrast ChangedPeerIDs, which seeds ALL of the peer's groups
|
||||||
|
// (correct when the peer's own attributes changed, e.g. IP/status).
|
||||||
|
OutputPeerIDs []string
|
||||||
|
|
||||||
|
// LinkGroups are groups used ONLY to match policies/routes/routers and walk to the
|
||||||
|
// OPPOSITE side — they are never expanded to their own members. Use this when a
|
||||||
|
// peer's group membership changed: pass the peer in ChangedPeerIDs and its
|
||||||
|
// group(s) here. The opposite side of the policies the group participates in
|
||||||
|
// refreshes, but the group's other members (siblings) do not — nothing changed for
|
||||||
|
// them. For an intra-group policy (A→A) the opposite side IS the group, so its
|
||||||
|
// members still refresh via the opposite-side fold, exactly when they genuinely
|
||||||
|
// gain/lose the changed peer. Unlike ChangedGroupIDs, a LinkGroup is not added to
|
||||||
|
// the output, so a one-sided membership change never wakes the whole group.
|
||||||
|
LinkGroups []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Change) isEmpty() bool {
|
func (c Change) isEmpty() bool {
|
||||||
@@ -186,7 +210,9 @@ func (c Change) isEmpty() bool {
|
|||||||
len(c.Networks) == 0 &&
|
len(c.Networks) == 0 &&
|
||||||
len(c.PostureCheckIDs) == 0 &&
|
len(c.PostureCheckIDs) == 0 &&
|
||||||
len(c.DistributionGroupIDs) == 0 &&
|
len(c.DistributionGroupIDs) == 0 &&
|
||||||
len(c.RemovedPeersByGroup) == 0
|
len(c.RemovedPeersByGroup) == 0 &&
|
||||||
|
len(c.LinkGroups) == 0 &&
|
||||||
|
len(c.OutputPeerIDs) == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expand returns the deduplicated affected peer IDs from the preloaded Snapshot,
|
// Expand returns the deduplicated affected peer IDs from the preloaded Snapshot,
|
||||||
@@ -197,8 +223,8 @@ func (snap *Snapshot) Expand(ctx context.Context, accountID string, c Change) []
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
r := newResolver(ctx, snap, accountID, c)
|
r := newResolver(ctx, snap, accountID, c)
|
||||||
log.WithContext(ctx).Tracef("affectedpeers expand start: account=%s changedGroups=%v changedPeers=%v policies=%d routes=%d routers=%d resources=%d networks=%d postureChecks=%v distributionGroups=%v",
|
log.WithContext(ctx).Tracef("affectedpeers expand start: account=%s changedGroups=%v changedPeers=%v linkGroups=%v policies=%d routes=%d routers=%d resources=%d networks=%d postureChecks=%v distributionGroups=%v",
|
||||||
accountID, c.ChangedGroupIDs, c.ChangedPeerIDs, len(c.Policies), len(c.Routes), len(c.Routers), len(c.Resources), len(c.Networks), c.PostureCheckIDs, c.DistributionGroupIDs)
|
accountID, c.ChangedGroupIDs, c.ChangedPeerIDs, c.LinkGroups, len(c.Policies), len(c.Routes), len(c.Routers), len(c.Resources), len(c.Networks), c.PostureCheckIDs, c.DistributionGroupIDs)
|
||||||
r.walk()
|
r.walk()
|
||||||
return r.expand()
|
return r.expand()
|
||||||
}
|
}
|
||||||
@@ -216,57 +242,84 @@ func Collect(ctx context.Context, s store.Store, accountID string, c Change) (gr
|
|||||||
}
|
}
|
||||||
r := newResolver(ctx, snap, accountID, c)
|
r := newResolver(ctx, snap, accountID, c)
|
||||||
r.walk()
|
r.walk()
|
||||||
return setToSlice(r.groupSet), setToSlice(r.peerSet)
|
return setToSlice(r.affectedGroups), setToSlice(r.affectedPeers)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newResolver(ctx context.Context, snap *Snapshot, accountID string, c Change) *resolver {
|
func newResolver(ctx context.Context, snap *Snapshot, accountID string, c Change) *resolver {
|
||||||
r := &resolver{
|
r := &resolver{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
snap: snap,
|
snap: snap,
|
||||||
accountID: accountID,
|
accountID: accountID,
|
||||||
change: c,
|
change: c,
|
||||||
changedGroupSet: toSet(c.ChangedGroupIDs),
|
linkGroups: toSet(c.ChangedGroupIDs),
|
||||||
changedPeerSet: toSet(c.ChangedPeerIDs),
|
outputGroups: toSet(c.ChangedGroupIDs),
|
||||||
groupSet: make(map[string]struct{}),
|
changedPeers: toSet(c.ChangedPeerIDs),
|
||||||
peerSet: make(map[string]struct{}),
|
affectedGroups: make(map[string]struct{}),
|
||||||
networkIDs: make(map[string]struct{}),
|
affectedPeers: make(map[string]struct{}),
|
||||||
}
|
}
|
||||||
|
// LinkGroups match policies/routes to find the opposite side but are NOT output:
|
||||||
|
// they go into linkGroups only, never outputGroups, so their members never fold in.
|
||||||
|
addAll(r.linkGroups, c.LinkGroups)
|
||||||
// Resolve each changed peer to its groups here so callers pass only ChangedPeerIDs.
|
// Resolve each changed peer to its groups here so callers pass only ChangedPeerIDs.
|
||||||
r.seedChangedGroupsFromPeers()
|
r.seedChangedGroupsFromPeers()
|
||||||
r.matchedPolicies = append(r.matchedPolicies, c.Policies...)
|
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
// seedChangedGroupsFromPeers adds each changed peer's groups to changedGroupSet so
|
// seedChangedGroupsFromPeers adds each changed peer's groups to linkGroups so
|
||||||
// the group-driven walkers fire for memberships, not just direct peer references.
|
// the group-driven walkers fire for memberships, not just direct peer references.
|
||||||
|
// These seeded groups are for MATCHING only — folding the changed entity's own
|
||||||
|
// side is gated on outputGroups (the caller-reported groups), so a seeded group
|
||||||
|
// never folds its whole membership; only the changed peer itself folds in.
|
||||||
func (r *resolver) seedChangedGroupsFromPeers() {
|
func (r *resolver) seedChangedGroupsFromPeers() {
|
||||||
if len(r.changedPeerSet) == 0 {
|
if len(r.changedPeers) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for groupID, members := range r.snap.groupPeers {
|
for groupID, members := range r.snap.groupPeers {
|
||||||
for pID := range r.changedPeerSet {
|
for pID := range r.changedPeers {
|
||||||
if _, ok := members[pID]; ok {
|
if _, ok := members[pID]; ok {
|
||||||
r.changedGroupSet[groupID] = struct{}{}
|
r.linkGroups[groupID] = struct{}{}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// policySide selects which side of a policy rule to walk.
|
||||||
|
type policySide int
|
||||||
|
|
||||||
|
const (
|
||||||
|
sideSource policySide = iota
|
||||||
|
sideDestination
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s policySide) opposite() policySide {
|
||||||
|
if s == sideSource {
|
||||||
|
return sideDestination
|
||||||
|
}
|
||||||
|
return sideSource
|
||||||
|
}
|
||||||
|
|
||||||
|
// walk resolves affected peers in two buckets, by how far each change propagates.
|
||||||
|
//
|
||||||
|
// BOTH-SIDES — the rule itself changed (an explicit policy edit, or a policy whose
|
||||||
|
// posture check changed). Source AND destination refresh, so each such policy is
|
||||||
|
// walked on both sides.
|
||||||
|
//
|
||||||
|
// OPPOSITE-SIDE — an endpoint moved but no rule changed. For each policy the change
|
||||||
|
// touches we fold only the side AWAY from the change:
|
||||||
|
// - a changed peer/group sits ON a policy side -> fold the opposite side;
|
||||||
|
// - a changed router/resource/network sits on a NETWORK -> fold the SOURCE side of
|
||||||
|
// the policies whose destination reaches it (and the routers it implies).
|
||||||
|
//
|
||||||
|
// Routes, nameserver groups, DNS and embedded-proxy services distribute to their own
|
||||||
|
// member peers, outside the policy graph, and are folded here too.
|
||||||
func (r *resolver) walk() {
|
func (r *resolver) walk() {
|
||||||
r.collectFromExplicitPolicies()
|
for _, policy := range r.bothSidesPolicies() {
|
||||||
r.collectFromExplicitRoutes(r.change.Routes)
|
r.foldPolicySide(policy, sideSource)
|
||||||
r.collectFromExplicitRouters(r.change.Routers)
|
r.foldPolicySide(policy, sideDestination)
|
||||||
r.collectFromExplicitResources(r.change.Resources)
|
}
|
||||||
r.collectFromExplicitNetworks(r.change.Networks)
|
|
||||||
r.collectFromPostureChecks(r.change.PostureCheckIDs)
|
|
||||||
|
|
||||||
// Distribution groups (nameserver/DNS) affect only their member peers: fold them
|
if len(r.linkGroups) > 0 || len(r.changedPeers) > 0 {
|
||||||
// straight into groupSet so expand() maps them to members, without the policy/
|
|
||||||
// route walk that changedGroupSet would trigger.
|
|
||||||
addAll(r.groupSet, r.change.DistributionGroupIDs)
|
|
||||||
|
|
||||||
if len(r.changedGroupSet) > 0 || len(r.changedPeerSet) > 0 {
|
|
||||||
r.collectFromPolicies()
|
r.collectFromPolicies()
|
||||||
r.collectFromRoutes()
|
r.collectFromRoutes()
|
||||||
r.collectFromNameServers()
|
r.collectFromNameServers()
|
||||||
@@ -275,7 +328,31 @@ func (r *resolver) walk() {
|
|||||||
r.collectFromProxyServices()
|
r.collectFromProxyServices()
|
||||||
}
|
}
|
||||||
|
|
||||||
r.collectResourceRouterBridge()
|
r.collectFromChangedRoutes(r.change.Routes)
|
||||||
|
r.collectFromChangedRouters(r.change.Routers)
|
||||||
|
r.collectFromChangedResources(r.change.Resources)
|
||||||
|
r.collectFromChangedNetworks(r.change.Networks)
|
||||||
|
|
||||||
|
// The explicitly changed peers always refresh their own maps. OnPeersUpdated only
|
||||||
|
// refreshes the resolver's output (it ignores the separately-passed changed peers),
|
||||||
|
// so the changed peer reaches its own new map only via here. An offline/deleted
|
||||||
|
// peer in the set is filtered downstream (filterConnectedAffectedPeers).
|
||||||
|
addAll(r.affectedPeers, setToSlice(r.changedPeers))
|
||||||
|
// OutputPeerIDs refresh themselves too, but unlike changedPeers their group
|
||||||
|
// memberships were not seeded into the walk (only the changed group was).
|
||||||
|
addAll(r.affectedPeers, r.change.OutputPeerIDs)
|
||||||
|
|
||||||
|
// Distribution groups (nameserver/DNS) affect only their member peers: fold them
|
||||||
|
// straight into affectedGroups so expand() maps them to members, without the
|
||||||
|
// policy/route walk that linkGroups would trigger.
|
||||||
|
addAll(r.affectedGroups, r.change.DistributionGroupIDs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// bothSidesPolicies are the policies whose rule changed: the explicitly edited ones
|
||||||
|
// plus those gated by a changed posture check. walk folds both their sides.
|
||||||
|
func (r *resolver) bothSidesPolicies() []*types.Policy {
|
||||||
|
policies := append([]*types.Policy(nil), r.change.Policies...)
|
||||||
|
return r.appendPoliciesForPostureChecks(policies, r.change.PostureCheckIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
type resolver struct {
|
type resolver struct {
|
||||||
@@ -284,27 +361,71 @@ type resolver struct {
|
|||||||
accountID string
|
accountID string
|
||||||
change Change
|
change Change
|
||||||
|
|
||||||
changedGroupSet map[string]struct{}
|
// Inputs — what changed. Set once at construction, read-only during the walk
|
||||||
changedPeerSet map[string]struct{}
|
// (except linkGroups, which collectFromExplicitResources also seeds).
|
||||||
|
//
|
||||||
|
// linkGroups is the MATCH set: caller-changed groups ∪ the groups of changed
|
||||||
|
// peers ∪ changed-resource groups. A rule/route/router matches the change when
|
||||||
|
// one of its groups is here — used only to find the opposite side to fold.
|
||||||
|
//
|
||||||
|
// outputGroups is the FOLD-WHOLE-GROUP set: ONLY Change.ChangedGroupIDs. When a
|
||||||
|
// matched group is here, its whole membership is affected. A peer-seeded group
|
||||||
|
// is in linkGroups but NOT outputGroups, so it folds only the changed peer
|
||||||
|
// (changedPeers), never its siblings.
|
||||||
|
linkGroups map[string]struct{}
|
||||||
|
outputGroups map[string]struct{}
|
||||||
|
changedPeers map[string]struct{}
|
||||||
|
|
||||||
groupSet map[string]struct{}
|
// Outputs — the answer. The only sets the walk accumulates into. affectedGroups
|
||||||
peerSet map[string]struct{}
|
// is expanded to its member peers in expand().
|
||||||
|
affectedGroups map[string]struct{}
|
||||||
matchedPolicies []*types.Policy
|
affectedPeers map[string]struct{}
|
||||||
networkIDs map[string]struct{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *resolver) policies() []*types.Policy { return r.snap.policies }
|
// policies returns the account's ENABLED policies from the snapshot. Disabled
|
||||||
|
// policies grant no access, so the walk skips them when scanning existing account
|
||||||
|
// data. Explicitly changed policies (Change.Policies, via bothSidesPolicies) are
|
||||||
|
// processed regardless of Enabled, so disabling one still refreshes its peers.
|
||||||
|
func (r *resolver) policies() []*types.Policy {
|
||||||
|
enabled := make([]*types.Policy, 0, len(r.snap.policies))
|
||||||
|
for _, policy := range r.snap.policies {
|
||||||
|
if policy != nil && policy.Enabled {
|
||||||
|
enabled = append(enabled, policy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return enabled
|
||||||
|
}
|
||||||
|
|
||||||
func (r *resolver) networkResources() []*resourceTypes.NetworkResource { return r.snap.resources }
|
// networkResources / networkRouters return the account's ENABLED resources/routers
|
||||||
|
// from the snapshot. Disabled objects route to nobody, so the walk skips them when
|
||||||
|
// it scans existing account data. The explicitly changed objects in the Change are
|
||||||
|
// processed regardless of Enabled (collectFromChanged*), so disabling one still
|
||||||
|
// refreshes the peers that lose access.
|
||||||
|
func (r *resolver) networkResources() []*resourceTypes.NetworkResource {
|
||||||
|
enabled := make([]*resourceTypes.NetworkResource, 0, len(r.snap.resources))
|
||||||
|
for _, resource := range r.snap.resources {
|
||||||
|
if resource.Enabled {
|
||||||
|
enabled = append(enabled, resource)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return enabled
|
||||||
|
}
|
||||||
|
|
||||||
func (r *resolver) networkRouters() []*routerTypes.NetworkRouter { return r.snap.routers }
|
func (r *resolver) networkRouters() []*routerTypes.NetworkRouter {
|
||||||
|
enabled := make([]*routerTypes.NetworkRouter, 0, len(r.snap.routers))
|
||||||
|
for _, router := range r.snap.routers {
|
||||||
|
if router.Enabled {
|
||||||
|
enabled = append(enabled, router)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return enabled
|
||||||
|
}
|
||||||
|
|
||||||
// peerIDsForGroups maps a group set to its member peer IDs via the preloaded index.
|
// peerIDsForGroups maps a group set to its member peer IDs via the preloaded index.
|
||||||
func (r *resolver) peerIDsForGroups(groupSet map[string]struct{}) []string {
|
func (r *resolver) peerIDsForGroups(groups map[string]struct{}) []string {
|
||||||
seen := make(map[string]struct{})
|
seen := make(map[string]struct{})
|
||||||
var ids []string
|
var ids []string
|
||||||
for gID := range groupSet {
|
for gID := range groups {
|
||||||
for pID := range r.snap.groupPeers[gID] {
|
for pID := range r.snap.groupPeers[gID] {
|
||||||
if _, ok := seen[pID]; ok {
|
if _, ok := seen[pID]; ok {
|
||||||
continue
|
continue
|
||||||
@@ -317,25 +438,25 @@ func (r *resolver) peerIDsForGroups(groupSet map[string]struct{}) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *resolver) expand() []string {
|
func (r *resolver) expand() []string {
|
||||||
peerIDs := r.peerIDsForGroups(r.groupSet)
|
peerIDs := r.peerIDsForGroups(r.affectedGroups)
|
||||||
|
|
||||||
log.WithContext(r.ctx).Tracef("affectedpeers expand: account=%s affectedGroups=%v -> %d group-member peers; direct peers=%v",
|
log.WithContext(r.ctx).Tracef("affectedpeers expand: account=%s affectedGroups=%v -> %d group-member peers; direct peers=%v",
|
||||||
r.accountID, setToSlice(r.groupSet), len(peerIDs), setToSlice(r.peerSet))
|
r.accountID, setToSlice(r.affectedGroups), len(peerIDs), setToSlice(r.affectedPeers))
|
||||||
|
|
||||||
seen := make(map[string]struct{}, len(peerIDs))
|
seen := make(map[string]struct{}, len(peerIDs))
|
||||||
for _, id := range peerIDs {
|
for _, id := range peerIDs {
|
||||||
seen[id] = struct{}{}
|
seen[id] = struct{}{}
|
||||||
}
|
}
|
||||||
for id := range r.peerSet {
|
for id := range r.affectedPeers {
|
||||||
if _, ok := seen[id]; !ok {
|
if _, ok := seen[id]; !ok {
|
||||||
peerIDs = append(peerIDs, id)
|
peerIDs = append(peerIDs, id)
|
||||||
seen[id] = struct{}{}
|
seen[id] = struct{}{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fold in removed peers only when their group is linked (in groupSet).
|
// Fold in removed peers only when their group is linked (in affectedGroups).
|
||||||
for groupID, removed := range r.change.RemovedPeersByGroup {
|
for groupID, removed := range r.change.RemovedPeersByGroup {
|
||||||
if _, linked := r.groupSet[groupID]; !linked {
|
if _, linked := r.affectedGroups[groupID]; !linked {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, id := range removed {
|
for _, id := range removed {
|
||||||
@@ -351,169 +472,349 @@ func (r *resolver) expand() []string {
|
|||||||
return peerIDs
|
return peerIDs
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *resolver) collectFromExplicitPolicies() {
|
// ruleSideGroups / ruleSideResource return the groups and the resource on the given
|
||||||
for _, policy := range r.matchedPolicies {
|
// side of a rule.
|
||||||
if policy == nil {
|
func ruleSideGroups(rule *types.PolicyRule, side policySide) []string {
|
||||||
continue
|
if side == sideDestination {
|
||||||
|
return rule.Destinations
|
||||||
|
}
|
||||||
|
return rule.Sources
|
||||||
|
}
|
||||||
|
|
||||||
|
func ruleSideResource(rule *types.PolicyRule, side policySide) types.Resource {
|
||||||
|
if side == sideDestination {
|
||||||
|
return rule.DestinationResource
|
||||||
|
}
|
||||||
|
return rule.SourceResource
|
||||||
|
}
|
||||||
|
|
||||||
|
// foldPolicySide folds one side of a policy down to affected peers: its groups
|
||||||
|
// (resolved to members in expand) and its direct peer. When the side is the
|
||||||
|
// DESTINATION and references a network resource (directly or via a destination
|
||||||
|
// group's resources), it also folds the routers that serve that resource's network
|
||||||
|
// — a destination resource is reached through its routers. A resource on the SOURCE
|
||||||
|
// side routes to nobody (GetPoliciesForNetworkResource matches destinations only),
|
||||||
|
// so the router hop is destination-only.
|
||||||
|
func (r *resolver) foldPolicySide(policy *types.Policy, side policySide) {
|
||||||
|
if policy == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, rule := range policy.Rules {
|
||||||
|
addAll(r.affectedGroups, ruleSideGroups(rule, side))
|
||||||
|
res := ruleSideResource(rule, side)
|
||||||
|
if res.Type == types.ResourceTypePeer && res.ID != "" {
|
||||||
|
r.affectedPeers[res.ID] = struct{}{}
|
||||||
}
|
}
|
||||||
log.WithContext(r.ctx).Tracef("collectFromExplicitPolicies: changed policy %s (%s) -> folding rule groups %v + direct peers",
|
}
|
||||||
policy.ID, policy.Name, policy.RuleGroups())
|
if side == sideDestination {
|
||||||
addAll(r.groupSet, policy.RuleGroups())
|
r.foldRoutersForResources(r.policyDestinationResourceIDs(policy))
|
||||||
collectPolicyDirectPeers(policy, r.peerSet)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *resolver) collectFromExplicitRoutes(routes []*route.Route) {
|
// appendPoliciesForPostureChecks appends every policy that references a changed
|
||||||
|
// posture check (a rule change, so walk both sides).
|
||||||
|
func (r *resolver) appendPoliciesForPostureChecks(policies []*types.Policy, postureCheckIDs []string) []*types.Policy {
|
||||||
|
if len(postureCheckIDs) == 0 {
|
||||||
|
return policies
|
||||||
|
}
|
||||||
|
ids := toSet(postureCheckIDs)
|
||||||
|
for _, policy := range r.policies() {
|
||||||
|
if !policyReferencesPostureChecks(policy, ids) || !policy.Enabled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log.WithContext(r.ctx).Tracef("appendPoliciesForPostureChecks: policy %s (%s) references changed posture checks %v -> both-sides policy",
|
||||||
|
policy.ID, policy.Name, postureCheckIDs)
|
||||||
|
policies = append(policies, policy)
|
||||||
|
}
|
||||||
|
return policies
|
||||||
|
}
|
||||||
|
|
||||||
|
// collectFromPolicies folds, for every policy whose rule a changed group or peer
|
||||||
|
// touches, only the OPPOSITE side (down to peers, incl. destination routers), plus
|
||||||
|
// the changed entity's own side: the changed group's whole membership when the
|
||||||
|
// group itself changed (outputGroups), or the changed peer alone when matched via a
|
||||||
|
// peer-seeded group (never its co-members).
|
||||||
|
func (r *resolver) collectFromPolicies() {
|
||||||
|
for _, policy := range r.policies() {
|
||||||
|
for _, rule := range policy.Rules {
|
||||||
|
if !rule.Enabled {
|
||||||
|
continue // a disabled rule grants no access
|
||||||
|
}
|
||||||
|
r.foldRuleSideIfChanged(policy, rule, sideSource)
|
||||||
|
r.foldRuleSideIfChanged(policy, rule, sideDestination)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// foldRuleSideIfChanged: when a changed group or direct peer sits on `side` of the
|
||||||
|
// rule, fold the opposite side fully (groups/peers + destination routers) and fold
|
||||||
|
// the changed entity's own side (the whole changed group, or the changed peer alone).
|
||||||
|
func (r *resolver) foldRuleSideIfChanged(policy *types.Policy, rule *types.PolicyRule, side policySide) {
|
||||||
|
nearGroups := ruleSideGroups(rule, side)
|
||||||
|
nearResource := ruleSideResource(rule, side)
|
||||||
|
|
||||||
|
matchedByGroup := anyInSet(nearGroups, r.linkGroups)
|
||||||
|
matchedByPeer := isDirectPeerInSet(nearResource, r.changedPeers)
|
||||||
|
if !matchedByGroup && !matchedByPeer {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Opposite side, fully down to peers (a destination opposite also folds routers).
|
||||||
|
r.foldPolicySideForRule(policy, rule, side.opposite())
|
||||||
|
|
||||||
|
// Own side: fold the whole changed group's members only when the group itself
|
||||||
|
// changed (outputGroups). A peer-seeded or link-only group is not folded here —
|
||||||
|
// its siblings never refresh. The changed peers themselves are folded once, after
|
||||||
|
// the walk (see walk()).
|
||||||
|
for _, gID := range nearGroups {
|
||||||
|
if _, ok := r.outputGroups[gID]; ok {
|
||||||
|
r.affectedGroups[gID] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// When the changed side IS a destination, the resources it targets are reached
|
||||||
|
// through their network's routers, so those routers refresh too (e.g. attaching a
|
||||||
|
// resource to a destination group, or a changed destination group/resource).
|
||||||
|
if side == sideDestination {
|
||||||
|
r.foldRoutersForResources(r.ruleDestinationResourceIDs(rule))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// foldPolicySideForRule folds one side of a single rule (groups + direct peer), and
|
||||||
|
// for a destination side the routers of that rule's destination resources.
|
||||||
|
func (r *resolver) foldPolicySideForRule(policy *types.Policy, rule *types.PolicyRule, side policySide) {
|
||||||
|
addAll(r.affectedGroups, ruleSideGroups(rule, side))
|
||||||
|
res := ruleSideResource(rule, side)
|
||||||
|
if res.Type == types.ResourceTypePeer && res.ID != "" {
|
||||||
|
r.affectedPeers[res.ID] = struct{}{}
|
||||||
|
}
|
||||||
|
if side == sideDestination {
|
||||||
|
r.foldRoutersForResources(r.ruleDestinationResourceIDs(rule))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// collectFromChangedRoutes folds an explicitly changed route's own groups and peer.
|
||||||
|
func (r *resolver) collectFromChangedRoutes(routes []*route.Route) {
|
||||||
for _, rt := range routes {
|
for _, rt := range routes {
|
||||||
if rt == nil {
|
if rt == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
log.WithContext(r.ctx).Tracef("collectFromExplicitRoutes: changed route %s -> folding groups=%v peerGroups=%v accessControlGroups=%v peer=%q",
|
log.WithContext(r.ctx).Tracef("collectFromChangedRoutes: changed route %s -> folding groups=%v peerGroups=%v accessControlGroups=%v peer=%q",
|
||||||
rt.ID, rt.Groups, rt.PeerGroups, rt.AccessControlGroups, rt.Peer)
|
rt.ID, rt.Groups, rt.PeerGroups, rt.AccessControlGroups, rt.Peer)
|
||||||
addAll(r.groupSet, rt.Groups, rt.PeerGroups, rt.AccessControlGroups)
|
addAll(r.affectedGroups, rt.Groups, rt.PeerGroups, rt.AccessControlGroups)
|
||||||
if rt.Peer != "" {
|
if rt.Peer != "" {
|
||||||
r.peerSet[rt.Peer] = struct{}{}
|
r.affectedPeers[rt.Peer] = struct{}{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// collectFromExplicitRouters folds changed routers' peers and marks their networks
|
// collectFromChangedRouters: a changed router refreshes its OWN backing peer/groups
|
||||||
// for the bridge. Passing the old router keeps a repointed router's previous peers
|
// (the changed entity) and the SOURCE side of every policy reaching a resource on
|
||||||
// affected without a post-commit read.
|
// its network (the router serves the whole network). Sibling routers on the network
|
||||||
func (r *resolver) collectFromExplicitRouters(routers []*routerTypes.NetworkRouter) {
|
// are independent and are NOT folded. Passing the old router state keeps a repointed
|
||||||
|
// router's previous backing affected without a post-commit read.
|
||||||
|
func (r *resolver) collectFromChangedRouters(routers []*routerTypes.NetworkRouter) {
|
||||||
for _, router := range routers {
|
for _, router := range routers {
|
||||||
if router == nil {
|
if router == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
log.WithContext(r.ctx).Tracef("collectFromExplicitRouters: changed router %s on network %s -> folding peerGroups=%v peer=%q and marking network for source bridge",
|
log.WithContext(r.ctx).Tracef("collectFromChangedRouters: changed router %s on network %s -> folding its own peerGroups=%v peer=%q + sources reaching network resources",
|
||||||
router.ID, router.NetworkID, router.PeerGroups, router.Peer)
|
router.ID, router.NetworkID, router.PeerGroups, router.Peer)
|
||||||
addAll(r.groupSet, router.PeerGroups)
|
addAll(r.affectedGroups, router.PeerGroups)
|
||||||
if router.Peer != "" {
|
if router.Peer != "" {
|
||||||
r.peerSet[router.Peer] = struct{}{}
|
r.affectedPeers[router.Peer] = struct{}{}
|
||||||
}
|
}
|
||||||
if router.NetworkID != "" {
|
if router.NetworkID != "" {
|
||||||
r.networkIDs[router.NetworkID] = struct{}{}
|
r.foldPolicySourcesForResources(r.networkResourceIDs(router.NetworkID))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// collectFromExplicitResources marks changed resources' networks for the bridge and
|
// collectFromChangedResources: a changed resource refreshes the SOURCE side of the
|
||||||
// treats their group IDs as changed, so policies targeting the resource via a
|
// policies targeting EXACTLY that resource — directly, or via one of the resource's
|
||||||
// now-detached (old) group still refresh.
|
// own groups (old∪new across the change, so a now-detached group's sources still
|
||||||
func (r *resolver) collectFromExplicitResources(resources []*resourceTypes.NetworkResource) {
|
// refresh) — plus the routers serving its network (the resource is reached through
|
||||||
|
// them). It does not touch sibling resources on the same network.
|
||||||
|
func (r *resolver) collectFromChangedResources(resources []*resourceTypes.NetworkResource) {
|
||||||
for _, resource := range resources {
|
for _, resource := range resources {
|
||||||
if resource == nil {
|
if resource == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
log.WithContext(r.ctx).Tracef("collectFromExplicitResources: changed resource %s on network %s -> marking network for bridge and treating groups %v as changed",
|
log.WithContext(r.ctx).Tracef("collectFromChangedResources: changed resource %s on network %s (groups %v) -> folding sources of policies targeting it + its network's routers",
|
||||||
resource.ID, resource.NetworkID, resource.GroupIDs)
|
resource.ID, resource.NetworkID, resource.GroupIDs)
|
||||||
addAll(r.changedGroupSet, resource.GroupIDs)
|
r.foldPolicySourcesForResource(resource.ID, resource.GroupIDs)
|
||||||
if resource.NetworkID != "" {
|
if resource.NetworkID != "" {
|
||||||
r.networkIDs[resource.NetworkID] = struct{}{}
|
r.foldRoutersOnNetworks(map[string]struct{}{resource.NetworkID: {}})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// collectFromExplicitNetworks marks changed networks for the bridge. A network has
|
// foldPolicySourcesForResource folds the source side of every policy whose
|
||||||
// no groups/peers of its own.
|
// destination is the given resource — referenced directly, or via any of the given
|
||||||
func (r *resolver) collectFromExplicitNetworks(networks []*networkTypes.Network) {
|
// groups (the resource's own old∪new groups, which captures a detached group).
|
||||||
for _, network := range networks {
|
func (r *resolver) foldPolicySourcesForResource(resourceID string, groupIDs []string) {
|
||||||
if network == nil {
|
groups := toSet(groupIDs)
|
||||||
|
for _, policy := range r.policies() {
|
||||||
|
if !policyTargetsResourceOrGroups(policy, resourceID, groups) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
log.WithContext(r.ctx).Tracef("collectFromExplicitNetworks: changed network %s -> marking for bridge", network.ID)
|
log.WithContext(r.ctx).Tracef("foldPolicySourcesForResource: policy %s (%s) targets changed resource %s -> folding its source groups/peers", policy.ID, policy.Name, resourceID)
|
||||||
if network.ID != "" {
|
collectPolicySources(policy, r.affectedGroups, r.affectedPeers)
|
||||||
r.networkIDs[network.ID] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *resolver) collectFromPostureChecks(postureCheckIDs []string) {
|
// policyTargetsResourceOrGroups reports whether a policy's destination is the given
|
||||||
if len(postureCheckIDs) == 0 {
|
// resource directly, or one of the given destination groups.
|
||||||
|
func policyTargetsResourceOrGroups(policy *types.Policy, resourceID string, groups map[string]struct{}) bool {
|
||||||
|
if policy == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, rule := range policy.Rules {
|
||||||
|
if !rule.Enabled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if rule.DestinationResource.Type != types.ResourceTypePeer && rule.DestinationResource.ID == resourceID && resourceID != "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if anyInSet(rule.Destinations, groups) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// collectFromChangedNetworks: a changed network refreshes the SOURCE side of the
|
||||||
|
// policies reaching any of its resources, plus its routers. A network has no
|
||||||
|
// groups/peers of its own.
|
||||||
|
func (r *resolver) collectFromChangedNetworks(networks []*networkTypes.Network) {
|
||||||
|
for _, network := range networks {
|
||||||
|
if network == nil || network.ID == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log.WithContext(r.ctx).Tracef("collectFromChangedNetworks: changed network %s -> folding sources reaching its resources + its routers", network.ID)
|
||||||
|
resourceIDs := r.networkResourceIDs(network.ID)
|
||||||
|
r.foldPolicySourcesForResources(resourceIDs)
|
||||||
|
r.foldRoutersOnNetworks(map[string]struct{}{network.ID: {}})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// foldPolicySourcesForResources folds the source groups/peers of every policy whose
|
||||||
|
// destination targets one of resourceIDs (directly or via a destination group).
|
||||||
|
func (r *resolver) foldPolicySourcesForResources(resourceIDs map[string]struct{}) {
|
||||||
|
if len(resourceIDs) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ids := toSet(postureCheckIDs)
|
|
||||||
for _, policy := range r.policies() {
|
for _, policy := range r.policies() {
|
||||||
if !policyReferencesPostureChecks(policy, ids) {
|
if r.policyTargetsResources(policy, resourceIDs) {
|
||||||
continue
|
log.WithContext(r.ctx).Tracef("foldPolicySourcesForResources: policy %s (%s) targets a changed resource -> folding its source groups/peers", policy.ID, policy.Name)
|
||||||
|
collectPolicySources(policy, r.affectedGroups, r.affectedPeers)
|
||||||
}
|
}
|
||||||
log.WithContext(r.ctx).Tracef("collectFromPostureChecks: policy %s (%s) references changed posture checks %v -> folding rule groups %v + direct peers",
|
|
||||||
policy.ID, policy.Name, postureCheckIDs, policy.RuleGroups())
|
|
||||||
addAll(r.groupSet, policy.RuleGroups())
|
|
||||||
collectPolicyDirectPeers(policy, r.peerSet)
|
|
||||||
r.matchedPolicies = append(r.matchedPolicies, policy)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *resolver) collectFromPolicies() {
|
|
||||||
for _, policy := range r.policies() {
|
|
||||||
matchedByGroup := policyReferencesGroups(policy, r.changedGroupSet)
|
|
||||||
matchedByPeer := len(r.changedPeerSet) > 0 && policyReferencesDirectPeers(policy, r.changedPeerSet)
|
|
||||||
if !matchedByGroup && !matchedByPeer {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
log.WithContext(r.ctx).Tracef("collectFromPolicies: policy %s (%s) matched (byGroup=%t byPeer=%t) -> folding rule groups %v + direct peers",
|
|
||||||
policy.ID, policy.Name, matchedByGroup, matchedByPeer, policy.RuleGroups())
|
|
||||||
addAll(r.groupSet, policy.RuleGroups())
|
|
||||||
collectPolicyDirectPeers(policy, r.peerSet)
|
|
||||||
r.matchedPolicies = append(r.matchedPolicies, policy)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// collectFromRoutes folds, per matched route, the OPPOSITE side(s) fully and the
|
||||||
|
// matched side's own groups only on a whole-group change (outputGroups). A route has
|
||||||
|
// three peer sides — routing (Peer/PeerGroups), consumer (Groups) and ACL
|
||||||
|
// (AccessControlGroups) — that each refresh the others; the changed side's own group
|
||||||
|
// folds its siblings only when the group itself changed, never on a one-peer move.
|
||||||
func (r *resolver) collectFromRoutes() {
|
func (r *resolver) collectFromRoutes() {
|
||||||
for _, rt := range r.snap.routes {
|
for _, rt := range r.snap.routes {
|
||||||
matchedByGroup := anyInSet(rt.Groups, r.changedGroupSet) || anyInSet(rt.PeerGroups, r.changedGroupSet) || anyInSet(rt.AccessControlGroups, r.changedGroupSet)
|
if !rt.Enabled {
|
||||||
matchedByPeer := rt.Peer != "" && len(r.changedPeerSet) > 0 && isInSet(rt.Peer, r.changedPeerSet)
|
continue // disabled routes route to nobody; skip existing account data
|
||||||
if !matchedByGroup && !matchedByPeer {
|
}
|
||||||
|
routing := anyInSet(rt.PeerGroups, r.linkGroups) || (rt.Peer != "" && isInSet(rt.Peer, r.changedPeers))
|
||||||
|
consumer := anyInSet(rt.Groups, r.linkGroups)
|
||||||
|
acl := anyInSet(rt.AccessControlGroups, r.linkGroups)
|
||||||
|
if !routing && !consumer && !acl {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
log.WithContext(r.ctx).Tracef("collectFromRoutes: route %s matched (byGroup=%t byPeer=%t) -> folding groups=%v peerGroups=%v accessControlGroups=%v peer=%q",
|
log.WithContext(r.ctx).Tracef("collectFromRoutes: route %s matched (routing=%t consumer=%t acl=%t) -> folding opposite sides; own side gated on outputGroups",
|
||||||
rt.ID, matchedByGroup, matchedByPeer, rt.Groups, rt.PeerGroups, rt.AccessControlGroups, rt.Peer)
|
rt.ID, routing, consumer, acl)
|
||||||
addAll(r.groupSet, rt.Groups, rt.PeerGroups, rt.AccessControlGroups)
|
r.foldRouteSide(rt.PeerGroups, routing)
|
||||||
if rt.Peer != "" {
|
r.foldRouteSide(rt.Groups, consumer)
|
||||||
r.peerSet[rt.Peer] = struct{}{}
|
r.foldRouteSide(rt.AccessControlGroups, acl)
|
||||||
|
// The single routing Peer folds when the routing side is the OPPOSITE of the
|
||||||
|
// match (consumer/acl need it), or when that very peer is the change.
|
||||||
|
if rt.Peer != "" && (consumer || acl || isInSet(rt.Peer, r.changedPeers)) {
|
||||||
|
r.affectedPeers[rt.Peer] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// foldRouteSide folds a route side: when this side is the one that matched, fold its
|
||||||
|
// groups only on a whole-group change (outputGroups) so siblings of a single moved
|
||||||
|
// peer stay put; otherwise it is an opposite side and folds fully.
|
||||||
|
func (r *resolver) foldRouteSide(groups []string, matchedHere bool) {
|
||||||
|
if matchedHere {
|
||||||
|
r.foldOutputGroups(groups)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
addAll(r.affectedGroups, groups)
|
||||||
|
}
|
||||||
|
|
||||||
|
// foldOutputGroups folds only the groups that the caller reported as wholly changed
|
||||||
|
// (outputGroups). Used for a matched object's OWN side, where a peer-seeded or
|
||||||
|
// link-only group must not pull in its siblings.
|
||||||
|
func (r *resolver) foldOutputGroups(groups ...[]string) {
|
||||||
|
for _, gs := range groups {
|
||||||
|
for _, gID := range gs {
|
||||||
|
if _, ok := r.outputGroups[gID]; ok {
|
||||||
|
r.affectedGroups[gID] = struct{}{}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *resolver) collectFromNameServers() {
|
func (r *resolver) collectFromNameServers() {
|
||||||
if len(r.changedGroupSet) == 0 {
|
if len(r.linkGroups) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, ns := range r.snap.nsGroups {
|
for _, ns := range r.snap.nsGroups {
|
||||||
if anyInSet(ns.Groups, r.changedGroupSet) {
|
if anyInSet(ns.Groups, r.linkGroups) {
|
||||||
log.WithContext(r.ctx).Tracef("collectFromNameServers: nameserver group %s references a changed group -> folding its groups %v", ns.ID, ns.Groups)
|
// A nameserver group has no opposite side: a peer's DNS config depends only
|
||||||
addAll(r.groupSet, ns.Groups)
|
// on its own membership, so a one-peer move refreshes that peer alone (folded
|
||||||
|
// elsewhere). Fold the referenced groups only on a whole-group change.
|
||||||
|
log.WithContext(r.ctx).Tracef("collectFromNameServers: nameserver group %s references a linked group -> folding its groups %v (outputGroups only)", ns.ID, ns.Groups)
|
||||||
|
r.foldOutputGroups(ns.Groups)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *resolver) collectFromDNSSettings() {
|
func (r *resolver) collectFromDNSSettings() {
|
||||||
if len(r.changedGroupSet) == 0 || r.snap.dnsSettings == nil {
|
if len(r.linkGroups) == 0 || r.snap.dnsSettings == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, gID := range r.snap.dnsSettings.DisabledManagementGroups {
|
for _, gID := range r.snap.dnsSettings.DisabledManagementGroups {
|
||||||
if _, ok := r.changedGroupSet[gID]; ok {
|
if _, ok := r.linkGroups[gID]; ok {
|
||||||
log.WithContext(r.ctx).Tracef("collectFromDNSSettings: changed group %s is in DisabledManagementGroups -> folding it", gID)
|
log.WithContext(r.ctx).Tracef("collectFromDNSSettings: changed group %s is in DisabledManagementGroups -> folding it", gID)
|
||||||
r.groupSet[gID] = struct{}{}
|
r.affectedGroups[gID] = struct{}{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// collectFromNetworkRouters handles a changed group/peer that BACKS a router (the
|
||||||
|
// routing peer set moved): the router's own peers refresh and so do the sources of
|
||||||
|
// the policies reaching its network's resources. Sibling routers on the network are
|
||||||
|
// independent and are not folded.
|
||||||
func (r *resolver) collectFromNetworkRouters() {
|
func (r *resolver) collectFromNetworkRouters() {
|
||||||
for _, router := range r.networkRouters() {
|
for _, router := range r.networkRouters() {
|
||||||
matchedByGroup := anyInSet(router.PeerGroups, r.changedGroupSet)
|
matchedByGroup := anyInSet(router.PeerGroups, r.linkGroups)
|
||||||
matchedByPeer := router.Peer != "" && len(r.changedPeerSet) > 0 && isInSet(router.Peer, r.changedPeerSet)
|
matchedByPeer := router.Peer != "" && len(r.changedPeers) > 0 && isInSet(router.Peer, r.changedPeers)
|
||||||
if !matchedByGroup && !matchedByPeer {
|
if !matchedByGroup && !matchedByPeer {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
log.WithContext(r.ctx).Tracef("collectFromNetworkRouters: router %s on network %s matched (byGroup=%t byPeer=%t) -> folding peerGroups=%v peer=%q and marking network for source bridge",
|
log.WithContext(r.ctx).Tracef("collectFromNetworkRouters: router %s on network %s matched (byGroup=%t byPeer=%t) -> folding its peerGroups=%v peer=%q (own groups on outputGroups) + sources reaching network resources",
|
||||||
router.ID, router.NetworkID, matchedByGroup, matchedByPeer, router.PeerGroups, router.Peer)
|
router.ID, router.NetworkID, matchedByGroup, matchedByPeer, router.PeerGroups, router.Peer)
|
||||||
addAll(r.groupSet, router.PeerGroups)
|
// The backing PeerGroups are the matched (own) side: fold them only on a
|
||||||
|
// whole-group change so a one-peer move does not wake sibling backing peers. The
|
||||||
|
// opposite side (policy sources reaching the network) is folded below.
|
||||||
|
r.foldOutputGroups(router.PeerGroups)
|
||||||
if router.Peer != "" {
|
if router.Peer != "" {
|
||||||
r.peerSet[router.Peer] = struct{}{}
|
r.affectedPeers[router.Peer] = struct{}{}
|
||||||
|
}
|
||||||
|
if router.NetworkID != "" {
|
||||||
|
r.foldPolicySourcesForResources(r.networkResourceIDs(router.NetworkID))
|
||||||
}
|
}
|
||||||
r.networkIDs[router.NetworkID] = struct{}{}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -526,42 +827,48 @@ func (r *resolver) collectFromProxyServices() {
|
|||||||
expanded := r.expandChangedPeersWithGroups()
|
expanded := r.expandChangedPeersWithGroups()
|
||||||
|
|
||||||
for _, svc := range services {
|
for _, svc := range services {
|
||||||
if svc == nil {
|
if svc == nil || !svc.Enabled {
|
||||||
continue
|
continue // a disabled service proxies nothing; skip existing account data
|
||||||
}
|
}
|
||||||
proxyPeers := proxyByCluster[svc.ProxyCluster]
|
proxyPeers := proxyByCluster[svc.ProxyCluster]
|
||||||
if len(proxyPeers) == 0 {
|
if len(proxyPeers) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
matchedByPeer := serviceMatchesChangedPeers(svc, proxyPeers, expanded)
|
matchedByPeer := serviceMatchesChangedPeers(svc, proxyPeers, expanded)
|
||||||
matchedByAccessGroup := anyInSet(svc.AccessGroups, r.changedGroupSet)
|
matchedByAccessGroup := anyInSet(svc.AccessGroups, r.linkGroups)
|
||||||
if !matchedByPeer && !matchedByAccessGroup {
|
if !matchedByPeer && !matchedByAccessGroup {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
log.WithContext(r.ctx).Tracef("collectFromProxyServices: service %s (cluster=%s) matched (byProxyOrTargetPeer=%t byAccessGroup=%t) -> folding %d proxy peers, peer targets and access groups %v",
|
log.WithContext(r.ctx).Tracef("collectFromProxyServices: service %s (cluster=%s) matched (byProxyOrTargetPeer=%t byAccessGroup=%t) -> folding %d proxy peers, peer targets; access groups %v on outputGroups only",
|
||||||
svc.ID, svc.ProxyCluster, matchedByPeer, matchedByAccessGroup, len(proxyPeers), svc.AccessGroups)
|
svc.ID, svc.ProxyCluster, matchedByPeer, matchedByAccessGroup, len(proxyPeers), svc.AccessGroups)
|
||||||
for _, pid := range proxyPeers {
|
for _, pid := range proxyPeers {
|
||||||
r.peerSet[pid] = struct{}{}
|
r.affectedPeers[pid] = struct{}{}
|
||||||
}
|
}
|
||||||
for _, target := range svc.Targets {
|
for _, target := range svc.Targets {
|
||||||
|
if !target.Enabled {
|
||||||
|
continue // a disabled target forwards nothing
|
||||||
|
}
|
||||||
if target.TargetType == rpservice.TargetTypePeer && target.TargetId != "" {
|
if target.TargetType == rpservice.TargetTypePeer && target.TargetId != "" {
|
||||||
r.peerSet[target.TargetId] = struct{}{}
|
r.affectedPeers[target.TargetId] = struct{}{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
addAll(r.groupSet, svc.AccessGroups)
|
// AccessGroups are the matched (own) side with no opposite to fold: a member's
|
||||||
|
// proxy access is self-contained, so a one-peer move refreshes that peer alone.
|
||||||
|
// Fold the groups only on a whole-group change.
|
||||||
|
r.foldOutputGroups(svc.AccessGroups)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *resolver) expandChangedPeersWithGroups() map[string]struct{} {
|
func (r *resolver) expandChangedPeersWithGroups() map[string]struct{} {
|
||||||
if len(r.changedGroupSet) == 0 {
|
if len(r.linkGroups) == 0 {
|
||||||
return r.changedPeerSet
|
return r.changedPeers
|
||||||
}
|
}
|
||||||
ids := r.peerIDsForGroups(r.changedGroupSet)
|
ids := r.peerIDsForGroups(r.linkGroups)
|
||||||
if len(ids) == 0 {
|
if len(ids) == 0 {
|
||||||
return r.changedPeerSet
|
return r.changedPeers
|
||||||
}
|
}
|
||||||
merged := make(map[string]struct{}, len(r.changedPeerSet)+len(ids))
|
merged := make(map[string]struct{}, len(r.changedPeers)+len(ids))
|
||||||
for id := range r.changedPeerSet {
|
for id := range r.changedPeers {
|
||||||
merged[id] = struct{}{}
|
merged[id] = struct{}{}
|
||||||
}
|
}
|
||||||
for _, id := range ids {
|
for _, id := range ids {
|
||||||
@@ -570,54 +877,36 @@ func (r *resolver) expandChangedPeersWithGroups() map[string]struct{} {
|
|||||||
return merged
|
return merged
|
||||||
}
|
}
|
||||||
|
|
||||||
// collectResourceRouterBridge crosses between source peers and routing peers, which
|
// foldRoutersForResources folds the routers serving the networks of the given
|
||||||
// are reachable only via resource -> network -> router, not through the policy's own
|
// resources (a destination resource is reached through its network's routers). It is
|
||||||
// groups: source -> router (targeted resources' networks), then router -> source.
|
// the resource -> network -> router hop used by foldPolicySide for a destination.
|
||||||
func (r *resolver) collectResourceRouterBridge() {
|
func (r *resolver) foldRoutersForResources(resourceIDs map[string]struct{}) {
|
||||||
r.bridgeSourceToRouters()
|
|
||||||
r.bridgeRoutersToSources()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *resolver) bridgeSourceToRouters() {
|
|
||||||
resourceIDs := r.policyDestinationResourceIDs(r.matchedPolicies...)
|
|
||||||
if len(resourceIDs) == 0 {
|
if len(resourceIDs) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
r.foldRoutersOnNetworks(r.resourceNetworkIDs(resourceIDs))
|
||||||
networkIDs := r.resourceNetworkIDs(resourceIDs)
|
|
||||||
log.WithContext(r.ctx).Tracef("bridgeSourceToRouters: targeted resources %v -> networks %v (their routers become affected via the router->source pass)",
|
|
||||||
setToSlice(resourceIDs), setToSlice(networkIDs))
|
|
||||||
for id := range networkIDs {
|
|
||||||
r.networkIDs[id] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *resolver) bridgeRoutersToSources() {
|
// ruleDestinationResourceIDs returns the destination resource IDs of a single rule:
|
||||||
if len(r.networkIDs) == 0 {
|
// the direct DestinationResource plus the resources of its destination groups.
|
||||||
return
|
func (r *resolver) ruleDestinationResourceIDs(rule *types.PolicyRule) map[string]struct{} {
|
||||||
|
resourceIDs := make(map[string]struct{})
|
||||||
|
if rule.DestinationResource.Type != types.ResourceTypePeer && rule.DestinationResource.ID != "" {
|
||||||
|
resourceIDs[rule.DestinationResource.ID] = struct{}{}
|
||||||
}
|
}
|
||||||
|
r.addGroupResourceIDs(toSet(rule.Destinations), resourceIDs)
|
||||||
|
return resourceIDs
|
||||||
|
}
|
||||||
|
|
||||||
log.WithContext(r.ctx).Tracef("bridgeRoutersToSources: affected networks %v -> folding their routing peers and the source peers of policies targeting their resources",
|
// networkResourceIDs returns the IDs of all resources on the given network.
|
||||||
setToSlice(r.networkIDs))
|
func (r *resolver) networkResourceIDs(networkID string) map[string]struct{} {
|
||||||
|
|
||||||
r.foldRoutersOnNetworks(r.networkIDs)
|
|
||||||
|
|
||||||
resourceIDs := make(map[string]struct{})
|
resourceIDs := make(map[string]struct{})
|
||||||
for _, resource := range r.networkResources() {
|
for _, resource := range r.networkResources() {
|
||||||
if _, ok := r.networkIDs[resource.NetworkID]; ok {
|
if resource.NetworkID == networkID {
|
||||||
resourceIDs[resource.ID] = struct{}{}
|
resourceIDs[resource.ID] = struct{}{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(resourceIDs) == 0 {
|
return resourceIDs
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, policy := range r.policies() {
|
|
||||||
if r.policyTargetsResources(policy, resourceIDs) {
|
|
||||||
log.WithContext(r.ctx).Tracef("bridgeRoutersToSources: policy %s (%s) targets an affected-network resource -> folding its source groups/peers", policy.ID, policy.Name)
|
|
||||||
collectPolicySources(policy, r.groupSet, r.peerSet)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *resolver) foldRoutersOnNetworks(networkIDs map[string]struct{}) {
|
func (r *resolver) foldRoutersOnNetworks(networkIDs map[string]struct{}) {
|
||||||
@@ -627,9 +916,9 @@ func (r *resolver) foldRoutersOnNetworks(networkIDs map[string]struct{}) {
|
|||||||
}
|
}
|
||||||
log.WithContext(r.ctx).Tracef("bridgeRoutersToSources: router %s serves affected network %s -> folding peerGroups=%v peer=%q",
|
log.WithContext(r.ctx).Tracef("bridgeRoutersToSources: router %s serves affected network %s -> folding peerGroups=%v peer=%q",
|
||||||
router.ID, router.NetworkID, router.PeerGroups, router.Peer)
|
router.ID, router.NetworkID, router.PeerGroups, router.Peer)
|
||||||
addAll(r.groupSet, router.PeerGroups)
|
addAll(r.affectedGroups, router.PeerGroups)
|
||||||
if router.Peer != "" {
|
if router.Peer != "" {
|
||||||
r.peerSet[router.Peer] = struct{}{}
|
r.affectedPeers[router.Peer] = struct{}{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -650,6 +939,9 @@ func (r *resolver) policyTargetsResources(policy *types.Policy, resourceIDs map[
|
|||||||
}
|
}
|
||||||
destGroupSet := make(map[string]struct{})
|
destGroupSet := make(map[string]struct{})
|
||||||
for _, rule := range policy.Rules {
|
for _, rule := range policy.Rules {
|
||||||
|
if !rule.Enabled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if rule.DestinationResource.Type != types.ResourceTypePeer && isInSet(rule.DestinationResource.ID, resourceIDs) {
|
if rule.DestinationResource.Type != types.ResourceTypePeer && isInSet(rule.DestinationResource.ID, resourceIDs) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -714,44 +1006,20 @@ func (r *resolver) addGroupResourceIDs(groupIDs map[string]struct{}, resourceIDs
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func collectPolicyDirectPeers(policy *types.Policy, peerSet map[string]struct{}) {
|
// collectPolicySources folds the source groups/peers of a snapshot policy's enabled
|
||||||
|
// rules (a disabled rule grants no access).
|
||||||
|
func collectPolicySources(policy *types.Policy, groups, peers map[string]struct{}) {
|
||||||
for _, rule := range policy.Rules {
|
for _, rule := range policy.Rules {
|
||||||
|
if !rule.Enabled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
addAll(groups, rule.Sources)
|
||||||
if rule.SourceResource.Type == types.ResourceTypePeer && rule.SourceResource.ID != "" {
|
if rule.SourceResource.Type == types.ResourceTypePeer && rule.SourceResource.ID != "" {
|
||||||
peerSet[rule.SourceResource.ID] = struct{}{}
|
peers[rule.SourceResource.ID] = struct{}{}
|
||||||
}
|
|
||||||
if rule.DestinationResource.Type == types.ResourceTypePeer && rule.DestinationResource.ID != "" {
|
|
||||||
peerSet[rule.DestinationResource.ID] = struct{}{}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func collectPolicySources(policy *types.Policy, groupSet, peerSet map[string]struct{}) {
|
|
||||||
for _, rule := range policy.Rules {
|
|
||||||
addAll(groupSet, rule.Sources)
|
|
||||||
if rule.SourceResource.Type == types.ResourceTypePeer && rule.SourceResource.ID != "" {
|
|
||||||
peerSet[rule.SourceResource.ID] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func policyReferencesGroups(policy *types.Policy, groupSet map[string]struct{}) bool {
|
|
||||||
for _, rule := range policy.Rules {
|
|
||||||
if anyInSet(rule.Sources, groupSet) || anyInSet(rule.Destinations, groupSet) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func policyReferencesDirectPeers(policy *types.Policy, changedSet map[string]struct{}) bool {
|
|
||||||
for _, rule := range policy.Rules {
|
|
||||||
if isDirectPeerInSet(rule.SourceResource, changedSet) || isDirectPeerInSet(rule.DestinationResource, changedSet) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func policyReferencesPostureChecks(policy *types.Policy, ids map[string]struct{}) bool {
|
func policyReferencesPostureChecks(policy *types.Policy, ids map[string]struct{}) bool {
|
||||||
for _, id := range policy.SourcePostureChecks {
|
for _, id := range policy.SourcePostureChecks {
|
||||||
if _, ok := ids[id]; ok {
|
if _, ok := ids[id]; ok {
|
||||||
@@ -776,7 +1044,7 @@ func serviceMatchesChangedPeers(svc *rpservice.Service, proxyPeers []string, cha
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, target := range svc.Targets {
|
for _, target := range svc.Targets {
|
||||||
if target.TargetType != rpservice.TargetTypePeer || target.TargetId == "" {
|
if !target.Enabled || target.TargetType != rpservice.TargetTypePeer || target.TargetId == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if _, ok := changedPeers[target.TargetId]; ok {
|
if _, ok := changedPeers[target.TargetId]; ok {
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import (
|
|||||||
"github.com/netbirdio/netbird/management/server/types"
|
"github.com/netbirdio/netbird/management/server/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// policyGroupsAndPeers mirrors the explicit-policy extraction (RuleGroups +
|
// policyGroupsAndPeers mirrors the both-sides extraction (RuleGroups + direct peers)
|
||||||
// direct peers) the resolver folds in, for asserting the pure logic.
|
// the resolver folds in for a changed policy, for asserting the pure logic.
|
||||||
func policyGroupsAndPeers(policies ...*types.Policy) (groups []string, peers []string) {
|
func policyGroupsAndPeers(policies ...*types.Policy) (groups []string, peers []string) {
|
||||||
peerSet := map[string]struct{}{}
|
peerSet := map[string]struct{}{}
|
||||||
for _, p := range policies {
|
for _, p := range policies {
|
||||||
@@ -19,7 +19,14 @@ func policyGroupsAndPeers(policies ...*types.Policy) (groups []string, peers []s
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
groups = append(groups, p.RuleGroups()...)
|
groups = append(groups, p.RuleGroups()...)
|
||||||
collectPolicyDirectPeers(p, peerSet)
|
for _, rule := range p.Rules {
|
||||||
|
if rule.SourceResource.Type == types.ResourceTypePeer && rule.SourceResource.ID != "" {
|
||||||
|
peerSet[rule.SourceResource.ID] = struct{}{}
|
||||||
|
}
|
||||||
|
if rule.DestinationResource.Type == types.ResourceTypePeer && rule.DestinationResource.ID != "" {
|
||||||
|
peerSet[rule.DestinationResource.ID] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for id := range peerSet {
|
for id := range peerSet {
|
||||||
peers = append(peers, id)
|
peers = append(peers, id)
|
||||||
@@ -80,26 +87,6 @@ func TestChangeIsEmpty(t *testing.T) {
|
|||||||
assert.False(t, Change{PostureCheckIDs: []string{"pc"}}.isEmpty())
|
assert.False(t, Change{PostureCheckIDs: []string{"pc"}}.isEmpty())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPolicyReferencesGroups(t *testing.T) {
|
|
||||||
policy := &types.Policy{Rules: []*types.PolicyRule{{Sources: []string{"g1", "g2"}, Destinations: []string{"g3"}}}}
|
|
||||||
|
|
||||||
assert.True(t, policyReferencesGroups(policy, map[string]struct{}{"g1": {}}))
|
|
||||||
assert.True(t, policyReferencesGroups(policy, map[string]struct{}{"g3": {}}))
|
|
||||||
assert.False(t, policyReferencesGroups(policy, map[string]struct{}{"g4": {}}))
|
|
||||||
assert.False(t, policyReferencesGroups(policy, map[string]struct{}{}))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPolicyReferencesDirectPeers(t *testing.T) {
|
|
||||||
policy := &types.Policy{Rules: []*types.PolicyRule{{
|
|
||||||
SourceResource: types.Resource{Type: types.ResourceTypePeer, ID: "p1"},
|
|
||||||
DestinationResource: types.Resource{Type: types.ResourceTypeHost, ID: "r1"},
|
|
||||||
}}}
|
|
||||||
|
|
||||||
assert.True(t, policyReferencesDirectPeers(policy, map[string]struct{}{"p1": {}}))
|
|
||||||
assert.False(t, policyReferencesDirectPeers(policy, map[string]struct{}{"r1": {}}))
|
|
||||||
assert.False(t, policyReferencesDirectPeers(policy, map[string]struct{}{"p2": {}}))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPolicyReferencesPostureChecks(t *testing.T) {
|
func TestPolicyReferencesPostureChecks(t *testing.T) {
|
||||||
policy := &types.Policy{SourcePostureChecks: []string{"pc1", "pc2"}}
|
policy := &types.Policy{SourcePostureChecks: []string{"pc1", "pc2"}}
|
||||||
|
|
||||||
@@ -107,24 +94,9 @@ func TestPolicyReferencesPostureChecks(t *testing.T) {
|
|||||||
assert.False(t, policyReferencesPostureChecks(policy, map[string]struct{}{"pc3": {}}))
|
assert.False(t, policyReferencesPostureChecks(policy, map[string]struct{}{"pc3": {}}))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCollectPolicyDirectPeers(t *testing.T) {
|
|
||||||
policy := &types.Policy{Rules: []*types.PolicyRule{{
|
|
||||||
SourceResource: types.Resource{Type: types.ResourceTypePeer, ID: "p1"},
|
|
||||||
DestinationResource: types.Resource{Type: types.ResourceTypePeer, ID: "p2"},
|
|
||||||
}, {
|
|
||||||
DestinationResource: types.Resource{Type: types.ResourceTypeHost, ID: "r1"},
|
|
||||||
}}}
|
|
||||||
|
|
||||||
peerSet := map[string]struct{}{}
|
|
||||||
collectPolicyDirectPeers(policy, peerSet)
|
|
||||||
|
|
||||||
assert.Contains(t, peerSet, "p1")
|
|
||||||
assert.Contains(t, peerSet, "p2")
|
|
||||||
assert.NotContains(t, peerSet, "r1")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCollectPolicySources(t *testing.T) {
|
func TestCollectPolicySources(t *testing.T) {
|
||||||
policy := &types.Policy{Rules: []*types.PolicyRule{{
|
policy := &types.Policy{Rules: []*types.PolicyRule{{
|
||||||
|
Enabled: true,
|
||||||
Sources: []string{"g1"},
|
Sources: []string{"g1"},
|
||||||
SourceResource: types.Resource{Type: types.ResourceTypePeer, ID: "p1"},
|
SourceResource: types.Resource{Type: types.ResourceTypePeer, ID: "p1"},
|
||||||
Destinations: []string{"g2"},
|
Destinations: []string{"g2"},
|
||||||
|
|||||||
@@ -520,7 +520,12 @@ func collectDeletableGroups(ctx context.Context, transaction store.Store, accoun
|
|||||||
// GroupAddPeer appends peer to the group
|
// GroupAddPeer appends peer to the group
|
||||||
func (am *DefaultAccountManager) GroupAddPeer(ctx context.Context, accountID, groupID, peerID string) error {
|
func (am *DefaultAccountManager) GroupAddPeer(ctx context.Context, accountID, groupID, peerID string) error {
|
||||||
var snap *affectedpeers.Snapshot
|
var snap *affectedpeers.Snapshot
|
||||||
change := affectedpeers.Change{ChangedGroupIDs: []string{groupID}}
|
// A membership change affects only the peer itself and the opposite side of THIS
|
||||||
|
// group's policies — not the group's other members, and not the peer's other
|
||||||
|
// groups. LinkGroups walks only this group (matched, not expanded); OutputPeerIDs
|
||||||
|
// refreshes the peer without seeding its other group memberships. For an
|
||||||
|
// intra-group policy the opposite side is the group, so its members still refresh.
|
||||||
|
change := affectedpeers.Change{OutputPeerIDs: []string{peerID}, LinkGroups: []string{groupID}}
|
||||||
|
|
||||||
err := am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error {
|
err := am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error {
|
||||||
if err := transaction.AddPeerToGroup(ctx, accountID, peerID, groupID); err != nil {
|
if err := transaction.AddPeerToGroup(ctx, accountID, peerID, groupID); err != nil {
|
||||||
@@ -586,10 +591,11 @@ func (am *DefaultAccountManager) GroupAddResource(ctx context.Context, accountID
|
|||||||
// GroupDeletePeer removes peer from the group
|
// GroupDeletePeer removes peer from the group
|
||||||
func (am *DefaultAccountManager) GroupDeletePeer(ctx context.Context, accountID, groupID, peerID string) error {
|
func (am *DefaultAccountManager) GroupDeletePeer(ctx context.Context, accountID, groupID, peerID string) error {
|
||||||
var snap *affectedpeers.Snapshot
|
var snap *affectedpeers.Snapshot
|
||||||
change := affectedpeers.Change{
|
// Same as GroupAddPeer: the removed peer and the opposite side of THIS group's
|
||||||
ChangedGroupIDs: []string{groupID},
|
// policies refresh, not the group's other members or the peer's other groups. The
|
||||||
RemovedPeersByGroup: map[string][]string{groupID: {peerID}},
|
// peer is no longer in the group's index, but LinkGroups still drives the
|
||||||
}
|
// opposite-side walk, and OutputPeerIDs refreshes the removed peer itself.
|
||||||
|
change := affectedpeers.Change{OutputPeerIDs: []string{peerID}, LinkGroups: []string{groupID}}
|
||||||
|
|
||||||
err := am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error {
|
err := am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error {
|
||||||
if err := transaction.RemovePeerFromGroup(ctx, peerID, groupID); err != nil {
|
if err := transaction.RemovePeerFromGroup(ctx, peerID, groupID); err != nil {
|
||||||
@@ -600,8 +606,6 @@ func (am *DefaultAccountManager) GroupDeletePeer(ctx context.Context, accountID,
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// The removed peer is carried in change.RemovedPeersByGroup and folded in
|
|
||||||
// only when the group is linked, so loading post-removal is correct.
|
|
||||||
var err error
|
var err error
|
||||||
if snap, err = affectedpeers.Load(ctx, transaction, accountID, change); err != nil {
|
if snap, err = affectedpeers.Load(ctx, transaction, accountID, change); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -220,7 +220,7 @@ func (h *handler) getAllUsers(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
includeServiceUser, err := strconv.ParseBool(serviceUser)
|
includeServiceUser, err := strconv.ParseBool(serviceUser)
|
||||||
log.WithContext(r.Context()).Debugf("Should include service user: %v", includeServiceUser)
|
log.WithContext(r.Context()).Tracef("Should include service user: %v", includeServiceUser)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "invalid service_user query parameter"), w)
|
util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "invalid service_user query parameter"), w)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -209,14 +209,14 @@ func (am *DefaultAccountManager) resolvePeerLocation(ctx context.Context, peer *
|
|||||||
if am.geo == nil || realIP == nil {
|
if am.geo == nil || realIP == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if peer.Location.ConnectionIP != nil && peer.Location.ConnectionIP.Equal(realIP) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
location, err := am.geo.Lookup(realIP)
|
location, err := am.geo.Lookup(realIP)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithContext(ctx).Warnf("failed to get location for peer %s realip: [%s]: %v", peer.ID, realIP.String(), err)
|
log.WithContext(ctx).Warnf("failed to get location for peer %s realip: [%s]: %v", peer.ID, realIP.String(), err)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
if peer.Location.ConnectionIP != nil && peer.Location.ConnectionIP.Equal(realIP) && peer.Location.GeoNameID == location.City.GeonameID {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return &nbpeer.Location{
|
return &nbpeer.Location{
|
||||||
ConnectionIP: realIP,
|
ConnectionIP: realIP,
|
||||||
CountryCode: location.Country.ISOCode,
|
CountryCode: location.Country.ISOCode,
|
||||||
@@ -730,7 +730,7 @@ func (am *DefaultAccountManager) handleSetupKeyAddedPeer(ctx context.Context, en
|
|||||||
func (am *DefaultAccountManager) AddPeer(ctx context.Context, accountID, setupKey, userID string, peer *nbpeer.Peer, temporary bool) (*nbpeer.Peer, *types.Network, []*posture.Checks, bool, error) {
|
func (am *DefaultAccountManager) AddPeer(ctx context.Context, accountID, setupKey, userID string, peer *nbpeer.Peer, temporary bool) (*nbpeer.Peer, *types.Network, []*posture.Checks, bool, error) {
|
||||||
if setupKey == "" && userID == "" && !peer.ProxyMeta.Embedded {
|
if setupKey == "" && userID == "" && !peer.ProxyMeta.Embedded {
|
||||||
// no auth method provided => reject access
|
// no auth method provided => reject access
|
||||||
return nil, nil, nil, false, status.Errorf(status.Unauthenticated, "no peer auth method provided, please use a setup key or interactive SSO login")
|
return nil, nil, nil, false, status.ErrNoAuthMethodProvided
|
||||||
}
|
}
|
||||||
|
|
||||||
upperKey := strings.ToUpper(setupKey)
|
upperKey := strings.ToUpper(setupKey)
|
||||||
@@ -1051,8 +1051,8 @@ func (am *DefaultAccountManager) SyncPeer(ctx context.Context, sync types.PeerSy
|
|||||||
return nil, nil, nil, 0, err
|
return nil, nil, nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
metaDiffAffectsPosture := posture.AffectsPosture(&metaDiff, resPostureChecks)
|
metaDiffAffectsPosture := posture.AffectsPosture(ctx, &metaDiff, resPostureChecks)
|
||||||
if isStatusChanged || sync.UpdateAccountPeers || ipv6CapabilityChanged || metaDiffAffectsPosture || metaDiff.VersionChanged || metaDiff.Hostname {
|
if requiresPeerUpdate(ctx, isStatusChanged, sync.UpdateAccountPeers, ipv6CapabilityChanged, metaDiffAffectsPosture, metaDiff.VersionChanged(), metaDiff.HostnameChanged()) {
|
||||||
changedPeerIDs := []string{peer.ID}
|
changedPeerIDs := []string{peer.ID}
|
||||||
affectedPeerIDs := am.syncPeerAffectedPeers(ctx, accountID, peer.ID, nmap, peerNotValid, metaDiffAffectsPosture)
|
affectedPeerIDs := am.syncPeerAffectedPeers(ctx, accountID, peer.ID, nmap, peerNotValid, metaDiffAffectsPosture)
|
||||||
if err = am.networkMapController.OnPeersUpdated(ctx, accountID, changedPeerIDs, affectedPeerIDs); err != nil {
|
if err = am.networkMapController.OnPeersUpdated(ctx, accountID, changedPeerIDs, affectedPeerIDs); err != nil {
|
||||||
@@ -1063,6 +1063,29 @@ func (am *DefaultAccountManager) SyncPeer(ctx context.Context, sync types.PeerSy
|
|||||||
return peer, nmap, resPostureChecks, dnsFwdPort, nil
|
return peer, nmap, resPostureChecks, dnsFwdPort, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func requiresPeerUpdate(ctx context.Context, isStatusChanged, updateAccountPeers, ipv6CapabilityChanged, metaDiffAffectsPosture, versionChanged, hostname bool) bool {
|
||||||
|
var reason string
|
||||||
|
switch {
|
||||||
|
case isStatusChanged:
|
||||||
|
reason = "status changed"
|
||||||
|
case updateAccountPeers:
|
||||||
|
reason = "update account peers"
|
||||||
|
case ipv6CapabilityChanged:
|
||||||
|
reason = "ipv6 capability changed"
|
||||||
|
case metaDiffAffectsPosture:
|
||||||
|
reason = "meta diff affects posture"
|
||||||
|
case versionChanged:
|
||||||
|
reason = "version changed"
|
||||||
|
case hostname:
|
||||||
|
reason = "hostname changed"
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
log.WithContext(ctx).Tracef("peer update required: %s", reason)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// syncPeerAffectedPeers resolves the peers affected by a SyncPeer change. The
|
// syncPeerAffectedPeers resolves the peers affected by a SyncPeer change. The
|
||||||
// peer's own validated network map is bidirectional for policy and routing
|
// peer's own validated network map is bidirectional for policy and routing
|
||||||
// reachability, so when the peer stays valid and no source-posture gate is in
|
// reachability, so when the peer stays valid and no source-posture gate is in
|
||||||
|
|||||||
@@ -107,6 +107,15 @@ type Location struct {
|
|||||||
GeoNameID uint // city level geoname id
|
GeoNameID uint // city level geoname id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// equal reports whether two locations match. ConnectionIP is a net.IP slice, so it uses
|
||||||
|
// IP.Equal, not ==.
|
||||||
|
func (l Location) equal(other Location) bool {
|
||||||
|
return l.CountryCode == other.CountryCode &&
|
||||||
|
l.CityName == other.CityName &&
|
||||||
|
l.GeoNameID == other.GeoNameID &&
|
||||||
|
l.ConnectionIP.Equal(other.ConnectionIP)
|
||||||
|
}
|
||||||
|
|
||||||
// NetworkAddress is the IP address with network and MAC address of a network interface
|
// NetworkAddress is the IP address with network and MAC address of a network interface
|
||||||
type NetworkAddress struct {
|
type NetworkAddress struct {
|
||||||
NetIP netip.Prefix `gorm:"serializer:json"`
|
NetIP netip.Prefix `gorm:"serializer:json"`
|
||||||
@@ -267,185 +276,141 @@ func (p *Peer) UpdateMetaIfNew(ctx context.Context, meta PeerSystemMeta, newLoca
|
|||||||
return MetaDiff{}
|
return MetaDiff{}
|
||||||
}
|
}
|
||||||
|
|
||||||
versionChanged := p.Meta.WtVersion != meta.WtVersion
|
|
||||||
|
|
||||||
// Avoid overwriting UIVersion if the update was triggered sole by the CLI client
|
// Avoid overwriting UIVersion if the update was triggered sole by the CLI client
|
||||||
if meta.UIVersion == "" {
|
if meta.UIVersion == "" {
|
||||||
meta.UIVersion = p.Meta.UIVersion
|
meta.UIVersion = p.Meta.UIVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
oldVersion := p.Meta.WtVersion
|
effectiveLocation := p.Location
|
||||||
|
if newLocation != nil {
|
||||||
|
effectiveLocation = *newLocation
|
||||||
|
}
|
||||||
|
|
||||||
diff := diffMeta(p.Meta, meta)
|
diff := diffMeta(p.Meta, meta, p.Location, effectiveLocation)
|
||||||
if diff.Any() {
|
if diff.Updated() {
|
||||||
p.Meta = meta
|
p.Meta = meta
|
||||||
}
|
}
|
||||||
diff.VersionChanged = versionChanged
|
p.Location = effectiveLocation
|
||||||
|
|
||||||
locationInfo := ""
|
if diff.Updated() {
|
||||||
if newLocation != nil {
|
log.WithContext(ctx).Debug(diff.LogSummary())
|
||||||
p.Location = *newLocation
|
|
||||||
diff.LocationChanged = true
|
|
||||||
locationInfo = fmt.Sprintf("location changed to %s, ", newLocation.ConnectionIP)
|
|
||||||
}
|
|
||||||
|
|
||||||
versionInfo := ""
|
|
||||||
if diff.VersionChanged {
|
|
||||||
versionInfo = fmt.Sprintf("version changed: %s -> %s, ", oldVersion, meta.WtVersion)
|
|
||||||
}
|
|
||||||
|
|
||||||
if diff.Any() || diff.VersionChanged || diff.LocationChanged {
|
|
||||||
log.WithContext(ctx).
|
|
||||||
Debugf("peer meta updated, %s%s%d field(s) changed: %s", versionInfo, locationInfo, len(diff.Changed), strings.Join(diff.Changed, ", "))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return diff
|
return diff
|
||||||
}
|
}
|
||||||
|
|
||||||
// MetaDiff records which PeerSystemMeta fields differ between two metas. Each bool
|
// MetaDiff holds a peer's full before/after state across a sync: both metas and both
|
||||||
// maps to a single struct field, except Environment, which is split into Cloud and
|
// connection locations (the location lives on Peer, not PeerSystemMeta, but posture
|
||||||
// Platform. Changed holds the human-readable `field: <old> -> <new>` entries so the
|
// checks read it). Changed lists what moved, for logging and the persistence decision;
|
||||||
// existing log line and isEqual can be derived from the same comparison.
|
// the snapshots let a posture check be replayed against old and new. Everything is derived
|
||||||
//
|
// from these fields, so there are no parallel per-field flags to keep in sync.
|
||||||
// VersionChanged and LocationChanged sit outside the per-meta-field set:
|
|
||||||
// VersionChanged tracks the WireGuard client version specifically (compared before
|
|
||||||
// the UIVersion fixup, to signal client upgrades) and LocationChanged tracks the
|
|
||||||
// peer's connection geo location, which lives on Peer rather than PeerSystemMeta.
|
|
||||||
// Neither contributes an entry to Changed, so the field-coverage accounting stays
|
|
||||||
// driven purely by the PeerSystemMeta comparison.
|
|
||||||
type MetaDiff struct {
|
type MetaDiff struct {
|
||||||
Hostname bool
|
OldMeta PeerSystemMeta
|
||||||
GoOS bool
|
NewMeta PeerSystemMeta
|
||||||
Kernel bool
|
OldLocation Location
|
||||||
KernelVersion bool
|
NewLocation Location
|
||||||
Core bool
|
|
||||||
Platform bool
|
|
||||||
OS bool
|
|
||||||
OSVersion bool
|
|
||||||
WtVersion bool
|
|
||||||
UIVersion bool
|
|
||||||
SystemSerialNumber bool
|
|
||||||
SystemProductName bool
|
|
||||||
SystemManufacturer bool
|
|
||||||
EnvironmentCloud bool
|
|
||||||
EnvironmentPlatform bool
|
|
||||||
Flags bool
|
|
||||||
Capabilities bool
|
|
||||||
NetworkAddresses bool
|
|
||||||
Files bool
|
|
||||||
|
|
||||||
VersionChanged bool
|
|
||||||
LocationChanged bool
|
|
||||||
|
|
||||||
Changed []string
|
Changed []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Any reports whether any PeerSystemMeta field changed.
|
// Updated reports whether anything changed and the peer must be persisted. diffMeta fills
|
||||||
func (d MetaDiff) Any() bool {
|
// Changed in the pass that builds the diff, so this is a length check, not a re-comparison.
|
||||||
|
// Pointer receiver: MetaDiff embeds two metas, so copying it per call is wasteful.
|
||||||
|
func (d *MetaDiff) Updated() bool {
|
||||||
return len(d.Changed) != 0
|
return len(d.Changed) != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Updated reports whether the peer needs to be persisted: any meta field changed
|
// VersionChanged reports whether the WireGuard client version changed (a client upgrade).
|
||||||
// or the geo location changed. The version flag alone does not imply a write,
|
func (d *MetaDiff) VersionChanged() bool {
|
||||||
// since a version change is also reflected in the WtVersion meta field.
|
return d.OldMeta.WtVersion != d.NewMeta.WtVersion
|
||||||
func (d MetaDiff) Updated() bool {
|
}
|
||||||
return d.Any() || d.LocationChanged || d.VersionChanged
|
|
||||||
|
// HostnameChanged reports whether the peer's hostname changed.
|
||||||
|
func (d *MetaDiff) HostnameChanged() bool {
|
||||||
|
return d.OldMeta.Hostname != d.NewMeta.Hostname
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogSummary renders the changed fields as a single human-readable line.
|
||||||
|
func (d *MetaDiff) LogSummary() string {
|
||||||
|
return fmt.Sprintf("peer meta updated, %d field(s) changed: %s",
|
||||||
|
len(d.Changed), strings.Join(d.Changed, ", "))
|
||||||
}
|
}
|
||||||
|
|
||||||
func metaDiff(oldMeta, newMeta PeerSystemMeta) []string {
|
func metaDiff(oldMeta, newMeta PeerSystemMeta) []string {
|
||||||
return diffMeta(oldMeta, newMeta).Changed
|
return diffMeta(oldMeta, newMeta, Location{}, Location{}).Changed
|
||||||
}
|
}
|
||||||
|
|
||||||
// diffMeta compares two metas field by field, returning both a per-field flag set
|
// diffMeta snapshots a peer's old and new state and records a Changed entry per field that
|
||||||
// (for callers that need to know exactly what changed, e.g. matching against
|
// moved. It is the single source of truth for the comparison: isEqual is an empty Changed
|
||||||
// posture checks) and the human-readable Changed list. It is the single source of
|
// list, so the log line and the persistence decision can never disagree.
|
||||||
// truth for meta comparison: isEqual reports equality as an empty diff, so the log
|
func diffMeta(oldMeta, newMeta PeerSystemMeta, oldLocation, newLocation Location) MetaDiff {
|
||||||
// line, the change decision, and the flags can never disagree.
|
d := MetaDiff{OldMeta: oldMeta, NewMeta: newMeta, OldLocation: oldLocation, NewLocation: newLocation}
|
||||||
func diffMeta(oldMeta, newMeta PeerSystemMeta) MetaDiff {
|
|
||||||
var d MetaDiff
|
|
||||||
add := func(field string, oldVal, newVal any) {
|
add := func(field string, oldVal, newVal any) {
|
||||||
d.Changed = append(d.Changed, fmt.Sprintf("%s: %v -> %v", field, oldVal, newVal))
|
d.Changed = append(d.Changed, fmt.Sprintf("%s: %v -> %v", field, oldVal, newVal))
|
||||||
}
|
}
|
||||||
|
|
||||||
if oldMeta.Hostname != newMeta.Hostname {
|
if oldMeta.Hostname != newMeta.Hostname {
|
||||||
d.Hostname = true
|
|
||||||
add("hostname", oldMeta.Hostname, newMeta.Hostname)
|
add("hostname", oldMeta.Hostname, newMeta.Hostname)
|
||||||
}
|
}
|
||||||
if oldMeta.GoOS != newMeta.GoOS {
|
if oldMeta.GoOS != newMeta.GoOS {
|
||||||
d.GoOS = true
|
|
||||||
add("goos", oldMeta.GoOS, newMeta.GoOS)
|
add("goos", oldMeta.GoOS, newMeta.GoOS)
|
||||||
}
|
}
|
||||||
if oldMeta.Kernel != newMeta.Kernel {
|
if oldMeta.Kernel != newMeta.Kernel {
|
||||||
d.Kernel = true
|
|
||||||
add("kernel", oldMeta.Kernel, newMeta.Kernel)
|
add("kernel", oldMeta.Kernel, newMeta.Kernel)
|
||||||
}
|
}
|
||||||
if oldMeta.KernelVersion != newMeta.KernelVersion {
|
if oldMeta.KernelVersion != newMeta.KernelVersion {
|
||||||
d.KernelVersion = true
|
|
||||||
add("kernel_version", oldMeta.KernelVersion, newMeta.KernelVersion)
|
add("kernel_version", oldMeta.KernelVersion, newMeta.KernelVersion)
|
||||||
}
|
}
|
||||||
if oldMeta.Core != newMeta.Core {
|
if oldMeta.Core != newMeta.Core {
|
||||||
d.Core = true
|
|
||||||
add("core", oldMeta.Core, newMeta.Core)
|
add("core", oldMeta.Core, newMeta.Core)
|
||||||
}
|
}
|
||||||
if oldMeta.Platform != newMeta.Platform {
|
if oldMeta.Platform != newMeta.Platform {
|
||||||
d.Platform = true
|
|
||||||
add("platform", oldMeta.Platform, newMeta.Platform)
|
add("platform", oldMeta.Platform, newMeta.Platform)
|
||||||
}
|
}
|
||||||
if oldMeta.OS != newMeta.OS {
|
if oldMeta.OS != newMeta.OS {
|
||||||
d.OS = true
|
|
||||||
add("os", oldMeta.OS, newMeta.OS)
|
add("os", oldMeta.OS, newMeta.OS)
|
||||||
}
|
}
|
||||||
if oldMeta.OSVersion != newMeta.OSVersion {
|
if oldMeta.OSVersion != newMeta.OSVersion {
|
||||||
d.OSVersion = true
|
|
||||||
add("os_version", oldMeta.OSVersion, newMeta.OSVersion)
|
add("os_version", oldMeta.OSVersion, newMeta.OSVersion)
|
||||||
}
|
}
|
||||||
if oldMeta.WtVersion != newMeta.WtVersion {
|
if oldMeta.WtVersion != newMeta.WtVersion {
|
||||||
d.WtVersion = true
|
|
||||||
add("wt_version", oldMeta.WtVersion, newMeta.WtVersion)
|
add("wt_version", oldMeta.WtVersion, newMeta.WtVersion)
|
||||||
}
|
}
|
||||||
if oldMeta.UIVersion != newMeta.UIVersion {
|
if oldMeta.UIVersion != newMeta.UIVersion {
|
||||||
d.UIVersion = true
|
|
||||||
add("ui_version", oldMeta.UIVersion, newMeta.UIVersion)
|
add("ui_version", oldMeta.UIVersion, newMeta.UIVersion)
|
||||||
}
|
}
|
||||||
if oldMeta.SystemSerialNumber != newMeta.SystemSerialNumber {
|
if oldMeta.SystemSerialNumber != newMeta.SystemSerialNumber {
|
||||||
d.SystemSerialNumber = true
|
|
||||||
add("system_serial_number", oldMeta.SystemSerialNumber, newMeta.SystemSerialNumber)
|
add("system_serial_number", oldMeta.SystemSerialNumber, newMeta.SystemSerialNumber)
|
||||||
}
|
}
|
||||||
if oldMeta.SystemProductName != newMeta.SystemProductName {
|
if oldMeta.SystemProductName != newMeta.SystemProductName {
|
||||||
d.SystemProductName = true
|
|
||||||
add("system_product_name", oldMeta.SystemProductName, newMeta.SystemProductName)
|
add("system_product_name", oldMeta.SystemProductName, newMeta.SystemProductName)
|
||||||
}
|
}
|
||||||
if oldMeta.SystemManufacturer != newMeta.SystemManufacturer {
|
if oldMeta.SystemManufacturer != newMeta.SystemManufacturer {
|
||||||
d.SystemManufacturer = true
|
|
||||||
add("system_manufacturer", oldMeta.SystemManufacturer, newMeta.SystemManufacturer)
|
add("system_manufacturer", oldMeta.SystemManufacturer, newMeta.SystemManufacturer)
|
||||||
}
|
}
|
||||||
if oldMeta.Environment.Cloud != newMeta.Environment.Cloud {
|
if oldMeta.Environment.Cloud != newMeta.Environment.Cloud {
|
||||||
d.EnvironmentCloud = true
|
|
||||||
add("environment_cloud", oldMeta.Environment.Cloud, newMeta.Environment.Cloud)
|
add("environment_cloud", oldMeta.Environment.Cloud, newMeta.Environment.Cloud)
|
||||||
}
|
}
|
||||||
if oldMeta.Environment.Platform != newMeta.Environment.Platform {
|
if oldMeta.Environment.Platform != newMeta.Environment.Platform {
|
||||||
d.EnvironmentPlatform = true
|
|
||||||
add("environment_platform", oldMeta.Environment.Platform, newMeta.Environment.Platform)
|
add("environment_platform", oldMeta.Environment.Platform, newMeta.Environment.Platform)
|
||||||
}
|
}
|
||||||
if !oldMeta.Flags.isEqual(newMeta.Flags) {
|
if !oldMeta.Flags.isEqual(newMeta.Flags) {
|
||||||
d.Flags = true
|
|
||||||
add("flags", fmt.Sprintf("%+v", oldMeta.Flags), fmt.Sprintf("%+v", newMeta.Flags))
|
add("flags", fmt.Sprintf("%+v", oldMeta.Flags), fmt.Sprintf("%+v", newMeta.Flags))
|
||||||
}
|
}
|
||||||
if !capabilitiesEqual(oldMeta.Capabilities, newMeta.Capabilities) {
|
if !capabilitiesEqual(oldMeta.Capabilities, newMeta.Capabilities) {
|
||||||
d.Capabilities = true
|
|
||||||
add("capabilities", oldMeta.Capabilities, newMeta.Capabilities)
|
add("capabilities", oldMeta.Capabilities, newMeta.Capabilities)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !sameMultiset(oldMeta.NetworkAddresses, newMeta.NetworkAddresses) {
|
if !sameMultiset(oldMeta.NetworkAddresses, newMeta.NetworkAddresses) {
|
||||||
d.NetworkAddresses = true
|
|
||||||
add("network_addresses", fmt.Sprintf("%v", oldMeta.NetworkAddresses), fmt.Sprintf("%v", newMeta.NetworkAddresses))
|
add("network_addresses", fmt.Sprintf("%v", oldMeta.NetworkAddresses), fmt.Sprintf("%v", newMeta.NetworkAddresses))
|
||||||
}
|
}
|
||||||
|
|
||||||
if !sameMultiset(oldMeta.Files, newMeta.Files) {
|
if !sameMultiset(oldMeta.Files, newMeta.Files) {
|
||||||
d.Files = true
|
|
||||||
add("files", fmt.Sprintf("%v", oldMeta.Files), fmt.Sprintf("%v", newMeta.Files))
|
add("files", fmt.Sprintf("%v", oldMeta.Files), fmt.Sprintf("%v", newMeta.Files))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !oldLocation.equal(newLocation) {
|
||||||
|
add("connection_ip", oldLocation.ConnectionIP, newLocation.ConnectionIP)
|
||||||
|
}
|
||||||
|
|
||||||
return d
|
return d
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ import (
|
|||||||
|
|
||||||
nbdns "github.com/netbirdio/netbird/dns"
|
nbdns "github.com/netbirdio/netbird/dns"
|
||||||
"github.com/netbirdio/netbird/management/server/activity"
|
"github.com/netbirdio/netbird/management/server/activity"
|
||||||
|
"github.com/netbirdio/netbird/management/server/geolocation"
|
||||||
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
||||||
"github.com/netbirdio/netbird/management/server/posture"
|
"github.com/netbirdio/netbird/management/server/posture"
|
||||||
"github.com/netbirdio/netbird/management/server/store"
|
"github.com/netbirdio/netbird/management/server/store"
|
||||||
@@ -2893,3 +2894,141 @@ func TestUpdatePeer_DnsLabelUniqueName(t *testing.T) {
|
|||||||
require.NoError(t, err, "renaming to unique FQDN should succeed")
|
require.NoError(t, err, "renaming to unique FQDN should succeed")
|
||||||
assert.Equal(t, "api-server", updated.DNSLabel, "DNS label should be first label of FQDN")
|
assert.Equal(t, "api-server", updated.DNSLabel, "DNS label should be first label of FQDN")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fakeGeo is a configurable geolocation.Geolocation implementation for tests. It
|
||||||
|
// returns a record built from the configured city geoname id, or an error when set.
|
||||||
|
type fakeGeo struct {
|
||||||
|
geoNameID uint
|
||||||
|
isoCode string
|
||||||
|
cityName string
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *fakeGeo) Lookup(net.IP) (*geolocation.Record, error) {
|
||||||
|
if g.err != nil {
|
||||||
|
return nil, g.err
|
||||||
|
}
|
||||||
|
record := &geolocation.Record{}
|
||||||
|
record.City.GeonameID = g.geoNameID
|
||||||
|
record.City.Names.En = g.cityName
|
||||||
|
record.Country.ISOCode = g.isoCode
|
||||||
|
return record, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *fakeGeo) GetAllCountries() ([]geolocation.Country, error) { return nil, nil }
|
||||||
|
|
||||||
|
func (g *fakeGeo) GetCitiesByCountry(string) ([]geolocation.City, error) { return nil, nil }
|
||||||
|
|
||||||
|
func (g *fakeGeo) Stop() error { return nil }
|
||||||
|
|
||||||
|
func TestResolvePeerLocation(t *testing.T) {
|
||||||
|
realIP := net.ParseIP("203.0.113.10")
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
geo geolocation.Geolocation
|
||||||
|
peer *nbpeer.Peer
|
||||||
|
realIP net.IP
|
||||||
|
want *nbpeer.Location
|
||||||
|
wantNil bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no geo configured returns nil",
|
||||||
|
geo: nil,
|
||||||
|
peer: &nbpeer.Peer{ID: "p1"},
|
||||||
|
realIP: realIP,
|
||||||
|
wantNil: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nil real IP returns nil",
|
||||||
|
geo: &fakeGeo{geoNameID: 100},
|
||||||
|
peer: &nbpeer.Peer{ID: "p1"},
|
||||||
|
realIP: nil,
|
||||||
|
wantNil: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "lookup error returns nil",
|
||||||
|
geo: &fakeGeo{err: fmt.Errorf("lookup boom")},
|
||||||
|
peer: &nbpeer.Peer{ID: "p1"},
|
||||||
|
realIP: realIP,
|
||||||
|
wantNil: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "same IP and same geoname returns nil",
|
||||||
|
geo: &fakeGeo{geoNameID: 100, isoCode: "US", cityName: "City A"},
|
||||||
|
peer: &nbpeer.Peer{
|
||||||
|
ID: "p1",
|
||||||
|
Location: nbpeer.Location{
|
||||||
|
ConnectionIP: realIP,
|
||||||
|
GeoNameID: 100,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
realIP: realIP,
|
||||||
|
wantNil: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "same IP but changed geoname returns location",
|
||||||
|
geo: &fakeGeo{geoNameID: 200, isoCode: "US", cityName: "City B"},
|
||||||
|
peer: &nbpeer.Peer{
|
||||||
|
ID: "p1",
|
||||||
|
Location: nbpeer.Location{
|
||||||
|
ConnectionIP: realIP,
|
||||||
|
GeoNameID: 100,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
realIP: realIP,
|
||||||
|
want: &nbpeer.Location{
|
||||||
|
ConnectionIP: realIP,
|
||||||
|
CountryCode: "US",
|
||||||
|
CityName: "City B",
|
||||||
|
GeoNameID: 200,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "different IP returns location",
|
||||||
|
geo: &fakeGeo{geoNameID: 100, isoCode: "US", cityName: "City A"},
|
||||||
|
peer: &nbpeer.Peer{
|
||||||
|
ID: "p1",
|
||||||
|
Location: nbpeer.Location{
|
||||||
|
ConnectionIP: net.ParseIP("198.51.100.7"),
|
||||||
|
GeoNameID: 100,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
realIP: realIP,
|
||||||
|
want: &nbpeer.Location{
|
||||||
|
ConnectionIP: realIP,
|
||||||
|
CountryCode: "US",
|
||||||
|
CityName: "City A",
|
||||||
|
GeoNameID: 100,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no prior location returns location",
|
||||||
|
geo: &fakeGeo{geoNameID: 100, isoCode: "US", cityName: "City A"},
|
||||||
|
peer: &nbpeer.Peer{ID: "p1"},
|
||||||
|
realIP: realIP,
|
||||||
|
want: &nbpeer.Location{
|
||||||
|
ConnectionIP: realIP,
|
||||||
|
CountryCode: "US",
|
||||||
|
CityName: "City A",
|
||||||
|
GeoNameID: 100,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
am := &DefaultAccountManager{geo: tt.geo}
|
||||||
|
got := am.resolvePeerLocation(context.Background(), tt.peer, tt.realIP)
|
||||||
|
if tt.wantNil {
|
||||||
|
assert.Nil(t, got, "resolved location should be nil")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NotNil(t, got, "resolved location should not be nil")
|
||||||
|
assert.True(t, tt.want.ConnectionIP.Equal(got.ConnectionIP), "connection IP should match")
|
||||||
|
assert.Equal(t, tt.want.CountryCode, got.CountryCode, "country code should match")
|
||||||
|
assert.Equal(t, tt.want.CityName, got.CityName, "city name should match")
|
||||||
|
assert.Equal(t, tt.want.GeoNameID, got.GeoNameID, "geoname id should match")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
202
management/server/posture/affects_posture_test.go
Normal file
202
management/server/posture/affects_posture_test.go
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
package posture
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
||||||
|
)
|
||||||
|
|
||||||
|
// diffFrom builds a MetaDiff from the old/new snapshots AffectsPosture replays against.
|
||||||
|
func diffFrom(oldMeta, newMeta nbpeer.PeerSystemMeta, oldLoc, newLoc nbpeer.Location) *nbpeer.MetaDiff {
|
||||||
|
return &nbpeer.MetaDiff{
|
||||||
|
OldMeta: oldMeta,
|
||||||
|
NewMeta: newMeta,
|
||||||
|
OldLocation: oldLoc,
|
||||||
|
NewLocation: newLoc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checks(def ChecksDefinition) []*Checks {
|
||||||
|
return []*Checks{{Checks: def}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAffectsPosture_NilDiff(t *testing.T) {
|
||||||
|
assert.False(t, AffectsPosture(context.Background(), nil, checks(ChecksDefinition{
|
||||||
|
NBVersionCheck: &NBVersionCheck{MinVersion: "1.0.0"},
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAffectsPosture_NBVersion(t *testing.T) {
|
||||||
|
c := checks(ChecksDefinition{NBVersionCheck: &NBVersionCheck{MinVersion: "1.2.0"}})
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
oldVer, newVer string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{"both above min, no flip", "1.3.0", "1.4.0", false},
|
||||||
|
{"both below min, no flip", "1.0.0", "1.1.0", false},
|
||||||
|
{"crosses up below->above", "1.1.0", "1.3.0", true},
|
||||||
|
{"crosses down above->below", "1.3.0", "1.1.0", true},
|
||||||
|
{"unparsable old only -> flip", "garbage", "1.3.0", true},
|
||||||
|
{"unparsable both -> no flip", "garbage", "junk", false},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
diff := diffFrom(
|
||||||
|
nbpeer.PeerSystemMeta{WtVersion: tt.oldVer},
|
||||||
|
nbpeer.PeerSystemMeta{WtVersion: tt.newVer},
|
||||||
|
nbpeer.Location{}, nbpeer.Location{},
|
||||||
|
)
|
||||||
|
assert.Equal(t, tt.want, AffectsPosture(context.Background(), diff, c))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAffectsPosture_OSVersion_KernelBumpWithinMin(t *testing.T) {
|
||||||
|
c := checks(ChecksDefinition{OSVersionCheck: &OSVersionCheck{
|
||||||
|
Linux: &MinKernelVersionCheck{MinKernelVersion: "5.0.0"},
|
||||||
|
}})
|
||||||
|
|
||||||
|
// Kernel moves but stays above the minimum: verdict stays pass -> not affected.
|
||||||
|
withinMin := diffFrom(
|
||||||
|
nbpeer.PeerSystemMeta{GoOS: "linux", KernelVersion: "5.10.0-arch1"},
|
||||||
|
nbpeer.PeerSystemMeta{GoOS: "linux", KernelVersion: "5.15.0-arch2"},
|
||||||
|
nbpeer.Location{}, nbpeer.Location{},
|
||||||
|
)
|
||||||
|
assert.False(t, AffectsPosture(context.Background(), withinMin, c))
|
||||||
|
|
||||||
|
// Kernel drops below the minimum: verdict flips pass -> fail -> affected.
|
||||||
|
crossesDown := diffFrom(
|
||||||
|
nbpeer.PeerSystemMeta{GoOS: "linux", KernelVersion: "5.10.0-arch1"},
|
||||||
|
nbpeer.PeerSystemMeta{GoOS: "linux", KernelVersion: "4.19.0-arch1"},
|
||||||
|
nbpeer.Location{}, nbpeer.Location{},
|
||||||
|
)
|
||||||
|
assert.True(t, AffectsPosture(context.Background(), crossesDown, c))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAffectsPosture_OSVersion_GoOSSwitchFlipsVerdict(t *testing.T) {
|
||||||
|
// Only Linux is constrained. An OS outside the switch (freebsd) passes; switching to a
|
||||||
|
// failing linux kernel flips the verdict pass -> fail.
|
||||||
|
c := checks(ChecksDefinition{OSVersionCheck: &OSVersionCheck{
|
||||||
|
Linux: &MinKernelVersionCheck{MinKernelVersion: "6.0.0"},
|
||||||
|
}})
|
||||||
|
|
||||||
|
diff := diffFrom(
|
||||||
|
nbpeer.PeerSystemMeta{GoOS: "freebsd"},
|
||||||
|
nbpeer.PeerSystemMeta{GoOS: "linux", KernelVersion: "4.19.0"},
|
||||||
|
nbpeer.Location{}, nbpeer.Location{},
|
||||||
|
)
|
||||||
|
assert.True(t, AffectsPosture(context.Background(), diff, c))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAffectsPosture_Process_GoOSSwitchFlipsVerdict(t *testing.T) {
|
||||||
|
// Process runs at a linux path. Switching GoOS to windows (no WindowsPath configured)
|
||||||
|
// flips the verdict.
|
||||||
|
c := checks(ChecksDefinition{ProcessCheck: &ProcessCheck{
|
||||||
|
Processes: []Process{{LinuxPath: "/usr/bin/foo"}},
|
||||||
|
}})
|
||||||
|
|
||||||
|
files := []nbpeer.File{{Path: "/usr/bin/foo", ProcessIsRunning: true}}
|
||||||
|
diff := diffFrom(
|
||||||
|
nbpeer.PeerSystemMeta{GoOS: "linux", Files: files},
|
||||||
|
nbpeer.PeerSystemMeta{GoOS: "windows", Files: files},
|
||||||
|
nbpeer.Location{}, nbpeer.Location{},
|
||||||
|
)
|
||||||
|
assert.True(t, AffectsPosture(context.Background(), diff, c))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAffectsPosture_Process_UnrelatedFileChange(t *testing.T) {
|
||||||
|
// A tracked process stays running while an unrelated file is added: the verdict does
|
||||||
|
// not move, so posture is not affected.
|
||||||
|
c := checks(ChecksDefinition{ProcessCheck: &ProcessCheck{
|
||||||
|
Processes: []Process{{LinuxPath: "/usr/bin/foo"}},
|
||||||
|
}})
|
||||||
|
|
||||||
|
diff := diffFrom(
|
||||||
|
nbpeer.PeerSystemMeta{GoOS: "linux", Files: []nbpeer.File{
|
||||||
|
{Path: "/usr/bin/foo", ProcessIsRunning: true},
|
||||||
|
}},
|
||||||
|
nbpeer.PeerSystemMeta{GoOS: "linux", Files: []nbpeer.File{
|
||||||
|
{Path: "/usr/bin/foo", ProcessIsRunning: true},
|
||||||
|
{Path: "/usr/bin/bar", ProcessIsRunning: true},
|
||||||
|
}},
|
||||||
|
nbpeer.Location{}, nbpeer.Location{},
|
||||||
|
)
|
||||||
|
assert.False(t, AffectsPosture(context.Background(), diff, c))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAffectsPosture_GeoLocation(t *testing.T) {
|
||||||
|
c := checks(ChecksDefinition{GeoLocationCheck: &GeoLocationCheck{
|
||||||
|
Action: CheckActionAllow,
|
||||||
|
Locations: []Location{{CountryCode: "DE"}},
|
||||||
|
}})
|
||||||
|
|
||||||
|
// Moving within allowed countries keeps the verdict; moving out flips it.
|
||||||
|
stayAllowed := diffFrom(
|
||||||
|
nbpeer.PeerSystemMeta{}, nbpeer.PeerSystemMeta{},
|
||||||
|
nbpeer.Location{CountryCode: "DE", CityName: "Berlin"},
|
||||||
|
nbpeer.Location{CountryCode: "DE", CityName: "Munich"},
|
||||||
|
)
|
||||||
|
assert.False(t, AffectsPosture(context.Background(), stayAllowed, c))
|
||||||
|
|
||||||
|
moveOut := diffFrom(
|
||||||
|
nbpeer.PeerSystemMeta{}, nbpeer.PeerSystemMeta{},
|
||||||
|
nbpeer.Location{CountryCode: "DE"},
|
||||||
|
nbpeer.Location{CountryCode: "FR"},
|
||||||
|
)
|
||||||
|
assert.True(t, AffectsPosture(context.Background(), moveOut, c))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAffectsPosture_PeerNetworkRange_ConnectionIP(t *testing.T) {
|
||||||
|
// The check reads the connection IP. Moving out of the allowed range flips the verdict;
|
||||||
|
// moving within it does not.
|
||||||
|
_, allowed, _ := net.ParseCIDR("10.0.0.0/8")
|
||||||
|
c := checks(ChecksDefinition{PeerNetworkRangeCheck: &PeerNetworkRangeCheck{
|
||||||
|
Action: CheckActionAllow,
|
||||||
|
Ranges: []netip.Prefix{netip.MustParsePrefix(allowed.String())},
|
||||||
|
}})
|
||||||
|
|
||||||
|
movesOutOfRange := diffFrom(
|
||||||
|
nbpeer.PeerSystemMeta{}, nbpeer.PeerSystemMeta{},
|
||||||
|
nbpeer.Location{ConnectionIP: net.ParseIP("10.1.2.3")},
|
||||||
|
nbpeer.Location{ConnectionIP: net.ParseIP("8.8.8.8")},
|
||||||
|
)
|
||||||
|
assert.True(t, AffectsPosture(context.Background(), movesOutOfRange, c))
|
||||||
|
|
||||||
|
staysInRange := diffFrom(
|
||||||
|
nbpeer.PeerSystemMeta{}, nbpeer.PeerSystemMeta{},
|
||||||
|
nbpeer.Location{ConnectionIP: net.ParseIP("10.1.2.3")},
|
||||||
|
nbpeer.Location{ConnectionIP: net.ParseIP("10.9.9.9")},
|
||||||
|
)
|
||||||
|
assert.False(t, AffectsPosture(context.Background(), staysInRange, c))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAffectsPosture_IrrelevantFieldChange(t *testing.T) {
|
||||||
|
// Hostname changes but no check reads it: not affected even with checks present.
|
||||||
|
c := checks(ChecksDefinition{
|
||||||
|
NBVersionCheck: &NBVersionCheck{MinVersion: "1.0.0"},
|
||||||
|
GeoLocationCheck: &GeoLocationCheck{Action: CheckActionAllow, Locations: []Location{{CountryCode: "DE"}}},
|
||||||
|
})
|
||||||
|
|
||||||
|
diff := diffFrom(
|
||||||
|
nbpeer.PeerSystemMeta{Hostname: "old", WtVersion: "1.5.0"},
|
||||||
|
nbpeer.PeerSystemMeta{Hostname: "new", WtVersion: "1.5.0"},
|
||||||
|
nbpeer.Location{CountryCode: "DE"}, nbpeer.Location{CountryCode: "DE"},
|
||||||
|
)
|
||||||
|
assert.False(t, AffectsPosture(context.Background(), diff, c))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAffectsPosture_NoChecks(t *testing.T) {
|
||||||
|
diff := diffFrom(
|
||||||
|
nbpeer.PeerSystemMeta{WtVersion: "1.0.0"},
|
||||||
|
nbpeer.PeerSystemMeta{WtVersion: "2.0.0"},
|
||||||
|
nbpeer.Location{}, nbpeer.Location{},
|
||||||
|
)
|
||||||
|
assert.False(t, AffectsPosture(context.Background(), diff, nil))
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
|
|
||||||
"github.com/hashicorp/go-version"
|
"github.com/hashicorp/go-version"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
||||||
"github.com/netbirdio/netbird/shared/management/http/api"
|
"github.com/netbirdio/netbird/shared/management/http/api"
|
||||||
@@ -52,34 +53,46 @@ type Checks struct {
|
|||||||
Checks ChecksDefinition `gorm:"serializer:json"`
|
Checks ChecksDefinition `gorm:"serializer:json"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// AffectsPosture reports whether the peer metadata changes described by diff can
|
// AffectsPosture reports whether the change in diff flips the verdict of any check. It
|
||||||
// alter the outcome of any of the given posture checks. It maps each check kind to
|
// replays each check against the peer's old and new state and compares verdicts, so a
|
||||||
// the metadata fields it inspects, so an unrelated change (e.g. a hostname update)
|
// change that moves a field but stays the right side of a threshold (e.g. a kernel bump
|
||||||
// does not force a posture re-evaluation.
|
// still above the minimum) does not force a re-evaluation. See verdictChanged for how an
|
||||||
func AffectsPosture(diff *nbpeer.MetaDiff, checks []*Checks) bool {
|
// evaluation error counts.
|
||||||
|
func AffectsPosture(ctx context.Context, diff *nbpeer.MetaDiff, checks []*Checks) bool {
|
||||||
if diff == nil {
|
if diff == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
oldPeer := nbpeer.Peer{Meta: diff.OldMeta, Location: diff.OldLocation}
|
||||||
|
newPeer := nbpeer.Peer{Meta: diff.NewMeta, Location: diff.NewLocation}
|
||||||
|
|
||||||
for _, c := range checks {
|
for _, c := range checks {
|
||||||
if c.Checks.ProcessCheck != nil && diff.Files {
|
for _, check := range c.GetChecks() {
|
||||||
return true
|
if verdictChanged(ctx, check, oldPeer, newPeer) {
|
||||||
}
|
return true
|
||||||
if c.Checks.OSVersionCheck != nil && (diff.OSVersion || diff.OS || diff.KernelVersion) {
|
}
|
||||||
return true
|
|
||||||
}
|
|
||||||
if c.Checks.NBVersionCheck != nil && diff.WtVersion {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if c.Checks.GeoLocationCheck != nil && diff.LocationChanged {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if c.Checks.PeerNetworkRangeCheck != nil && diff.NetworkAddresses {
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// verdictChanged replays check against old and new state and reports whether the verdict
|
||||||
|
// differs. Like callers, it treats an evaluation error as deny: two errors are the same
|
||||||
|
// verdict (no change), an error on one side only is a flip.
|
||||||
|
func verdictChanged(ctx context.Context, check Check, oldPeer, newPeer nbpeer.Peer) bool {
|
||||||
|
oldPass, oldErr := check.Check(ctx, oldPeer)
|
||||||
|
newPass, newErr := check.Check(ctx, newPeer)
|
||||||
|
|
||||||
|
oldVerdict := oldPass && (oldErr == nil)
|
||||||
|
newVerdict := newPass && (newErr == nil)
|
||||||
|
changed := oldVerdict != newVerdict
|
||||||
|
|
||||||
|
log.WithContext(ctx).Tracef("posture check %s replay: verdict %t -> %t (changed=%t), errs: %v -> %v",
|
||||||
|
check.Name(), oldVerdict, newVerdict, changed, oldErr, newErr)
|
||||||
|
|
||||||
|
return changed
|
||||||
|
}
|
||||||
|
|
||||||
// ChecksDefinition contains definition of actual check
|
// ChecksDefinition contains definition of actual check
|
||||||
type ChecksDefinition struct {
|
type ChecksDefinition struct {
|
||||||
NBVersionCheck *NBVersionCheck `json:",omitempty"`
|
NBVersionCheck *NBVersionCheck `json:",omitempty"`
|
||||||
|
|||||||
@@ -489,6 +489,7 @@ func TestArePostureCheckChangesAffectPeers(t *testing.T) {
|
|||||||
|
|
||||||
policy := &types.Policy{
|
policy := &types.Policy{
|
||||||
AccountID: account.Id,
|
AccountID: account.Id,
|
||||||
|
Enabled: true,
|
||||||
Rules: []*types.PolicyRule{
|
Rules: []*types.PolicyRule{
|
||||||
{
|
{
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
|
|||||||
@@ -1059,8 +1059,8 @@ func (am *DefaultAccountManager) BuildUserInfosForAccount(ctx context.Context, a
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
log.WithContext(ctx).Debugf("Got %d users from ExternalCache for account %s", len(usersFromIntegration), accountID)
|
log.WithContext(ctx).Tracef("Got %d users from ExternalCache for account %s", len(usersFromIntegration), accountID)
|
||||||
log.WithContext(ctx).Debugf("Got %d users from InternalCache for account %s", len(queriedUsers), accountID)
|
log.WithContext(ctx).Tracef("Got %d users from InternalCache for account %s", len(queriedUsers), accountID)
|
||||||
queriedUsers = append(queriedUsers, usersFromIntegration...)
|
queriedUsers = append(queriedUsers, usersFromIntegration...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,10 @@ type Type int32
|
|||||||
var (
|
var (
|
||||||
ErrExtraSettingsNotFound = errors.New("extra settings not found")
|
ErrExtraSettingsNotFound = errors.New("extra settings not found")
|
||||||
ErrPeerAlreadyLoggedIn = errors.New("peer with the same public key is already logged in")
|
ErrPeerAlreadyLoggedIn = errors.New("peer with the same public key is already logged in")
|
||||||
|
|
||||||
|
// ErrNoAuthMethodProvided is returned when a peer login attempt carries neither a
|
||||||
|
// setup key nor an SSO token. Match it with errors.Is.
|
||||||
|
ErrNoAuthMethodProvided = Errorf(Unauthenticated, "no peer auth method provided, please use a setup key or interactive SSO login")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Error is an internal error
|
// Error is an internal error
|
||||||
@@ -66,6 +70,16 @@ func (e *Error) Error() string {
|
|||||||
return e.Message
|
return e.Message
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Is reports whether target is an *Error with the same type and message,
|
||||||
|
// enabling matching with errors.Is against sentinel errors.
|
||||||
|
func (e *Error) Is(target error) bool {
|
||||||
|
var t *Error
|
||||||
|
if !errors.As(target, &t) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return e.ErrorType == t.ErrorType && e.Message == t.Message
|
||||||
|
}
|
||||||
|
|
||||||
// Errorf returns Error(ErrorType, fmt.Sprintf(format, a...)).
|
// Errorf returns Error(ErrorType, fmt.Sprintf(format, a...)).
|
||||||
func Errorf(errorType Type, format string, a ...interface{}) error {
|
func Errorf(errorType Type, format string, a ...interface{}) error {
|
||||||
return &Error{
|
return &Error{
|
||||||
|
|||||||
Reference in New Issue
Block a user