ipnext,ipnlocal,wgengine/filter: add extension hooks for custom filter matchers

Add PacketMatch hooks to the packet filter, allowing extensions to
customize filtering decisions:

- IngressAllowHooks: checked in RunIn after pre() but before the
  standard runIn4/runIn6 match rules. Hooks can accept packets to
  destinations outside the local IP set. First match wins; the
  returned why string is used for logging.

- LinkLocalAllowHooks: checked inside pre() for both ingress and
  egress, providing exceptions to the default policy of dropping
  link-local unicast packets. First match wins. The GCP DNS address
  (169.254.169.254) is always allowed regardless of hooks.

PacketMatch returns (match bool, why string) to provide a log reason
consistent with the existing filter functions.

Hooks are registered via the new FilterHooks struct in ipnext.Hooks
and wired through to filter.Filter in LocalBackend.updateFilterLocked.

Fixes tailscale/corp#35989
Fixes tailscale/corp#37207

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Michael Ben-Ami <mzb@tailscale.com>
This commit is contained in:
Michael Ben-Ami
2026-02-20 17:36:40 +00:00
committed by mzbenami
parent dc80fd6324
commit 811fe7d18e
4 changed files with 296 additions and 18 deletions
+5 -1
View File
@@ -2884,7 +2884,11 @@ func (b *LocalBackend) updateFilterLocked(prefs ipn.PrefsView) {
b.setFilter(filter.NewShieldsUpFilter(localNets, logNets, oldFilter, b.logf))
} else {
b.logf("[v1] netmap packet filter: %v filters", len(packetFilter))
b.setFilter(filter.New(packetFilter, b.srcIPHasCapForFilter, localNets, logNets, oldFilter, b.logf))
filt := filter.New(packetFilter, b.srcIPHasCapForFilter, localNets, logNets, oldFilter, b.logf)
filt.IngressAllowHooks = b.extHost.Hooks().Filter.IngressAllowHooks
filt.LinkLocalAllowHooks = b.extHost.Hooks().Filter.LinkLocalAllowHooks
b.setFilter(filt)
}
// The filter for a jailed node is the exact same as a ShieldsUp filter.
oldJailedFilter := b.e.GetJailedFilter()