net/ipset, wgengine/filter/filtertype: add split-out packages

This moves NewContainsIPFunc from tsaddr to new ipset package.

And wgengine/filter types gets split into wgengine/filter/filtertype,
so netmap (and thus the CLI, etc) doesn't need to bring in ipset,
bart, etc.

Then add a test making sure the CLI deps don't regress.

Updates #1278

Change-Id: Ia246d6d9502bbefbdeacc4aef1bed9c8b24f54d5
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick 2024-06-16 11:34:11 -07:00 committed by Brad Fitzpatrick
parent 36b1b4af2f
commit 86e0f9b912
20 changed files with 388 additions and 347 deletions

View File

@ -6,12 +6,10 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
github.com/beorn7/perks/quantile from github.com/prometheus/client_golang/prometheus
github.com/bits-and-blooms/bitset from github.com/gaissmai/bart
💣 github.com/cespare/xxhash/v2 from github.com/prometheus/client_golang/prometheus
L github.com/coreos/go-iptables/iptables from tailscale.com/util/linuxfw
W 💣 github.com/dblohm7/wingoes from tailscale.com/util/winutil
github.com/fxamacker/cbor/v2 from tailscale.com/tka
github.com/gaissmai/bart from tailscale.com/net/tsaddr
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
L github.com/google/nftables from tailscale.com/util/linuxfw
L 💣 github.com/google/nftables/alignedbuff from github.com/google/nftables/xt
@ -48,7 +46,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
L github.com/vishvananda/netns from github.com/tailscale/netlink+
github.com/x448/float16 from github.com/fxamacker/cbor/v2
💣 go4.org/mem from tailscale.com/client/tailscale+
go4.org/netipx from tailscale.com/net/tsaddr+
go4.org/netipx from tailscale.com/net/tsaddr
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/netmon+
google.golang.org/protobuf/encoding/protodelim from github.com/prometheus/common/expfmt
google.golang.org/protobuf/encoding/prototext from github.com/prometheus/common/expfmt+
@ -97,14 +95,12 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
tailscale.com/ipn/ipnstate from tailscale.com/client/tailscale+
tailscale.com/metrics from tailscale.com/cmd/derper+
tailscale.com/net/dnscache from tailscale.com/derp/derphttp
tailscale.com/net/flowtrack from tailscale.com/net/packet+
tailscale.com/net/ktimeout from tailscale.com/cmd/derper
tailscale.com/net/netaddr from tailscale.com/ipn+
tailscale.com/net/netknob from tailscale.com/net/netns
💣 tailscale.com/net/netmon from tailscale.com/derp/derphttp+
tailscale.com/net/netns from tailscale.com/derp/derphttp
tailscale.com/net/netutil from tailscale.com/client/tailscale
tailscale.com/net/packet from tailscale.com/wgengine/filter
tailscale.com/net/sockstats from tailscale.com/derp/derphttp
tailscale.com/net/stun from tailscale.com/net/stunserver
tailscale.com/net/stunserver from tailscale.com/cmd/derper
@ -121,13 +117,13 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
W tailscale.com/tsconst from tailscale.com/net/netmon
tailscale.com/tstime from tailscale.com/derp+
tailscale.com/tstime/mono from tailscale.com/tstime/rate
tailscale.com/tstime/rate from tailscale.com/derp+
tailscale.com/tstime/rate from tailscale.com/derp
tailscale.com/tsweb from tailscale.com/cmd/derper
tailscale.com/tsweb/promvarz from tailscale.com/tsweb
tailscale.com/tsweb/varz from tailscale.com/tsweb+
tailscale.com/types/dnstype from tailscale.com/tailcfg
tailscale.com/types/empty from tailscale.com/ipn
tailscale.com/types/ipproto from tailscale.com/net/flowtrack+
tailscale.com/types/ipproto from tailscale.com/tailcfg+
tailscale.com/types/key from tailscale.com/client/tailscale+
tailscale.com/types/lazy from tailscale.com/version+
tailscale.com/types/logger from tailscale.com/cmd/derper+
@ -162,7 +158,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
W 💣 tailscale.com/util/winutil/winenv from tailscale.com/hostinfo+
tailscale.com/version from tailscale.com/derp+
tailscale.com/version/distro from tailscale.com/envknob+
tailscale.com/wgengine/filter from tailscale.com/types/netmap
tailscale.com/wgengine/filter/filtertype from tailscale.com/types/netmap
golang.org/x/crypto/acme from golang.org/x/crypto/acme/autocert
golang.org/x/crypto/acme/autocert from tailscale.com/cmd/derper
golang.org/x/crypto/argon2 from tailscale.com/tka

View File

@ -104,6 +104,8 @@ func TestDeps(t *testing.T) {
"gvisor.dev/gvisor/pkg/cpuid": "https://github.com/tailscale/tailscale/issues/9756",
"gvisor.dev/gvisor/pkg/tcpip": "https://github.com/tailscale/tailscale/issues/9756",
"gvisor.dev/gvisor/pkg/tcpip/header": "https://github.com/tailscale/tailscale/issues/9756",
"tailscale.com/net/packet": "not needed in derper",
"github.com/gaissmai/bart": "not needed in derper",
},
}.Check(t)
}

View File

@ -1,9 +1,7 @@
tailscale.com/cmd/stund dependencies: (generated by github.com/tailscale/depaware)
github.com/beorn7/perks/quantile from github.com/prometheus/client_golang/prometheus
github.com/bits-and-blooms/bitset from github.com/gaissmai/bart
💣 github.com/cespare/xxhash/v2 from github.com/prometheus/client_golang/prometheus
github.com/gaissmai/bart from tailscale.com/net/tsaddr
github.com/google/uuid from tailscale.com/util/fastuuid
💣 github.com/prometheus/client_golang/prometheus from tailscale.com/tsweb/promvarz
github.com/prometheus/client_golang/prometheus/internal from github.com/prometheus/client_golang/prometheus

View File

@ -5,12 +5,10 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/internal/common+
W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
github.com/bits-and-blooms/bitset from github.com/gaissmai/bart
L github.com/coreos/go-iptables/iptables from tailscale.com/util/linuxfw
W 💣 github.com/dblohm7/wingoes from github.com/dblohm7/wingoes/pe+
W 💣 github.com/dblohm7/wingoes/pe from tailscale.com/util/winutil/authenticode
github.com/fxamacker/cbor/v2 from tailscale.com/tka
github.com/gaissmai/bart from tailscale.com/net/tsaddr
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
L github.com/google/nftables from tailscale.com/util/linuxfw
L 💣 github.com/google/nftables/alignedbuff from github.com/google/nftables/xt
@ -59,7 +57,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
L github.com/vishvananda/netns from github.com/tailscale/netlink+
github.com/x448/float16 from github.com/fxamacker/cbor/v2
💣 go4.org/mem from tailscale.com/client/tailscale+
go4.org/netipx from tailscale.com/net/tsaddr+
go4.org/netipx from tailscale.com/net/tsaddr
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/netmon+
k8s.io/client-go/util/homedir from tailscale.com/cmd/tailscale/cli
nhooyr.io/websocket from tailscale.com/control/controlhttp+
@ -98,7 +96,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/net/dns/recursive from tailscale.com/net/dnsfallback
tailscale.com/net/dnscache from tailscale.com/control/controlhttp+
tailscale.com/net/dnsfallback from tailscale.com/control/controlhttp
tailscale.com/net/flowtrack from tailscale.com/net/packet+
tailscale.com/net/flowtrack from tailscale.com/net/packet
tailscale.com/net/netaddr from tailscale.com/ipn+
tailscale.com/net/netcheck from tailscale.com/cmd/tailscale/cli
tailscale.com/net/neterror from tailscale.com/net/netcheck+
@ -106,7 +104,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
💣 tailscale.com/net/netmon from tailscale.com/cmd/tailscale/cli+
tailscale.com/net/netns from tailscale.com/derp/derphttp+
tailscale.com/net/netutil from tailscale.com/client/tailscale+
tailscale.com/net/packet from tailscale.com/wgengine/capture+
tailscale.com/net/packet from tailscale.com/wgengine/capture
tailscale.com/net/ping from tailscale.com/net/netcheck
tailscale.com/net/portmapper from tailscale.com/cmd/tailscale/cli+
tailscale.com/net/sockstats from tailscale.com/control/controlhttp+
@ -170,7 +168,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/version from tailscale.com/client/web+
tailscale.com/version/distro from tailscale.com/client/web+
tailscale.com/wgengine/capture from tailscale.com/cmd/tailscale/cli
tailscale.com/wgengine/filter from tailscale.com/types/netmap
tailscale.com/wgengine/filter/filtertype from tailscale.com/types/netmap
golang.org/x/crypto/argon2 from tailscale.com/tka
golang.org/x/crypto/blake2b from golang.org/x/crypto/argon2+
golang.org/x/crypto/blake2s from tailscale.com/clientupdate/distsign+

View File

@ -17,6 +17,10 @@ func TestDeps(t *testing.T) {
"gvisor.dev/gvisor/pkg/cpuid": "https://github.com/tailscale/tailscale/issues/9756",
"gvisor.dev/gvisor/pkg/tcpip": "https://github.com/tailscale/tailscale/issues/9756",
"gvisor.dev/gvisor/pkg/tcpip/header": "https://github.com/tailscale/tailscale/issues/9756",
"tailscale.com/wgengine/filter": "brings in bart, etc",
"github.com/bits-and-blooms/bitset": "unneeded in CLI",
"github.com/gaissmai/bart": "unneeded in CLI",
"tailscale.com/net/ipset": "unneeded in CLI",
},
}.Check(t)
}

View File

@ -295,6 +295,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/net/dnscache from tailscale.com/control/controlclient+
tailscale.com/net/dnsfallback from tailscale.com/cmd/tailscaled+
tailscale.com/net/flowtrack from tailscale.com/net/packet+
tailscale.com/net/ipset from tailscale.com/ipn/ipnlocal+
tailscale.com/net/netaddr from tailscale.com/ipn+
tailscale.com/net/netcheck from tailscale.com/wgengine/magicsock+
tailscale.com/net/neterror from tailscale.com/net/dns/resolver+
@ -408,6 +409,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/wgengine from tailscale.com/cmd/tailscaled+
tailscale.com/wgengine/capture from tailscale.com/ipn/ipnlocal+
tailscale.com/wgengine/filter from tailscale.com/control/controlclient+
tailscale.com/wgengine/filter/filtertype from tailscale.com/types/netmap+
💣 tailscale.com/wgengine/magicsock from tailscale.com/ipn/ipnlocal+
tailscale.com/wgengine/netlog from tailscale.com/wgengine
tailscale.com/wgengine/netstack from tailscale.com/cmd/tailscaled

View File

@ -61,6 +61,7 @@
"tailscale.com/net/dns"
"tailscale.com/net/dnscache"
"tailscale.com/net/dnsfallback"
"tailscale.com/net/ipset"
"tailscale.com/net/netcheck"
"tailscale.com/net/netkernelconf"
"tailscale.com/net/netmon"
@ -2761,13 +2762,13 @@ func (b *LocalBackend) setAtomicValuesFromPrefsLocked(p ipn.PrefsView) {
b.setExposeRemoteWebClientAtomicBoolLocked(p)
if !p.Valid() {
b.containsViaIPFuncAtomic.Store(tsaddr.FalseContainsIPFunc())
b.containsViaIPFuncAtomic.Store(ipset.FalseContainsIPFunc())
b.setTCPPortsIntercepted(nil)
b.lastServeConfJSON = mem.B(nil)
b.serveConfig = ipn.ServeConfigView{}
} else {
filtered := tsaddr.FilterPrefixesCopy(p.AdvertiseRoutes(), tsaddr.IsViaPrefix)
b.containsViaIPFuncAtomic.Store(tsaddr.NewContainsIPFunc(views.SliceOf(filtered)))
b.containsViaIPFuncAtomic.Store(ipset.NewContainsIPFunc(views.SliceOf(filtered)))
b.setTCPPortsInterceptedFromNetmapAndPrefsLocked(p)
}
}

80
net/ipset/ipset.go Normal file
View File

@ -0,0 +1,80 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package ipset provides code for creating efficient IP-in-set lookup functions
// with different implementations depending on the set.
package ipset
import (
"net/netip"
"github.com/gaissmai/bart"
"tailscale.com/types/views"
)
// FalseContainsIPFunc is shorthand for NewContainsIPFunc(views.Slice[netip.Prefix]{}).
func FalseContainsIPFunc() func(ip netip.Addr) bool {
return func(ip netip.Addr) bool { return false }
}
// pathForTest is a test hook for NewContainsIPFunc, to test that it took the
// right construction path.
var pathForTest = func(string) {}
// NewContainsIPFunc returns a func that reports whether ip is in addrs.
//
// The returned func is optimized for the length of contents of addrs.
func NewContainsIPFunc(addrs views.Slice[netip.Prefix]) func(ip netip.Addr) bool {
// Specialize the three common cases: no address, just IPv4
// (or just IPv6), and both IPv4 and IPv6.
if addrs.Len() == 0 {
pathForTest("empty")
return func(netip.Addr) bool { return false }
}
// If any addr is a prefix with more than a single IP, then do either a
// linear scan or a bart table, depending on the number of addrs.
if addrs.ContainsFunc(func(p netip.Prefix) bool { return !p.IsSingleIP() }) {
if addrs.Len() > 6 {
pathForTest("bart")
// Built a bart table.
t := &bart.Table[struct{}]{}
for i := range addrs.Len() {
t.Insert(addrs.At(i), struct{}{})
}
return func(ip netip.Addr) bool {
_, ok := t.Get(ip)
return ok
}
} else {
pathForTest("linear-contains")
// Small enough to do a linear search.
acopy := addrs.AsSlice()
return func(ip netip.Addr) bool {
for _, a := range acopy {
if a.Contains(ip) {
return true
}
}
return false
}
}
}
// Fast paths for 1 and 2 IPs:
if addrs.Len() == 1 {
pathForTest("one-ip")
a := addrs.At(0)
return func(ip netip.Addr) bool { return ip == a.Addr() }
}
if addrs.Len() == 2 {
pathForTest("two-ip")
a, b := addrs.At(0), addrs.At(1)
return func(ip netip.Addr) bool { return ip == a.Addr() || ip == b.Addr() }
}
// General case:
pathForTest("ip-map")
m := map[netip.Addr]bool{}
for i := range addrs.Len() {
m[addrs.At(i).Addr()] = true
}
return func(ip netip.Addr) bool { return m[ip] }
}

149
net/ipset/ipset_test.go Normal file
View File

