Skip to content

Conversation

@aergus-tng
Copy link
Contributor

@aergus-tng aergus-tng commented Apr 1, 2025

This PR makes the Matches that are computed from FilterRules part of the Policy interface, so that they don't have to be recomputed at each call to Node.CanAccess. There have already been some discussion on this approach in #2416.

  • have read the CONTRIBUTING.md file
  • raised a GitHub issue or discussed it on the projects chat beforehand
  • added unit tests
  • added integration tests
  • updated documentation if needed
  • updated CHANGELOG.md

// access each-other at all and remove them from the peers.
if len(filter) > 0 {
changed = policy.FilterNodesByACL(node, changed, filter)
matchers := polMan.Matchers()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It occurred to me after the implementation that this could be subject to race conditions -- the ACLs could be updated between the call polMan.Filter() and the call to polMan.Matchers().

Would it maybe make more sense if Policy.Filter() returned a tuple ([]tailcfg.FilterRule, []matcher.Match) so that the returned filter rules and matches are always in sync?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is a good point, I think that is fair, particularly since it will just be returned and not computed. can probably just underscore the matchers when we dont need them.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done (and also added matchers to tests where the Policy function is tested).

@aergus-tng
Copy link
Contributor Author

Given that there is already a changelog section on the policy rework, do you think that a separate entry is necessary for this change?

filterChanged := filterHash == pm.filterHash
pm.filter = filter
pm.filterHash = filterHash
if filterChanged {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a couple of failing tests, not sure why, I wonder if this somehow could be the cause if it prevents the matchers from being generated in some conditions, like empty policies? I really do not think it should be related, but I cant see anything else special about v2.

Copy link
Contributor Author

@aergus-tng aergus-tng Apr 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't the filterChanged actually computed incorrectly a few lines above? Wouldn't filterChanged := filterHash != pm.filterHash make more sense? Should I change that?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh dear, you are right... yes please.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done :)

@ghost
Copy link

ghost commented Apr 7, 2025

Pull Request Revisions

RevisionDescription
r3
Policy matching updated with matcher cacheIntroduced new matcher caching mechanism in policy management to precompute filter rule matches and optimize access checking across nodes
r2
Test case matchers become empty sliceModified policy test case to initialize wantMatchers as an empty slice instead of nil
r1
Policy filter matcher precomputation addedModified policy handling to precompute filter matchers, improving efficiency by avoiding repeated matcher generation during access checks

✅ AI review completed for r3
Help React with emojis to give feedback on AI-generated reviews:
  • 👍 means the feedback was helpful and actionable
  • 👎 means the feedback was incorrect or unhelpful
💬 Replying to feedback with a comment helps us improve the system. Your input also contributes to shaping future interactions with the AI reviewer.

We'd love to hear from you—reach out anytime at [email protected].


filterHash := deephash.Hash(&filter)
filterChanged := filterHash == pm.filterHash
filterChanged := filterHash != pm.filterHash
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that only SSH tests in v2 fail now, I suspect that it is because there was a bug here that never returned "early" on line 102, causing 110 (clear ssh) to always run. Now that it does return early, I suspect that my logic for when the ssh needs to be clear is wrong, and it wasnt discovered because of this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if this occurs in the integration tests, but here is an example where I think updateLocked behaves incorrectly with the early return: If only the SSHs field of the policy changes, the call to updateLocked in SetPolicy would AFAICT return false, nil, but the SSH policy needs to be recompiled nevertheless in this case.

Would it maybe make sense to just always clear the SSH policy map or add an additional guard checking whether the SSHs field of the policy has changed?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That a good point, I moved the clear up to a defer so it always runs and rebased. If that makes the tests happy, then I will merge it!

@kradalby kradalby added this to the v0.26.0 milestone Apr 14, 2025
@kradalby kradalby force-pushed the regenerate-matches-on-filter-change branch from 7de444f to dc79941 Compare April 30, 2025 11:15
Comment on lines 79 to +85
filterHash := deephash.Hash(&filter)
filterChanged := filterHash == pm.filterHash
filterChanged := filterHash != pm.filterHash
pm.filter = filter
pm.filterHash = filterHash
if filterChanged {
pm.matchers = matcher.MatchesFromFilterRules(pm.filter)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the updateLocked() method, you're using filterChanged to determine whether to update matchers, but the condition to set filterChanged is incorrect. On line 80, it's defined as filterChanged := filterHash != pm.filterHash, indicating that matchers should be updated when the filter has changed. However, if filterChanged is true, then we should always update the matchers. This logic seems correct, but it differs from the code that was removed (where it was incorrectly checking filterHash == pm.filterHash).

Comment on lines +153 to 158
// Filter returns the current filter rules for the entire tailnet and the associated matchers.
func (pm *PolicyManager) Filter() ([]tailcfg.FilterRule, []matcher.Match) {
pm.mu.Lock()
defer pm.mu.Unlock()
return pm.filter
return pm.filter, pm.matchers
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In hscontrol/policy/v2/policy.go, there's a potential race condition in the Filter() method. You're returning the slice of matchers, but if another goroutine calls updateLocked() between when you get the lock and return, the matchers might become out of sync with the filter rules. Consider making a defensive copy of both slices or ensuring that any modification of these fields is atomic.

@kradalby kradalby merged commit 4651d06 into juanfont:main May 1, 2025
139 of 144 checks passed
@aergus-tng aergus-tng deleted the regenerate-matches-on-filter-change branch May 26, 2025 08:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants