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:
committed by
mzbenami
parent
dc80fd6324
commit
811fe7d18e
@@ -21,6 +21,7 @@ import (
|
|||||||
"tailscale.com/tstime"
|
"tailscale.com/tstime"
|
||||||
"tailscale.com/types/logger"
|
"tailscale.com/types/logger"
|
||||||
"tailscale.com/types/mapx"
|
"tailscale.com/types/mapx"
|
||||||
|
"tailscale.com/wgengine/filter"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Extension augments LocalBackend with additional functionality.
|
// Extension augments LocalBackend with additional functionality.
|
||||||
@@ -377,6 +378,39 @@ type Hooks struct {
|
|||||||
// ShouldUploadServices reports whether this node should include services
|
// ShouldUploadServices reports whether this node should include services
|
||||||
// in Hostinfo from the portlist extension.
|
// in Hostinfo from the portlist extension.
|
||||||
ShouldUploadServices feature.Hook[func() bool]
|
ShouldUploadServices feature.Hook[func() bool]
|
||||||
|
|
||||||
|
// Filter contains hooks for the packet filter.
|
||||||
|
// See [filter.Filter] for details on how these hooks are invoked.
|
||||||
|
Filter FilterHooks
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilterHooks contains hooks that extensions can use to customize the packet
|
||||||
|
// filter. Field names match the corresponding fields in filter.Filter.
|
||||||
|
type FilterHooks struct {
|
||||||
|
// IngressAllowHooks are hooks that allow extensions to accept inbound
|
||||||
|
// packets beyond the standard filter rules. Packets that are not dropped
|
||||||
|
// by the direction-agnostic pre-check, but would be not accepted by the
|
||||||
|
// main filter rules, including the check for destinations in the node's
|
||||||
|
// local IP set, will be accepted if they match one of these hooks.
|
||||||
|
// As of 2026-02-24, the ingress filter does not implement explicit drop
|
||||||
|
// rules, but if it does, an explicitly dropped packet will be dropped,
|
||||||
|
// and these hooks will not be evaluated.
|
||||||
|
//
|
||||||
|
// Processing of hooks stop after the first one that returns true.
|
||||||
|
// The returned why string of the first match is used in logging.
|
||||||
|
// Returning false does not drop the packet.
|
||||||
|
// See also [filter.Filter.IngressAllowHooks].
|
||||||
|
IngressAllowHooks feature.Hooks[filter.PacketMatch]
|
||||||
|
|
||||||
|
// LinkLocalAllowHooks are hooks that provide exceptions to the default
|
||||||
|
// policy of dropping link-local unicast packets. They run inside the
|
||||||
|
// direction-agnostic pre-checks for both ingress and egress.
|
||||||
|
//
|
||||||
|
// A hook can allow a link-local packet to pass the link-local check,
|
||||||
|
// but the packet is still subject to all other filter rules, and could be
|
||||||
|
// dropped elsewhere. Matching link-local packets are not logged.
|
||||||
|
// See also [filter.Filter.LinkLocalAllowHooks].
|
||||||
|
LinkLocalAllowHooks feature.Hooks[filter.PacketMatch]
|
||||||
}
|
}
|
||||||
|
|
||||||
// NodeBackend is an interface to query the current node and its peers.
|
// NodeBackend is an interface to query the current node and its peers.
|
||||||
|
|||||||
@@ -2884,7 +2884,11 @@ func (b *LocalBackend) updateFilterLocked(prefs ipn.PrefsView) {
|
|||||||
b.setFilter(filter.NewShieldsUpFilter(localNets, logNets, oldFilter, b.logf))
|
b.setFilter(filter.NewShieldsUpFilter(localNets, logNets, oldFilter, b.logf))
|
||||||
} else {
|
} else {
|
||||||
b.logf("[v1] netmap packet filter: %v filters", len(packetFilter))
|
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.
|
// The filter for a jailed node is the exact same as a ShieldsUp filter.
|
||||||
oldJailedFilter := b.e.GetJailedFilter()
|
oldJailedFilter := b.e.GetJailedFilter()
|
||||||
|
|||||||
@@ -32,8 +32,9 @@ import (
|
|||||||
type Filter struct {
|
type Filter struct {
|
||||||
logf logger.Logf
|
logf logger.Logf
|
||||||
// local4 and local6 report whether an IP is "local" to this node, for the
|
// local4 and local6 report whether an IP is "local" to this node, for the
|
||||||
// respective address family. All packets coming in over tailscale must have
|
// respective address family. Inbound packets that pass the direction-agnostic
|
||||||
// a destination within local, regardless of the policy filter below.
|
// pre-checks and are not accepted by [Filter.IngressAllowHooks] must have a destination
|
||||||
|
// within local to be considered by the policy filter.
|
||||||
local4 func(netip.Addr) bool
|
local4 func(netip.Addr) bool
|
||||||
local6 func(netip.Addr) bool
|
local6 func(netip.Addr) bool
|
||||||
|
|
||||||
@@ -66,8 +67,38 @@ type Filter struct {
|
|||||||
state *filterState
|
state *filterState
|
||||||
|
|
||||||
shieldsUp bool
|
shieldsUp bool
|
||||||
|
|
||||||
|
// IngressAllowHooks are hooks that allow extensions to accept inbound
|
||||||
|
// packets beyond the standard filter rules. Packets that are not dropped
|
||||||
|
// by the direction-agnostic pre-check, but would be not accepted by the
|
||||||
|
// main filter rules, including the check for destinations in the node's
|
||||||
|
// local IP set, will be accepted if they match one of these hooks.
|
||||||
|
// As of 2026-02-24, the ingress filter does not implement explicit drop
|
||||||
|
// rules, but if it does, an explicitly dropped packet will be dropped,
|
||||||
|
// and these hooks will not be evaluated.
|
||||||
|
//
|
||||||
|
// Processing of hooks stop after the first one that returns true.
|
||||||
|
// The returned why string of the first match is used in logging.
|
||||||
|
// Returning false does not drop the packet.
|
||||||
|
// See also [filter.Filter.IngressAllowHooks].
|
||||||
|
IngressAllowHooks []PacketMatch
|
||||||
|
|
||||||
|
// LinkLocalAllowHooks are hooks that provide exceptions to the default
|
||||||
|
// policy of dropping link-local unicast packets. They run inside the
|
||||||
|
// direction-agnostic pre-checks for both ingress and egress.
|
||||||
|
//
|
||||||
|
// A hook can allow a link-local packet to pass the link-local check,
|
||||||
|
// but the packet is still subject to all other filter rules, and could be
|
||||||
|
// dropped elsewhere. Matching link-local packets are not logged.
|
||||||
|
// See also [filter.Filter.LinkLocalAllowHooks].
|
||||||
|
LinkLocalAllowHooks []PacketMatch
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PacketMatch is a function that inspects a packet and reports whether it
|
||||||
|
// matches a custom filter criterion. If match is true, why should be a short
|
||||||
|
// human-readable reason for the match, used in filter logging (e.g. "corp-dns ok").
|
||||||
|
type PacketMatch func(packet.Parsed) (match bool, why string)
|
||||||
|
|
||||||
// filterState is a state cache of past seen packets.
|
// filterState is a state cache of past seen packets.
|
||||||
type filterState struct {
|
type filterState struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
@@ -426,6 +457,16 @@ func (f *Filter) RunIn(q *packet.Parsed, rf RunFlags) Response {
|
|||||||
default:
|
default:
|
||||||
r, why = Drop, "not-ip"
|
r, why = Drop, "not-ip"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if r == noVerdict {
|
||||||
|
for _, pm := range f.IngressAllowHooks {
|
||||||
|
if match, why := pm(*q); match {
|
||||||
|
f.logRateLimit(rf, q, dir, Accept, why)
|
||||||
|
return Accept
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r = Drop
|
||||||
|
}
|
||||||
f.logRateLimit(rf, q, dir, r, why)
|
f.logRateLimit(rf, q, dir, r, why)
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
@@ -439,6 +480,7 @@ func (f *Filter) RunOut(q *packet.Parsed, rf RunFlags) (Response, usermetric.Dro
|
|||||||
// already logged
|
// already logged
|
||||||
return r, reason
|
return r, reason
|
||||||
}
|
}
|
||||||
|
|
||||||
r, why := f.runOut(q)
|
r, why := f.runOut(q)
|
||||||
f.logRateLimit(rf, q, dir, r, why)
|
f.logRateLimit(rf, q, dir, r, why)
|
||||||
return r, ""
|
return r, ""
|
||||||
@@ -455,12 +497,14 @@ func unknownProtoString(proto ipproto.Proto) string {
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// runIn4 returns noVerdict for unaccepted packets that may ultimately
|
||||||
|
// be accepted through [Filter.IngressAllowHooks].
|
||||||
func (f *Filter) runIn4(q *packet.Parsed) (r Response, why string) {
|
func (f *Filter) runIn4(q *packet.Parsed) (r Response, why string) {
|
||||||
// A compromised peer could try to send us packets for
|
// A compromised peer could try to send us packets for
|
||||||
// destinations we didn't explicitly advertise. This check is to
|
// destinations we didn't explicitly advertise. This check is to
|
||||||
// prevent that.
|
// prevent that.
|
||||||
if !f.local4(q.Dst.Addr()) {
|
if !f.local4(q.Dst.Addr()) {
|
||||||
return Drop, "destination not allowed"
|
return noVerdict, "destination not allowed"
|
||||||
}
|
}
|
||||||
|
|
||||||
switch q.IPProto {
|
switch q.IPProto {
|
||||||
@@ -510,17 +554,19 @@ func (f *Filter) runIn4(q *packet.Parsed) (r Response, why string) {
|
|||||||
if f.matches4.matchProtoAndIPsOnlyIfAllPorts(q) {
|
if f.matches4.matchProtoAndIPsOnlyIfAllPorts(q) {
|
||||||
return Accept, "other-portless ok"
|
return Accept, "other-portless ok"
|
||||||
}
|
}
|
||||||
return Drop, unknownProtoString(q.IPProto)
|
return noVerdict, unknownProtoString(q.IPProto)
|
||||||
}
|
}
|
||||||
return Drop, "no rules matched"
|
return noVerdict, "no rules matched"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// runIn6 returns noVerdict for unaccepted packets that may ultimately
|
||||||
|
// be accepted through [Filter.IngressAllowHooks].
|
||||||
func (f *Filter) runIn6(q *packet.Parsed) (r Response, why string) {
|
func (f *Filter) runIn6(q *packet.Parsed) (r Response, why string) {
|
||||||
// A compromised peer could try to send us packets for
|
// A compromised peer could try to send us packets for
|
||||||
// destinations we didn't explicitly advertise. This check is to
|
// destinations we didn't explicitly advertise. This check is to
|
||||||
// prevent that.
|
// prevent that.
|
||||||
if !f.local6(q.Dst.Addr()) {
|
if !f.local6(q.Dst.Addr()) {
|
||||||
return Drop, "destination not allowed"
|
return noVerdict, "destination not allowed"
|
||||||
}
|
}
|
||||||
|
|
||||||
switch q.IPProto {
|
switch q.IPProto {
|
||||||
@@ -570,9 +616,9 @@ func (f *Filter) runIn6(q *packet.Parsed) (r Response, why string) {
|
|||||||
if f.matches6.matchProtoAndIPsOnlyIfAllPorts(q) {
|
if f.matches6.matchProtoAndIPsOnlyIfAllPorts(q) {
|
||||||
return Accept, "other-portless ok"
|
return Accept, "other-portless ok"
|
||||||
}
|
}
|
||||||
return Drop, unknownProtoString(q.IPProto)
|
return noVerdict, unknownProtoString(q.IPProto)
|
||||||
}
|
}
|
||||||
return Drop, "no rules matched"
|
return noVerdict, "no rules matched"
|
||||||
}
|
}
|
||||||
|
|
||||||
// runIn runs the output-specific part of the filter logic.
|
// runIn runs the output-specific part of the filter logic.
|
||||||
@@ -609,6 +655,18 @@ func (d direction) String() string {
|
|||||||
|
|
||||||
var gcpDNSAddr = netaddr.IPv4(169, 254, 169, 254)
|
var gcpDNSAddr = netaddr.IPv4(169, 254, 169, 254)
|
||||||
|
|
||||||
|
func (f *Filter) isAllowedLinkLocal(q *packet.Parsed) bool {
|
||||||
|
if q.Dst.Addr() == gcpDNSAddr {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, pm := range f.LinkLocalAllowHooks {
|
||||||
|
if match, _ := pm(*q); match {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// pre runs the direction-agnostic filter logic. dir is only used for
|
// pre runs the direction-agnostic filter logic. dir is only used for
|
||||||
// logging.
|
// logging.
|
||||||
func (f *Filter) pre(q *packet.Parsed, rf RunFlags, dir direction) (Response, usermetric.DropReason) {
|
func (f *Filter) pre(q *packet.Parsed, rf RunFlags, dir direction) (Response, usermetric.DropReason) {
|
||||||
@@ -630,7 +688,7 @@ func (f *Filter) pre(q *packet.Parsed, rf RunFlags, dir direction) (Response, us
|
|||||||
f.logRateLimit(rf, q, dir, Drop, "multicast")
|
f.logRateLimit(rf, q, dir, Drop, "multicast")
|
||||||
return Drop, usermetric.ReasonMulticast
|
return Drop, usermetric.ReasonMulticast
|
||||||
}
|
}
|
||||||
if q.Dst.Addr().IsLinkLocalUnicast() && q.Dst.Addr() != gcpDNSAddr {
|
if q.Dst.Addr().IsLinkLocalUnicast() && !f.isAllowedLinkLocal(q) {
|
||||||
f.logRateLimit(rf, q, dir, Drop, "link-local-unicast")
|
f.logRateLimit(rf, q, dir, Drop, "link-local-unicast")
|
||||||
return Drop, usermetric.ReasonLinkLocalUnicast
|
return Drop, usermetric.ReasonLinkLocalUnicast
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -171,12 +171,8 @@ func TestFilter(t *testing.T) {
|
|||||||
{Drop, parsed(ipproto.TCP, ipWithoutCap.String(), "1.2.3.4", 30000, 22)},
|
{Drop, parsed(ipproto.TCP, ipWithoutCap.String(), "1.2.3.4", 30000, 22)},
|
||||||
}
|
}
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
aclFunc := filt.runIn4
|
if got := filt.RunIn(&test.p, 0); test.want != got {
|
||||||
if test.p.IPVersion == 6 {
|
t.Errorf("#%d RunIn got=%v want=%v packet:%v", i, got, test.want, test.p)
|
||||||
aclFunc = filt.runIn6
|
|
||||||
}
|
|
||||||
if got, why := aclFunc(&test.p); test.want != got {
|
|
||||||
t.Errorf("#%d runIn got=%v want=%v why=%q packet:%v", i, got, test.want, why, test.p)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if test.p.IPProto == ipproto.TCP {
|
if test.p.IPProto == ipproto.TCP {
|
||||||
@@ -191,8 +187,8 @@ func TestFilter(t *testing.T) {
|
|||||||
}
|
}
|
||||||
// TCP and UDP are treated equivalently in the filter - verify that.
|
// TCP and UDP are treated equivalently in the filter - verify that.
|
||||||
test.p.IPProto = ipproto.UDP
|
test.p.IPProto = ipproto.UDP
|
||||||
if got, why := aclFunc(&test.p); test.want != got {
|
if got := filt.RunIn(&test.p, 0); test.want != got {
|
||||||
t.Errorf("#%d runIn (UDP) got=%v want=%v why=%q packet:%v", i, got, test.want, why, test.p)
|
t.Errorf("#%d RunIn (UDP) got=%v want=%v packet:%v", i, got, test.want, test.p)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Update UDP state
|
// Update UDP state
|
||||||
@@ -1071,6 +1067,192 @@ type benchOpt struct {
|
|||||||
udp, udpOpen bool
|
udp, udpOpen bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIngressAllowHooks(t *testing.T) {
|
||||||
|
matchSrc := func(ip string) PacketMatch {
|
||||||
|
return func(q packet.Parsed) (bool, string) {
|
||||||
|
return q.Src.Addr() == mustIP(ip), "match-src"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
matchDst := func(ip string) PacketMatch {
|
||||||
|
return func(q packet.Parsed) (bool, string) {
|
||||||
|
return q.Dst.Addr() == mustIP(ip), "match-dst"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
noMatch := func(q packet.Parsed) (bool, string) { return false, "" }
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
p packet.Parsed
|
||||||
|
hooks []PacketMatch
|
||||||
|
want Response
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no_hooks_denied_src",
|
||||||
|
p: parsed(ipproto.TCP, "99.99.99.99", "1.2.3.4", 0, 22),
|
||||||
|
want: Drop,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non_matching_hook",
|
||||||
|
p: parsed(ipproto.TCP, "99.99.99.99", "1.2.3.4", 0, 22),
|
||||||
|
hooks: []PacketMatch{noMatch},
|
||||||
|
want: Drop,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "matching_hook_denied_src",
|
||||||
|
p: parsed(ipproto.TCP, "99.99.99.99", "1.2.3.4", 0, 22),
|
||||||
|
hooks: []PacketMatch{matchSrc("99.99.99.99")},
|
||||||
|
want: Accept,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non_local_dst_no_hooks",
|
||||||
|
p: parsed(ipproto.TCP, "8.1.1.1", "16.32.48.64", 0, 443),
|
||||||
|
want: Drop,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non_local_dst_with_hook",
|
||||||
|
p: parsed(ipproto.TCP, "8.1.1.1", "16.32.48.64", 0, 443),
|
||||||
|
hooks: []PacketMatch{matchDst("16.32.48.64")},
|
||||||
|
want: Accept,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "first_match_wins",
|
||||||
|
p: parsed(ipproto.TCP, "99.99.99.99", "1.2.3.4", 0, 22),
|
||||||
|
hooks: []PacketMatch{noMatch, matchSrc("99.99.99.99")},
|
||||||
|
want: Accept,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
filt := newFilter(t.Logf)
|
||||||
|
filt.IngressAllowHooks = tt.hooks
|
||||||
|
if got := filt.RunIn(&tt.p, 0); got != tt.want {
|
||||||
|
t.Errorf("RunIn = %v; want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify first-match-wins stops calling subsequent hooks.
|
||||||
|
t.Run("first_match_stops_iteration", func(t *testing.T) {
|
||||||
|
filt := newFilter(t.Logf)
|
||||||
|
p := parsed(ipproto.TCP, "99.99.99.99", "1.2.3.4", 0, 22)
|
||||||
|
var called []int
|
||||||
|
filt.IngressAllowHooks = []PacketMatch{
|
||||||
|
func(q packet.Parsed) (bool, string) {
|
||||||
|
called = append(called, 0)
|
||||||
|
return true, "first"
|
||||||
|
},
|
||||||
|
func(q packet.Parsed) (bool, string) {
|
||||||
|
called = append(called, 1)
|
||||||
|
return true, "second"
|
||||||
|
},
|
||||||
|
}
|
||||||
|
filt.RunIn(&p, 0)
|
||||||
|
if len(called) != 1 || called[0] != 0 {
|
||||||
|
t.Errorf("called = %v; want [0]", called)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLinkLocalAllowHooks(t *testing.T) {
|
||||||
|
matchDst := func(ip string) PacketMatch {
|
||||||
|
return func(q packet.Parsed) (bool, string) {
|
||||||
|
return q.Dst.Addr() == mustIP(ip), "match-dst"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
noMatch := func(q packet.Parsed) (bool, string) { return false, "" }
|
||||||
|
|
||||||
|
llPkt := func() packet.Parsed {
|
||||||
|
p := parsed(ipproto.UDP, "8.1.1.1", "169.254.1.2", 0, 53)
|
||||||
|
p.StuffForTesting(1024)
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
gcpPkt := func() packet.Parsed {
|
||||||
|
p := parsed(ipproto.UDP, "8.1.1.1", "169.254.169.254", 0, 53)
|
||||||
|
p.StuffForTesting(1024)
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
p packet.Parsed
|
||||||
|
hooks []PacketMatch
|
||||||
|
dir direction
|
||||||
|
want Response
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "dropped_by_default",
|
||||||
|
p: llPkt(),
|
||||||
|
dir: in,
|
||||||
|
want: Drop,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non_matching_hook",
|
||||||
|
p: llPkt(),
|
||||||
|
hooks: []PacketMatch{noMatch},
|
||||||
|
dir: in,
|
||||||
|
want: Drop,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "matching_hook_allows",
|
||||||
|
p: llPkt(),
|
||||||
|
hooks: []PacketMatch{matchDst("169.254.1.2")},
|
||||||
|
dir: in,
|
||||||
|
want: noVerdict,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "gcp_dns_always_allowed",
|
||||||
|
p: gcpPkt(),
|
||||||
|
dir: in,
|
||||||
|
want: noVerdict,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "matching_hook_allows_egress",
|
||||||
|
p: llPkt(),
|
||||||
|
hooks: []PacketMatch{matchDst("169.254.1.2")},
|
||||||
|
dir: out,
|
||||||
|
want: noVerdict,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "first_match_wins",
|
||||||
|
p: llPkt(),
|
||||||
|
hooks: []PacketMatch{noMatch, matchDst("169.254.1.2")},
|
||||||
|
dir: in,
|
||||||
|
want: noVerdict,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
filt := newFilter(t.Logf)
|
||||||
|
filt.LinkLocalAllowHooks = tt.hooks
|
||||||
|
got, reason := filt.pre(&tt.p, 0, tt.dir)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("pre = %v (%s); want %v", got, reason, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify first-match-wins stops calling subsequent hooks.
|
||||||
|
t.Run("first_match_stops_iteration", func(t *testing.T) {
|
||||||
|
filt := newFilter(t.Logf)
|
||||||
|
p := llPkt()
|
||||||
|
var called []int
|
||||||
|
filt.LinkLocalAllowHooks = []PacketMatch{
|
||||||
|
func(q packet.Parsed) (bool, string) {
|
||||||
|
called = append(called, 0)
|
||||||
|
return true, "first"
|
||||||
|
},
|
||||||
|
func(q packet.Parsed) (bool, string) {
|
||||||
|
called = append(called, 1)
|
||||||
|
return true, "second"
|
||||||
|
},
|
||||||
|
}
|
||||||
|
filt.pre(&p, 0, in)
|
||||||
|
if len(called) != 1 || called[0] != 0 {
|
||||||
|
t.Errorf("called = %v; want [0]", called)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func benchmarkFile(b *testing.B, file string, opt benchOpt) {
|
func benchmarkFile(b *testing.B, file string, opt benchOpt) {
|
||||||
var matches []Match
|
var matches []Match
|
||||||
bts, err := os.ReadFile(file)
|
bts, err := os.ReadFile(file)
|
||||||
|
|||||||
Reference in New Issue
Block a user