@ -0,0 +1,149 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package ipset
import (
"net/netip"
"testing"
"tailscale.com/tstest"
"tailscale.com/types/views"
)
func pp(ss ...string) (ret []netip.Prefix) {
for _, s := range ss {
ret = append(ret, netip.MustParsePrefix(s))
}
return
}
func aa(ss ...string) (ret []netip.Addr) {
for _, s := range ss {
ret = append(ret, netip.MustParseAddr(s))
}
return
}
var newContainsIPFuncTests = []struct {
name string
pfx []netip.Prefix
want string
wantIn []netip.Addr
wantOut []netip.Addr
}{
{
name: "empty",
pfx: pp(),
want: "empty",
wantOut: aa("8.8.8.8"),
},
{
name: "cidr-list-1",
pfx: pp("10.0.0.0/8"),
want: "linear-contains",
wantIn: aa("10.0.0.1", "10.2.3.4"),
wantOut: aa("8.8.8.8"),
},
{
name: "cidr-list-2",
pfx: pp("1.0.0.0/8", "3.0.0.0/8"),
want: "linear-contains",
wantIn: aa("1.0.0.1", "3.0.0.1"),
wantOut: aa("2.0.0.1"),
},
{
name: "cidr-list-3",
pfx: pp("1.0.0.0/8", "3.0.0.0/8", "5.0.0.0/8"),
want: "linear-contains",
wantIn: aa("1.0.0.1", "5.0.0.1"),
wantOut: aa("2.0.0.1"),
},
{
name: "cidr-list-4",
pfx: pp("1.0.0.0/8", "3.0.0.0/8", "5.0.0.0/8", "7.0.0.0/8"),
want: "linear-contains",
wantIn: aa("1.0.0.1", "7.0.0.1"),
wantOut: aa("2.0.0.1"),
},
{
name: "cidr-list-5",
pfx: pp("1.0.0.0/8", "3.0.0.0/8", "5.0.0.0/8", "7.0.0.0/8", "9.0.0.0/8"),
want: "linear-contains",
wantIn: aa("1.0.0.1", "9.0.0.1"),
wantOut: aa("2.0.0.1"),
},
{
name: "cidr-list-10",
pfx: pp("1.0.0.0/8", "3.0.0.0/8", "5.0.0.0/8", "7.0.0.0/8", "9.0.0.0/8",
"11.0.0.0/8", "13.0.0.0/8", "15.0.0.0/8", "17.0.0.0/8", "19.0.0.0/8"),
want: "bart", // big enough that bart is faster than linear-contains
wantIn: aa("1.0.0.1", "19.0.0.1"),
wantOut: aa("2.0.0.1"),
},
{
name: "one-ip",
pfx: pp("10.1.0.0/32"),
want: "one-ip",
wantIn: aa("10.1.0.0"),
wantOut: aa("10.0.0.9"),
},
{
name: "two-ip",
pfx: pp("10.1.0.0/32", "10.2.0.0/32"),
want: "two-ip",
wantIn: aa("10.1.0.0", "10.2.0.0"),
wantOut: aa("8.8.8.8"),
},
{
name: "three-ip",
pfx: pp("10.1.0.0/32", "10.2.0.0/32", "10.3.0.0/32"),
want: "ip-map",
wantIn: aa("10.1.0.0", "10.2.0.0"),
wantOut: aa("8.8.8.8"),
},
}
func BenchmarkNewContainsIPFunc(b *testing.B) {
for _, tt := range newContainsIPFuncTests {
b.Run(tt.name, func(b *testing.B) {
f := NewContainsIPFunc(views.SliceOf(tt.pfx))
for i := 0; i < b.N; i++ {
for _, ip := range tt.wantIn {
if !f(ip) {
b.Fatal("unexpected false")
}
}
for _, ip := range tt.wantOut {
if f(ip) {
b.Fatal("unexpected true")
}
}
}
})
}
}
func TestNewContainsIPFunc(t *testing.T) {
for _, tt := range newContainsIPFuncTests {
t.Run(tt.name, func(t *testing.T) {
var got string
tstest.Replace(t, &pathForTest, func(path string) { got = path })
f := NewContainsIPFunc(views.SliceOf(tt.pfx))
if got != tt.want {
t.Errorf("func type = %q; want %q", got, tt.want)
}
for _, ip := range tt.wantIn {
if !f(ip) {
t.Errorf("match(%v) = false; want true", ip)
}
}
for _, ip := range tt.wantOut {
if f(ip) {
t.Errorf("match(%v) = true; want false", ip)
}
}
})
}
}

View File

