matcher: clarify DestsIsTheInternet single-family semantics

DestsIsTheInternet now reports the internet when either family's /0
is contained (0.0.0.0/0 or ::/0), matching what operators expect when
they write the /0 directly. Also documents MatchFromStrings fail-open.

Updates #3157
This commit is contained in:
Kristoffer Dalby
2026-04-17 06:51:08 +00:00
parent 93e8c7285f
commit 427b2f15ee
2 changed files with 54 additions and 6 deletions

View File

@@ -53,6 +53,11 @@ func MatchFromFilterRule(rule tailcfg.FilterRule) Match {
return MatchFromStrings(rule.SrcIPs, dests) return MatchFromStrings(rule.SrcIPs, dests)
} }
// MatchFromStrings builds a Match from raw source and destination
// strings. Unparseable entries are silently dropped (fail-open): the
// resulting Match is narrower than the input described, but never
// wider. Callers that need strict validation should pre-validate
// their inputs via util.ParseIPSet.
func MatchFromStrings(sources, destinations []string) Match { func MatchFromStrings(sources, destinations []string) Match {
srcs := new(netipx.IPSetBuilder) srcs := new(netipx.IPSetBuilder)
dests := new(netipx.IPSetBuilder) dests := new(netipx.IPSetBuilder)
@@ -96,18 +101,20 @@ func (m *Match) DestsOverlapsPrefixes(prefixes ...netip.Prefix) bool {
return slices.ContainsFunc(prefixes, m.dests.OverlapsPrefix) return slices.ContainsFunc(prefixes, m.dests.OverlapsPrefix)
} }
// DestsIsTheInternet reports if the destination contains "the internet" // DestsIsTheInternet reports whether the destination covers "the
// which is a IPSet that represents "autogroup:internet" and is special // internet" — the set represented by autogroup:internet, special-cased
// cased for exit nodes. // for exit nodes. Returns true if either family's /0 is contained
// This checks if dests is a superset of TheInternet(), which handles // (0.0.0.0/0 or ::/0), or if dests is a superset of TheInternet(). A
// merged filter rules where TheInternet is combined with other destinations. // single-family /0 counts because operators may write it directly and
// it still denotes the whole internet for that family.
func (m *Match) DestsIsTheInternet() bool { func (m *Match) DestsIsTheInternet() bool {
if m.dests.ContainsPrefix(tsaddr.AllIPv4()) || if m.dests.ContainsPrefix(tsaddr.AllIPv4()) ||
m.dests.ContainsPrefix(tsaddr.AllIPv6()) { m.dests.ContainsPrefix(tsaddr.AllIPv6()) {
return true return true
} }
// Check if dests contains all prefixes of TheInternet (superset check) // Superset-of-TheInternet check handles merged filter rules
// where the internet prefixes are combined with other dests.
theInternet := util.TheInternet() theInternet := util.TheInternet()
for _, prefix := range theInternet.Prefixes() { for _, prefix := range theInternet.Prefixes() {
if !m.dests.ContainsPrefix(prefix) { if !m.dests.ContainsPrefix(prefix) {

View File

@@ -2,6 +2,7 @@ package matcher
import ( import (
"net/netip" "net/netip"
"strings"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -428,4 +429,44 @@ func TestDebugString(t *testing.T) {
assert.Contains(t, s, "Destinations:") assert.Contains(t, s, "Destinations:")
assert.Contains(t, s, "10.0.0.0/8") assert.Contains(t, s, "10.0.0.0/8")
assert.Contains(t, s, "192.168.1.0/24") assert.Contains(t, s, "192.168.1.0/24")
// Sources appear before Destinations in the output.
assert.Less(
t,
strings.Index(s, "Sources:"),
strings.Index(s, "Destinations:"),
"Sources section must precede Destinations",
)
}
func TestDebugString_Empty(t *testing.T) {
t.Parallel()
m := MatchFromStrings(nil, nil)
s := m.DebugString()
assert.Contains(t, s, "Match:")
assert.Contains(t, s, "Sources:")
assert.Contains(t, s, "Destinations:")
assert.NotContains(t, s, "/")
}
// TestMatchFromStrings_MalformedFailsOpen asserts that unparseable
// entries are silently dropped and do not crash or widen the Match.
func TestMatchFromStrings_MalformedFailsOpen(t *testing.T) {
t.Parallel()
m := MatchFromStrings(
[]string{"not-a-cidr", "10.0.0.0/8"},
[]string{"also-bogus", "192.168.1.0/24"},
)
assert.True(t, m.SrcsContainsIPs(netip.MustParseAddr("10.1.2.3")),
"valid src entry must still match")
assert.False(t, m.SrcsContainsIPs(netip.MustParseAddr("1.1.1.1")),
"malformed src entry must not widen the set")
assert.True(t, m.DestsContainsIP(netip.MustParseAddr("192.168.1.10")),
"valid dst entry must still match")
assert.False(t, m.DestsContainsIP(netip.MustParseAddr("8.8.8.8")),
"malformed dst entry must not widen the set")
} }