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>main
parent
36b1b4af2f
commit
86e0f9b912
@ -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] } |
||||
} |
||||
@ -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) |
||||
} |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
@ -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) |
||||
} |
||||
Loading…
Reference in new issue