@ -11,7 +11,6 @@
"slices"
"sync"
"github.com/gaissmai/bart"
"go4.org/netipx"
"tailscale.com/net/netaddr"
"tailscale.com/types/views"
@ -161,77 +160,6 @@ type oncePrefix struct {
v netip.Prefix
}
// FalseContainsIPFunc is shorthand for NewContainsIPFunc(views.Slice[netip.Prefix]{}).
func FalseContainsIPFunc() func(ip netip.Addr) bool {
return func(ip netip.Addr) bool { return false }
}
// pathForTest is a test hook for NewContainsIPFunc, to test that it took the
// right construction path.
var pathForTest = func(string) {}
// NewContainsIPFunc returns a func that reports whether ip is in addrs.
//
// It's optimized for the cases of addrs being empty and addrs
// containing 1 or 2 single-IP prefixes (such as one IPv4 address and
// one IPv6 address).
//
// Otherwise the implementation is somewhat slow.
func NewContainsIPFunc(addrs views.Slice[netip.Prefix]) func(ip netip.Addr) bool {
// Specialize the three common cases: no address, just IPv4
// (or just IPv6), and both IPv4 and IPv6.
if addrs.Len() == 0 {
pathForTest("empty")
return func(netip.Addr) bool { return false }
}
// If any addr is a prefix with more than a single IP, then do either a
// linear scan or a bart table, depending on the number of addrs.
if addrs.ContainsFunc(func(p netip.Prefix) bool { return !p.IsSingleIP() }) {
if addrs.Len() > 6 {
pathForTest("bart")
// Built a bart table.
t := &bart.Table[struct{}]{}
for i := range addrs.Len() {
t.Insert(addrs.At(i), struct{}{})
}
return func(ip netip.Addr) bool {
_, ok := t.Get(ip)
return ok
}
} else {
pathForTest("linear-contains")
// Small enough to do a linear search.
acopy := addrs.AsSlice()
return func(ip netip.Addr) bool {
for _, a := range acopy {
if a.Contains(ip) {
return true
}
}
return false
}
}
}
// Fast paths for 1 and 2 IPs:
if addrs.Len() == 1 {
pathForTest("one-ip")
a := addrs.At(0)
return func(ip netip.Addr) bool { return ip == a.Addr() }
}
if addrs.Len() == 2 {
pathForTest("two-ip")
a, b := addrs.At(0), addrs.At(1)
return func(ip netip.Addr) bool { return ip == a.Addr() || ip == b.Addr() }
}
// General case:
pathForTest("ip-map")
m := map[netip.Addr]bool{}
for i := range addrs.Len() {
m[addrs.At(i).Addr()] = true
}
return func(ip netip.Addr) bool { return m[ip] }
}
// PrefixesContainsIP reports whether any prefix in ipp contains ip.
func PrefixesContainsIP(ipp []netip.Prefix, ip netip.Addr) bool {
for _, r := range ipp {

View File

@ -8,8 +8,6 @@
"testing"
"tailscale.com/net/netaddr"
"tailscale.com/tstest"
"tailscale.com/types/views"
)
func TestInCrostiniRange(t *testing.T) {
@ -67,143 +65,6 @@ func TestCGNATRange(t *testing.T) {
}
}
func pp(ss ...string) (ret []netip.Prefix) {
for _, s := range ss {
ret = append(ret, netip.MustParsePrefix(s))
}
return
}
func aa(ss ...string) (ret []netip.Addr) {
for _, s := range ss {
ret = append(ret, netip.MustParseAddr(s))
}
return
}
var newContainsIPFuncTests = []struct {
name string
pfx []netip.Prefix
want string
wantIn []netip.Addr
wantOut []netip.Addr
}{
{
name: "empty",
pfx: pp(),
want: "empty",
wantOut: aa("8.8.8.8"),
},
{
name: "cidr-list-1",
pfx: pp("10.0.0.0/8"),
want: "linear-contains",
wantIn: aa("10.0.0.1", "10.2.3.4"),
wantOut: aa("8.8.8.8"),
},
{
name: "cidr-list-2",
pfx: pp("1.0.0.0/8", "3.0.0.0/8"),
want: "linear-contains",
wantIn: aa("1.0.0.1", "3.0.0.1"),
wantOut: aa("2.0.0.1"),
},
{
name: "cidr-list-3",
pfx: pp("1.0.0.0/8", "3.0.0.0/8", "5.0.0.0/8"),
want: "linear-contains",
wantIn: aa("1.0.0.1", "5.0.0.1"),
wantOut: aa("2.0.0.1"),
},
{
name: "cidr-list-4",
pfx: pp("1.0.0.0/8", "3.0.0.0/8", "5.0.0.0/8", "7.0.0.0/8"),
want: "linear-contains",
wantIn: aa("1.0.0.1", "7.0.0.1"),
wantOut: aa("2.0.0.1"),
},
{
name: "cidr-list-5",
pfx: pp("1.0.0.0/8", "3.0.0.0/8", "5.0.0.0/8", "7.0.0.0/8", "9.0.0.0/8"),
want: "linear-contains",
wantIn: aa("1.0.0.1", "9.0.0.1"),
wantOut: aa("2.0.0.1"),
},
{
name: "cidr-list-10",
pfx: pp("1.0.0.0/8", "3.0.0.0/8", "5.0.0.0/8", "7.0.0.0/8", "9.0.0.0/8",
"11.0.0.0/8", "13.0.0.0/8", "15.0.0.0/8", "17.0.0.0/8", "19.0.0.0/8"),
want: "bart", // big enough that bart is faster than linear-contains
wantIn: aa("1.0.0.1", "19.0.0.1"),
wantOut: aa("2.0.0.1"),
},
{
name: "one-ip",
pfx: pp("10.1.0.0/32"),
want: "one-ip",
wantIn: aa("10.1.0.0"),
wantOut: aa("10.0.0.9"),
},
{
name: "two-ip",
pfx: pp("10.1.0.0/32", "10.2.0.0/32"),
want: "two-ip",
wantIn: aa("10.1.0.0", "10.2.0.0"),
wantOut: aa("8.8.8.8"),
},
{
name: "three-ip",
pfx: pp("10.1.0.0/32", "10.2.0.0/32", "10.3.0.0/32"),
want: "ip-map",
wantIn: aa("10.1.0.0", "10.2.0.0"),
wantOut: aa("8.8.8.8"),
},
}
func BenchmarkNewContainsIPFunc(b *testing.B) {
for _, tt := range newContainsIPFuncTests {
b.Run(tt.name, func(b *testing.B) {
f := NewContainsIPFunc(views.SliceOf(tt.pfx))
for i := 0; i < b.N; i++ {
for _, ip := range tt.wantIn {
if !f(ip) {
b.Fatal("unexpected false")
}
}
for _, ip := range tt.wantOut {
if f(ip) {
b.Fatal("unexpected true")
}
}
}
})
}
}
func TestNewContainsIPFunc(t *testing.T) {
for _, tt := range newContainsIPFuncTests {
t.Run(tt.name, func(t *testing.T) {
var got string
tstest.Replace(t, &pathForTest, func(path string) { got = path })
f := NewContainsIPFunc(views.SliceOf(tt.pfx))
if got != tt.want {
t.Errorf("func type = %q; want %q", got, tt.want)
}
for _, ip := range tt.wantIn {
if !f(ip) {
t.Errorf("match(%v) = false; want true", ip)
}
}
for _, ip := range tt.wantOut {
if f(ip) {
t.Errorf("match(%v) = true; want false", ip)
}
}
})
}
}
var sinkIP netip.Addr
func BenchmarkTailscaleServiceAddr(b *testing.B) {

View File

@ -18,7 +18,7 @@
"tailscale.com/types/key"
"tailscale.com/types/views"
"tailscale.com/util/set"
"tailscale.com/wgengine/filter"
"tailscale.com/wgengine/filter/filtertype"
)
// NetworkMap is the current state of the world.
@ -40,7 +40,7 @@ type NetworkMap struct {
Peers []tailcfg.NodeView // sorted by Node.ID
DNS tailcfg.DNSConfig
PacketFilter []filter.Match
PacketFilter []filtertype.Match
PacketFilterRules views.Slice[tailcfg.FilterRule]
SSHPolicy *tailcfg.SSHPolicy // or nil, if not enabled/allowed

View File

@ -14,9 +14,9 @@
"go4.org/netipx"
"tailscale.com/envknob"
"tailscale.com/net/flowtrack"
"tailscale.com/net/ipset"
"tailscale.com/net/netaddr"
"tailscale.com/net/packet"
"tailscale.com/net/tsaddr"
"tailscale.com/tailcfg"
"tailscale.com/tstime/rate"
"tailscale.com/types/ipproto"
@ -24,6 +24,7 @@
"tailscale.com/types/views"
"tailscale.com/util/mak"
"tailscale.com/util/slicesx"
"tailscale.com/wgengine/filter/filtertype"
)
// Filter is a stateful packet filter.
@ -110,6 +111,13 @@ func (r Response) IsDrop() bool {
HexdumpAccepts // print packet hexdump when logging accepts
)
type (
Match = filtertype.Match
NetPortRange = filtertype.NetPortRange
PortRange = filtertype.PortRange
CapMatch = filtertype.CapMatch
)
// NewAllowAllForTest returns a packet filter that accepts
// everything. Use in tests only, as it permits some kinds of spoofing
// attacks to reach the OS network stack.
@ -192,23 +200,23 @@ func New(matches []Match, localNets, logIPs *netipx.IPSet, shareStateWith *Filte
matches6: matchesFamily(matches, netip.Addr.Is6),
cap4: capMatchesFunc(matches, netip.Addr.Is4),
cap6: capMatchesFunc(matches, netip.Addr.Is6),
local4: tsaddr.FalseContainsIPFunc(),
local6: tsaddr.FalseContainsIPFunc(),
logIPs4: tsaddr.FalseContainsIPFunc(),
logIPs6: tsaddr.FalseContainsIPFunc(),
local4: ipset.FalseContainsIPFunc(),
local6: ipset.FalseContainsIPFunc(),
logIPs4: ipset.FalseContainsIPFunc(),
logIPs6: ipset.FalseContainsIPFunc(),
state: state,
}
if localNets != nil {
p := localNets.Prefixes()
p4, p6 := slicesx.Partition(p, func(p netip.Prefix) bool { return p.Addr().Is4() })
f.local4 = tsaddr.NewContainsIPFunc(views.SliceOf(p4))
f.local6 = tsaddr.NewContainsIPFunc(views.SliceOf(p6))
f.local4 = ipset.NewContainsIPFunc(views.SliceOf(p4))
f.local6 = ipset.NewContainsIPFunc(views.SliceOf(p6))
}
if logIPs != nil {
p := logIPs.Prefixes()
p4, p6 := slicesx.Partition(p, func(p netip.Prefix) bool { return p.Addr().Is4() })
f.logIPs4 = tsaddr.NewContainsIPFunc(views.SliceOf(p4))
f.logIPs6 = tsaddr.NewContainsIPFunc(views.SliceOf(p6))
f.logIPs4 = ipset.NewContainsIPFunc(views.SliceOf(p4))
f.logIPs6 = ipset.NewContainsIPFunc(views.SliceOf(p6))
}
return f
@ -233,7 +241,7 @@ func matchesFamily(ms matches, keep func(netip.Addr) bool) matches {
}
}
if len(retm.Srcs) > 0 && len(retm.Dsts) > 0 {
retm.SrcsContains = tsaddr.NewContainsIPFunc(views.SliceOf(retm.Srcs))
retm.SrcsContains = ipset.NewContainsIPFunc(views.SliceOf(retm.Srcs))
ret = append(ret, retm)
}
}
@ -255,7 +263,7 @@ func capMatchesFunc(ms matches, keep func(netip.Addr) bool) matches {
}
}
if len(retm.Srcs) > 0 {
retm.SrcsContains = tsaddr.NewContainsIPFunc(views.SliceOf(retm.Srcs))
retm.SrcsContains = ipset.NewContainsIPFunc(views.SliceOf(retm.Srcs))
ret = append(ret, retm)
}
}

View File

@ -19,6 +19,7 @@
"github.com/google/go-cmp/cmp/cmpopts"
"go4.org/netipx"
xmaps "golang.org/x/exp/maps"
"tailscale.com/net/ipset"
"tailscale.com/net/packet"
"tailscale.com/net/tsaddr"
"tailscale.com/tailcfg"
@ -28,6 +29,7 @@
"tailscale.com/types/logger"
"tailscale.com/types/views"
"tailscale.com/util/must"
"tailscale.com/wgengine/filter/filtertype"
)
// testAllowedProto is an IP protocol number we treat as allowed for
@ -44,7 +46,7 @@ func m(srcs []netip.Prefix, dsts []NetPortRange, protos ...ipproto.Proto) Match
return Match{
IPProto: protos,
Srcs: srcs,
SrcsContains: tsaddr.NewContainsIPFunc(views.SliceOf(srcs)),
SrcsContains: ipset.NewContainsIPFunc(views.SliceOf(srcs)),
Dsts: dsts,
}
}
@ -440,7 +442,7 @@ func TestLoggingPrivacy(t *testing.T) {
}
f := newFilter(logf)
f.logIPs4 = tsaddr.NewContainsIPFunc(views.SliceOf([]netip.Prefix{
f.logIPs4 = ipset.NewContainsIPFunc(views.SliceOf([]netip.Prefix{
tsaddr.CGNATRange(),
tsaddr.TailscaleULARange(),
}))
@ -702,7 +704,7 @@ func nets(nets ...string) (ret []netip.Prefix) {
func ports(s string) PortRange {
if s == "*" {
return allPorts
return filtertype.AllPorts
}
var fs, ls string

View File

@ -0,0 +1,98 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package filtertype defines the types used by wgengine/filter.
package filtertype
import (
"fmt"
"net/netip"
"strings"
"tailscale.com/tailcfg"
"tailscale.com/types/ipproto"
)
//go:generate go run tailscale.com/cmd/cloner --type=Match,CapMatch
// PortRange is a range of TCP and UDP ports.
type PortRange struct {
First, Last uint16 // inclusive
}
var AllPorts = PortRange{0, 0xffff}
func (pr PortRange) String() string {
if pr.First == 0 && pr.Last == 65535 {
return "*"
} else if pr.First == pr.Last {
return fmt.Sprintf("%d", pr.First)
} else {
return fmt.Sprintf("%d-%d", pr.First, pr.Last)
}
}
// contains returns whether port is in pr.
func (pr PortRange) Contains(port uint16) bool {
return port >= pr.First && port <= pr.Last
}
// NetPortRange combines an IP address prefix and PortRange.
type NetPortRange struct {
Net netip.Prefix
Ports PortRange
}
func (npr NetPortRange) String() string {
return fmt.Sprintf("%v:%v", npr.Net, npr.Ports)
}
// CapMatch is a capability grant match predicate.
type CapMatch struct {
// Dst is the IP prefix that the destination IP address matches against
// to get the capability.
Dst netip.Prefix
// Cap is the capability that's granted if the destination IP addresses
// matches Dst.
Cap tailcfg.PeerCapability
// Values are the raw JSON values of the capability.
// See tailcfg.PeerCapability and tailcfg.PeerCapMap for details.
Values []tailcfg.RawMessage
}
// Match matches packets from any IP address in Srcs to any ip:port in
// Dsts.
type Match struct {
IPProto []ipproto.Proto // required set (no default value at this layer)
Srcs []netip.Prefix
SrcsContains func(netip.Addr) bool `json:"-"` // report whether Addr is in Srcs
Dsts []NetPortRange // optional, if Srcs match
Caps []CapMatch // optional, if Srcs match
}
func (m Match) String() string {
// TODO(bradfitz): use strings.Builder, add String tests
srcs := []string{}
for _, src := range m.Srcs {
srcs = append(srcs, src.String())
}
dsts := []string{}
for _, dst := range m.Dsts {
dsts = append(dsts, dst.String())
}
var ss, ds string
if len(srcs) == 1 {
ss = srcs[0]
} else {
ss = "[" + strings.Join(srcs, ",") + "]"
}
if len(dsts) == 1 {
ds = dsts[0]
} else {
ds = "[" + strings.Join(dsts, ",") + "]"
}
return fmt.Sprintf("%v%v=>%v", m.IPProto, ss, ds)
}

View File

@ -3,7 +3,7 @@
// Code generated by tailscale.com/cmd/cloner; DO NOT EDIT.
package filter
package filtertype
import (
"net/netip"

View File

@ -4,101 +4,13 @@
package filter
import (
"fmt"
"net/netip"
"slices"
"strings"
"tailscale.com/net/packet"
"tailscale.com/tailcfg"
"tailscale.com/types/ipproto"
"tailscale.com/wgengine/filter/filtertype"
)
//go:generate go run tailscale.com/cmd/cloner --type=Match,CapMatch
// PortRange is a range of TCP and UDP ports.
type PortRange struct {
First, Last uint16 // inclusive
}
var allPorts = PortRange{0, 0xffff}
func (pr PortRange) String() string {
if pr.First == 0 && pr.Last == 65535 {
return "*"
} else if pr.First == pr.Last {
return fmt.Sprintf("%d", pr.First)
} else {
return fmt.Sprintf("%d-%d", pr.First, pr.Last)
}
}
// contains returns whether port is in pr.
func (pr PortRange) contains(port uint16) bool {
return port >= pr.First && port <= pr.Last
}
// NetPortRange combines an IP address prefix and PortRange.
type NetPortRange struct {
Net netip.Prefix
Ports PortRange
}
func (npr NetPortRange) String() string {
return fmt.Sprintf("%v:%v", npr.Net, npr.Ports)
}
// CapMatch is a capability grant match predicate.
type CapMatch struct {
// Dst is the IP prefix that the destination IP address matches against
// to get the capability.
Dst netip.Prefix
// Cap is the capability that's granted if the destination IP addresses
// matches Dst.
Cap tailcfg.PeerCapability
// Values are the raw JSON values of the capability.
// See tailcfg.PeerCapability and tailcfg.PeerCapMap for details.
Values []tailcfg.RawMessage
}
// Match matches packets from any IP address in Srcs to any ip:port in
// Dsts.
type Match struct {
IPProto []ipproto.Proto // required set (no default value at this layer)
Srcs []netip.Prefix
SrcsContains func(netip.Addr) bool `json:"-"` // report whether Addr is in Srcs
Dsts []NetPortRange // optional, if Srcs match
Caps []CapMatch // optional, if Srcs match
}
func (m Match) String() string {
// TODO(bradfitz): use strings.Builder, add String tests
srcs := []string{}
for _, src := range m.Srcs {
srcs = append(srcs, src.String())
}
dsts := []string{}
for _, dst := range m.Dsts {
dsts = append(dsts, dst.String())
}
var ss, ds string
if len(srcs) == 1 {
ss = srcs[0]
} else {
ss = "[" + strings.Join(srcs, ",") + "]"
}
if len(dsts) == 1 {
ds = dsts[0]
} else {
ds = "[" + strings.Join(dsts, ",") + "]"
}
return fmt.Sprintf("%v%v=>%v", m.IPProto, ss, ds)
}
type matches []Match
type matches []filtertype.Match
func (ms matches) match(q *packet.Parsed) bool {
for _, m := range ms {
@ -112,7 +24,7 @@ func (ms matches) match(q *packet.Parsed) bool {
if !dst.Net.Contains(q.Dst.Addr()) {
continue
}
if !dst.Ports.contains(q.Dst.Port()) {
if !dst.Ports.Contains(q.Dst.Port()) {
continue
}
return true
@ -147,7 +59,7 @@ func (ms matches) matchProtoAndIPsOnlyIfAllPorts(q *packet.Parsed) bool {
continue
}
for _, dst := range m.Dsts {
if dst.Ports != allPorts {
if dst.Ports != filtertype.AllPorts {
continue
}
if dst.Net.Contains(q.Dst.Addr()) {

View File

@ -9,8 +9,8 @@
"strings"
"go4.org/netipx"
"tailscale.com/net/ipset"
"tailscale.com/net/netaddr"
"tailscale.com/net/tsaddr"
"tailscale.com/tailcfg"
"tailscale.com/types/ipproto"
"tailscale.com/types/views"
@ -63,7 +63,7 @@ func MatchesFromFilterRules(pf []tailcfg.FilterRule) ([]Match, error) {
}
m.Srcs = append(m.Srcs, nets...)
}
m.SrcsContains = tsaddr.NewContainsIPFunc(views.SliceOf(m.Srcs))
m.SrcsContains = ipset.NewContainsIPFunc(views.SliceOf(m.Srcs))
for _, d := range r.DstPorts {
nets, err := parseIPSet(d.IP, d.Bits)

View File

@ -39,6 +39,7 @@
"tailscale.com/ipn/ipnlocal"
"tailscale.com/metrics"
"tailscale.com/net/dns"
"tailscale.com/net/ipset"
"tailscale.com/net/netaddr"
"tailscale.com/net/packet"
"tailscale.com/net/tsaddr"
@ -330,7 +331,7 @@ func Create(logf logger.Logf, tundev *tstun.Wrapper, e wgengine.Engine, mc *magi
driveForLocal: driveForLocal,
}
ns.ctx, ns.ctxCancel = context.WithCancel(context.Background())
ns.atomicIsLocalIPFunc.Store(tsaddr.FalseContainsIPFunc())
ns.atomicIsLocalIPFunc.Store(ipset.FalseContainsIPFunc())
ns.tundev.PostFilterPacketInboundFromWireGuard = ns.injectInbound
ns.tundev.PreFilterPacketOutboundToWireGuardNetstackIntercept = ns.handleLocalPackets
stacksForMetrics.Store(ns, struct{}{})
@ -568,10 +569,10 @@ func ipPrefixToAddressWithPrefix(ipp netip.Prefix) tcpip.AddressWithPrefix {
func (ns *Impl) UpdateNetstackIPs(nm *netmap.NetworkMap) {
var selfNode tailcfg.NodeView
if nm != nil {
ns.atomicIsLocalIPFunc.Store(tsaddr.NewContainsIPFunc(nm.GetAddresses()))
ns.atomicIsLocalIPFunc.Store(ipset.NewContainsIPFunc(nm.GetAddresses()))
selfNode = nm.SelfNode
} else {
ns.atomicIsLocalIPFunc.Store(tsaddr.FalseContainsIPFunc())
ns.atomicIsLocalIPFunc.Store(ipset.FalseContainsIPFunc())
}
oldPfx := make(map[netip.Prefix]bool)

View File

@ -27,6 +27,7 @@
"tailscale.com/ipn/ipnstate"
"tailscale.com/net/dns"
"tailscale.com/net/flowtrack"
"tailscale.com/net/ipset"
"tailscale.com/net/netmon"
"tailscale.com/net/packet"
"tailscale.com/net/sockstats"
@ -330,8 +331,8 @@ func NewUserspaceEngine(logf logger.Logf, conf Config) (_ Engine, reterr error)
return nil, err
}
}
e.isLocalAddr.Store(tsaddr.FalseContainsIPFunc())
e.isDNSIPOverTailscale.Store(tsaddr.FalseContainsIPFunc())
e.isLocalAddr.Store(ipset.FalseContainsIPFunc())
e.isDNSIPOverTailscale.Store(ipset.FalseContainsIPFunc())
if conf.NetMon != nil {
e.netMon = conf.NetMon
@ -854,7 +855,7 @@ func (e *userspaceEngine) Reconfig(cfg *wgcfg.Config, routerCfg *router.Config,
panic("dnsCfg must not be nil")
}
e.isLocalAddr.Store(tsaddr.NewContainsIPFunc(views.SliceOf(routerCfg.LocalAddrs)))
e.isLocalAddr.Store(ipset.NewContainsIPFunc(views.SliceOf(routerCfg.LocalAddrs)))
e.wgLock.Lock()
defer e.wgLock.Unlock()
@ -912,7 +913,7 @@ func (e *userspaceEngine) Reconfig(cfg *wgcfg.Config, routerCfg *router.Config,
// instead have ipnlocal populate a map of DNS IP => linkName and
// put that in the *dns.Config instead, and plumb it down to the
// dns.Manager. Maybe also with isLocalAddr above.
e.isDNSIPOverTailscale.Store(tsaddr.NewContainsIPFunc(views.SliceOf(dnsIPsOverTailscale(dnsCfg, routerCfg))))
e.isDNSIPOverTailscale.Store(ipset.NewContainsIPFunc(views.SliceOf(dnsIPsOverTailscale(dnsCfg, routerCfg))))
// See if any peers have changed disco keys, which means they've restarted.
// If so, we need to update the wireguard-go/device.Device in two phases: