diff --git a/cmd/tailscale/cli/set.go b/cmd/tailscale/cli/set.go index fdd332060..fa0f0da9a 100644 --- a/cmd/tailscale/cli/set.go +++ b/cmd/tailscale/cli/set.go @@ -15,6 +15,7 @@ import ( "tailscale.com/net/netutil" "tailscale.com/net/tsaddr" "tailscale.com/safesocket" + "tailscale.com/types/views" ) var setCmd = &ffcli.Command{ @@ -171,7 +172,7 @@ func calcAdvertiseRoutesForSet(advertiseExitNodeSet, advertiseRoutesSet bool, cu if alreadyAdvertisesExitNode == setArgs.advertiseDefaultRoute { return curPrefs.AdvertiseRoutes, nil } - routes = tsaddr.FilterPrefixesCopy(curPrefs.AdvertiseRoutes, func(p netip.Prefix) bool { + routes = tsaddr.FilterPrefixesCopy(views.SliceOf(curPrefs.AdvertiseRoutes), func(p netip.Prefix) bool { return p.Bits() != 0 }) if setArgs.advertiseDefaultRoute { diff --git a/cmd/viewer/tests/tests_view.go b/cmd/viewer/tests/tests_view.go index 042f10829..4b9bf475c 100644 --- a/cmd/viewer/tests/tests_view.go +++ b/cmd/viewer/tests/tests_view.go @@ -309,8 +309,8 @@ func (v StructWithSlicesView) StructPointers() views.SliceView[*StructWithPtrs, func (v StructWithSlicesView) Structs() StructWithPtrs { panic("unsupported") } func (v StructWithSlicesView) Ints() *int { panic("unsupported") } func (v StructWithSlicesView) Slice() views.Slice[string] { return views.SliceOf(v.ж.Slice) } -func (v StructWithSlicesView) Prefixes() views.IPPrefixSlice { - return views.IPPrefixSliceOf(v.ж.Prefixes) +func (v StructWithSlicesView) Prefixes() views.Slice[netip.Prefix] { + return views.SliceOf(v.ж.Prefixes) } func (v StructWithSlicesView) Data() mem.RO { return mem.B(v.ж.Data) } diff --git a/cmd/viewer/viewer.go b/cmd/viewer/viewer.go index 0dc0b1dfb..fa3d3d7b6 100644 --- a/cmd/viewer/viewer.go +++ b/cmd/viewer/viewer.go @@ -69,8 +69,6 @@ func (v *{{.ViewName}}) UnmarshalJSON(b []byte) error { {{end}} {{define "byteSliceField"}}func (v {{.ViewName}}) {{.FieldName}}() mem.RO { return mem.B(v.ж.{{.FieldName}}) } {{end}} -{{define "ipPrefixSliceField"}}func (v {{.ViewName}}) {{.FieldName}}() views.IPPrefixSlice { return views.IPPrefixSliceOf(v.ж.{{.FieldName}}) } -{{end}} {{define "sliceField"}}func (v {{.ViewName}}) {{.FieldName}}() views.Slice[{{.FieldType}}] { return views.SliceOf(v.ж.{{.FieldName}}) } {{end}} {{define "viewSliceField"}}func (v {{.ViewName}}) {{.FieldName}}() views.SliceView[{{.FieldType}},{{.FieldViewName}}] { return views.SliceOfViews[{{.FieldType}},{{.FieldViewName}}](v.ж.{{.FieldName}}) } @@ -176,9 +174,6 @@ func genView(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named, thi case "byte": it.Import("go4.org/mem") writeTemplate("byteSliceField") - case "inet.af/netip.Prefix", "net/netip.Prefix": - it.Import("tailscale.com/types/views") - writeTemplate("ipPrefixSliceField") default: it.Import("tailscale.com/types/views") shallow, deep, base := requiresCloning(elem) diff --git a/ipn/ipn_view.go b/ipn/ipn_view.go index 1abeb6709..8a04c2c32 100644 --- a/ipn/ipn_view.go +++ b/ipn/ipn_view.go @@ -79,8 +79,8 @@ func (v PrefsView) Hostname() string { return v.ж.Hostname } func (v PrefsView) NotepadURLs() bool { return v.ж.NotepadURLs } func (v PrefsView) ForceDaemon() bool { return v.ж.ForceDaemon } func (v PrefsView) Egg() bool { return v.ж.Egg } -func (v PrefsView) AdvertiseRoutes() views.IPPrefixSlice { - return views.IPPrefixSliceOf(v.ж.AdvertiseRoutes) +func (v PrefsView) AdvertiseRoutes() views.Slice[netip.Prefix] { + return views.SliceOf(v.ж.AdvertiseRoutes) } func (v PrefsView) NoSNAT() bool { return v.ж.NoSNAT } func (v PrefsView) NetfilterMode() preftype.NetfilterMode { return v.ж.NetfilterMode } diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index b62326fbb..9d0cacac0 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -776,13 +776,13 @@ func (b *LocalBackend) populatePeerStatusLocked(sb *ipnstate.StatusBuilder) { func peerStatusFromNode(ps *ipnstate.PeerStatus, n *tailcfg.Node) { ps.ID = n.StableID ps.Created = n.Created - ps.ExitNodeOption = tsaddr.ContainsExitRoutes(n.AllowedIPs) + ps.ExitNodeOption = tsaddr.ContainsExitRoutes(views.SliceOf(n.AllowedIPs)) if n.Tags != nil { v := views.SliceOf(n.Tags) ps.Tags = &v } if n.PrimaryRoutes != nil { - v := views.IPPrefixSliceOf(n.PrimaryRoutes) + v := views.SliceOf(n.PrimaryRoutes) ps.PrimaryRoutes = &v } @@ -2301,7 +2301,8 @@ func (b *LocalBackend) setAtomicValuesFromPrefsLocked(p ipn.PrefsView) { b.lastServeConfJSON = mem.B(nil) b.serveConfig = ipn.ServeConfigView{} } else { - b.containsViaIPFuncAtomic.Store(tsaddr.NewContainsIPFunc(p.AdvertiseRoutes().Filter(tsaddr.IsViaPrefix))) + filtered := tsaddr.FilterPrefixesCopy(p.AdvertiseRoutes(), tsaddr.IsViaPrefix) + b.containsViaIPFuncAtomic.Store(tsaddr.NewContainsIPFunc(filtered)) b.setTCPPortsInterceptedFromNetmapAndPrefsLocked(p) } } diff --git a/ipn/ipnstate/ipnstate.go b/ipn/ipnstate/ipnstate.go index 1d1d28b6c..77dfd376a 100644 --- a/ipn/ipnstate/ipnstate.go +++ b/ipn/ipnstate/ipnstate.go @@ -209,7 +209,7 @@ type PeerStatus struct { // PrimaryRoutes are the routes this node is currently the primary // subnet router for, as determined by the control plane. It does // not include the IPs in TailscaleIPs. - PrimaryRoutes *views.IPPrefixSlice `json:",omitempty"` + PrimaryRoutes *views.Slice[netip.Prefix] `json:",omitempty"` // Endpoints: Addrs []string diff --git a/ipn/prefs.go b/ipn/prefs.go index e81c44ad2..59bc373a7 100644 --- a/ipn/prefs.go +++ b/ipn/prefs.go @@ -23,6 +23,7 @@ import ( "tailscale.com/tailcfg" "tailscale.com/types/persist" "tailscale.com/types/preftype" + "tailscale.com/types/views" "tailscale.com/util/dnsname" ) @@ -506,7 +507,7 @@ func (p *Prefs) AdvertisesExitNode() bool { if p == nil { return false } - return tsaddr.ContainsExitRoutes(p.AdvertiseRoutes) + return tsaddr.ContainsExitRoutes(views.SliceOf(p.AdvertiseRoutes)) } // SetAdvertiseExitNode mutates p (if non-nil) to add or remove the two diff --git a/net/tsaddr/tsaddr.go b/net/tsaddr/tsaddr.go index 2ce98b04c..6319e30f4 100644 --- a/net/tsaddr/tsaddr.go +++ b/net/tsaddr/tsaddr.go @@ -13,6 +13,7 @@ import ( "go4.org/netipx" "tailscale.com/net/netaddr" + "tailscale.com/types/views" ) // ChromeOSVMRange returns the subset of the CGNAT IPv4 range used by @@ -225,9 +226,10 @@ func PrefixIs6(p netip.Prefix) bool { return p.Addr().Is6() } // ContainsExitRoutes reports whether rr contains both the IPv4 and // IPv6 /0 route. -func ContainsExitRoutes(rr []netip.Prefix) bool { +func ContainsExitRoutes(rr views.Slice[netip.Prefix]) bool { var v4, v6 bool - for _, r := range rr { + for i := range rr.LenIter() { + r := rr.At(i) if r == allIPv4 { v4 = true } else if r == allIPv6 { @@ -237,6 +239,17 @@ func ContainsExitRoutes(rr []netip.Prefix) bool { return v4 && v6 } +// ContainsNonExitSubnetRoutes reports whether v contains Subnet +// Routes other than ExitNode Routes. +func ContainsNonExitSubnetRoutes(rr views.Slice[netip.Prefix]) bool { + for i := range rr.LenIter() { + if rr.At(i).Bits() != 0 { + return true + } + } + return false +} + var ( allIPv4 = netip.MustParsePrefix("0.0.0.0/0") allIPv6 = netip.MustParsePrefix("::/0") @@ -258,10 +271,10 @@ func SortPrefixes(p []netip.Prefix) { // FilterPrefixes returns a new slice, not aliasing in, containing elements of // in that match f. -func FilterPrefixesCopy(in []netip.Prefix, f func(netip.Prefix) bool) []netip.Prefix { +func FilterPrefixesCopy(in views.Slice[netip.Prefix], f func(netip.Prefix) bool) []netip.Prefix { var out []netip.Prefix - for _, v := range in { - if f(v) { + for i := range in.LenIter() { + if v := in.At(i); f(v) { out = append(out, v) } } diff --git a/tailcfg/tailcfg_view.go b/tailcfg/tailcfg_view.go index 3ed6f6b7e..607031c92 100644 --- a/tailcfg/tailcfg_view.go +++ b/tailcfg/tailcfg_view.go @@ -131,27 +131,25 @@ func (v *NodeView) UnmarshalJSON(b []byte) error { return nil } -func (v NodeView) ID() NodeID { return v.ж.ID } -func (v NodeView) StableID() StableNodeID { return v.ж.StableID } -func (v NodeView) Name() string { return v.ж.Name } -func (v NodeView) User() UserID { return v.ж.User } -func (v NodeView) Sharer() UserID { return v.ж.Sharer } -func (v NodeView) Key() key.NodePublic { return v.ж.Key } -func (v NodeView) KeyExpiry() time.Time { return v.ж.KeyExpiry } -func (v NodeView) KeySignature() mem.RO { return mem.B(v.ж.KeySignature) } -func (v NodeView) Machine() key.MachinePublic { return v.ж.Machine } -func (v NodeView) DiscoKey() key.DiscoPublic { return v.ж.DiscoKey } -func (v NodeView) Addresses() views.IPPrefixSlice { return views.IPPrefixSliceOf(v.ж.Addresses) } -func (v NodeView) AllowedIPs() views.IPPrefixSlice { return views.IPPrefixSliceOf(v.ж.AllowedIPs) } -func (v NodeView) Endpoints() views.Slice[string] { return views.SliceOf(v.ж.Endpoints) } -func (v NodeView) DERP() string { return v.ж.DERP } -func (v NodeView) Hostinfo() HostinfoView { return v.ж.Hostinfo } -func (v NodeView) Created() time.Time { return v.ж.Created } -func (v NodeView) Cap() CapabilityVersion { return v.ж.Cap } -func (v NodeView) Tags() views.Slice[string] { return views.SliceOf(v.ж.Tags) } -func (v NodeView) PrimaryRoutes() views.IPPrefixSlice { - return views.IPPrefixSliceOf(v.ж.PrimaryRoutes) -} +func (v NodeView) ID() NodeID { return v.ж.ID } +func (v NodeView) StableID() StableNodeID { return v.ж.StableID } +func (v NodeView) Name() string { return v.ж.Name } +func (v NodeView) User() UserID { return v.ж.User } +func (v NodeView) Sharer() UserID { return v.ж.Sharer } +func (v NodeView) Key() key.NodePublic { return v.ж.Key } +func (v NodeView) KeyExpiry() time.Time { return v.ж.KeyExpiry } +func (v NodeView) KeySignature() mem.RO { return mem.B(v.ж.KeySignature) } +func (v NodeView) Machine() key.MachinePublic { return v.ж.Machine } +func (v NodeView) DiscoKey() key.DiscoPublic { return v.ж.DiscoKey } +func (v NodeView) Addresses() views.Slice[netip.Prefix] { return views.SliceOf(v.ж.Addresses) } +func (v NodeView) AllowedIPs() views.Slice[netip.Prefix] { return views.SliceOf(v.ж.AllowedIPs) } +func (v NodeView) Endpoints() views.Slice[string] { return views.SliceOf(v.ж.Endpoints) } +func (v NodeView) DERP() string { return v.ж.DERP } +func (v NodeView) Hostinfo() HostinfoView { return v.ж.Hostinfo } +func (v NodeView) Created() time.Time { return v.ж.Created } +func (v NodeView) Cap() CapabilityVersion { return v.ж.Cap } +func (v NodeView) Tags() views.Slice[string] { return views.SliceOf(v.ж.Tags) } +func (v NodeView) PrimaryRoutes() views.Slice[netip.Prefix] { return views.SliceOf(v.ж.PrimaryRoutes) } func (v NodeView) LastSeen() *time.Time { if v.ж.LastSeen == nil { return nil @@ -266,41 +264,39 @@ func (v *HostinfoView) UnmarshalJSON(b []byte) error { return nil } -func (v HostinfoView) IPNVersion() string { return v.ж.IPNVersion } -func (v HostinfoView) FrontendLogID() string { return v.ж.FrontendLogID } -func (v HostinfoView) BackendLogID() string { return v.ж.BackendLogID } -func (v HostinfoView) OS() string { return v.ж.OS } -func (v HostinfoView) OSVersion() string { return v.ж.OSVersion } -func (v HostinfoView) Container() opt.Bool { return v.ж.Container } -func (v HostinfoView) Env() string { return v.ж.Env } -func (v HostinfoView) Distro() string { return v.ж.Distro } -func (v HostinfoView) DistroVersion() string { return v.ж.DistroVersion } -func (v HostinfoView) DistroCodeName() string { return v.ж.DistroCodeName } -func (v HostinfoView) App() string { return v.ж.App } -func (v HostinfoView) Desktop() opt.Bool { return v.ж.Desktop } -func (v HostinfoView) Package() string { return v.ж.Package } -func (v HostinfoView) DeviceModel() string { return v.ж.DeviceModel } -func (v HostinfoView) PushDeviceToken() string { return v.ж.PushDeviceToken } -func (v HostinfoView) Hostname() string { return v.ж.Hostname } -func (v HostinfoView) ShieldsUp() bool { return v.ж.ShieldsUp } -func (v HostinfoView) ShareeNode() bool { return v.ж.ShareeNode } -func (v HostinfoView) NoLogsNoSupport() bool { return v.ж.NoLogsNoSupport } -func (v HostinfoView) WireIngress() bool { return v.ж.WireIngress } -func (v HostinfoView) AllowsUpdate() bool { return v.ж.AllowsUpdate } -func (v HostinfoView) Machine() string { return v.ж.Machine } -func (v HostinfoView) GoArch() string { return v.ж.GoArch } -func (v HostinfoView) GoArchVar() string { return v.ж.GoArchVar } -func (v HostinfoView) GoVersion() string { return v.ж.GoVersion } -func (v HostinfoView) RoutableIPs() views.IPPrefixSlice { - return views.IPPrefixSliceOf(v.ж.RoutableIPs) -} -func (v HostinfoView) RequestTags() views.Slice[string] { return views.SliceOf(v.ж.RequestTags) } -func (v HostinfoView) Services() views.Slice[Service] { return views.SliceOf(v.ж.Services) } -func (v HostinfoView) NetInfo() NetInfoView { return v.ж.NetInfo.View() } -func (v HostinfoView) SSH_HostKeys() views.Slice[string] { return views.SliceOf(v.ж.SSH_HostKeys) } -func (v HostinfoView) Cloud() string { return v.ж.Cloud } -func (v HostinfoView) Userspace() opt.Bool { return v.ж.Userspace } -func (v HostinfoView) UserspaceRouter() opt.Bool { return v.ж.UserspaceRouter } +func (v HostinfoView) IPNVersion() string { return v.ж.IPNVersion } +func (v HostinfoView) FrontendLogID() string { return v.ж.FrontendLogID } +func (v HostinfoView) BackendLogID() string { return v.ж.BackendLogID } +func (v HostinfoView) OS() string { return v.ж.OS } +func (v HostinfoView) OSVersion() string { return v.ж.OSVersion } +func (v HostinfoView) Container() opt.Bool { return v.ж.Container } +func (v HostinfoView) Env() string { return v.ж.Env } +func (v HostinfoView) Distro() string { return v.ж.Distro } +func (v HostinfoView) DistroVersion() string { return v.ж.DistroVersion } +func (v HostinfoView) DistroCodeName() string { return v.ж.DistroCodeName } +func (v HostinfoView) App() string { return v.ж.App } +func (v HostinfoView) Desktop() opt.Bool { return v.ж.Desktop } +func (v HostinfoView) Package() string { return v.ж.Package } +func (v HostinfoView) DeviceModel() string { return v.ж.DeviceModel } +func (v HostinfoView) PushDeviceToken() string { return v.ж.PushDeviceToken } +func (v HostinfoView) Hostname() string { return v.ж.Hostname } +func (v HostinfoView) ShieldsUp() bool { return v.ж.ShieldsUp } +func (v HostinfoView) ShareeNode() bool { return v.ж.ShareeNode } +func (v HostinfoView) NoLogsNoSupport() bool { return v.ж.NoLogsNoSupport } +func (v HostinfoView) WireIngress() bool { return v.ж.WireIngress } +func (v HostinfoView) AllowsUpdate() bool { return v.ж.AllowsUpdate } +func (v HostinfoView) Machine() string { return v.ж.Machine } +func (v HostinfoView) GoArch() string { return v.ж.GoArch } +func (v HostinfoView) GoArchVar() string { return v.ж.GoArchVar } +func (v HostinfoView) GoVersion() string { return v.ж.GoVersion } +func (v HostinfoView) RoutableIPs() views.Slice[netip.Prefix] { return views.SliceOf(v.ж.RoutableIPs) } +func (v HostinfoView) RequestTags() views.Slice[string] { return views.SliceOf(v.ж.RequestTags) } +func (v HostinfoView) Services() views.Slice[Service] { return views.SliceOf(v.ж.Services) } +func (v HostinfoView) NetInfo() NetInfoView { return v.ж.NetInfo.View() } +func (v HostinfoView) SSH_HostKeys() views.Slice[string] { return views.SliceOf(v.ж.SSH_HostKeys) } +func (v HostinfoView) Cloud() string { return v.ж.Cloud } +func (v HostinfoView) Userspace() opt.Bool { return v.ж.Userspace } +func (v HostinfoView) UserspaceRouter() opt.Bool { return v.ж.UserspaceRouter } func (v HostinfoView) Location() *Location { if v.ж.Location == nil { return nil diff --git a/types/views/views.go b/types/views/views.go index b86c3f94a..30bffcd74 100644 --- a/types/views/views.go +++ b/types/views/views.go @@ -9,10 +9,6 @@ import ( "encoding/json" "errors" "maps" - "net/netip" - "slices" - - "tailscale.com/net/tsaddr" ) func unmarshalSliceFromJSON[T any](b []byte, x *[]T) error { @@ -229,84 +225,6 @@ func SliceEqualAnyOrder[T comparable](a, b Slice[T]) bool { return true } -// IPPrefixSlice is a read-only accessor for a slice of netip.Prefix. -type IPPrefixSlice struct { - ж Slice[netip.Prefix] -} - -// IPPrefixSliceOf returns a IPPrefixSlice for the provided slice. -func IPPrefixSliceOf(x []netip.Prefix) IPPrefixSlice { return IPPrefixSlice{SliceOf(x)} } - -// IsNil reports whether the underlying slice is nil. -func (v IPPrefixSlice) IsNil() bool { return v.ж.IsNil() } - -// Len returns the length of the slice. -func (v IPPrefixSlice) Len() int { return v.ж.Len() } - -// LenIter returns a slice the same length as the v.Len(). -// The caller can then range over it to get the valid indexes. -// It does not allocate. -func (v IPPrefixSlice) LenIter() []struct{} { return make([]struct{}, v.ж.Len()) } - -// At returns the IPPrefix at index `i` of the slice. -func (v IPPrefixSlice) At(i int) netip.Prefix { return v.ж.At(i) } - -// AppendTo appends the underlying slice values to dst. -func (v IPPrefixSlice) AppendTo(dst []netip.Prefix) []netip.Prefix { - return v.ж.AppendTo(dst) -} - -// Unwrap returns the underlying Slice[netip.Prefix]. -func (v IPPrefixSlice) Unwrap() Slice[netip.Prefix] { - return v.ж -} - -// AsSlice returns a copy of underlying slice. -func (v IPPrefixSlice) AsSlice() []netip.Prefix { - return v.ж.AsSlice() -} - -// Filter returns a new slice, containing elements of v that match f. -func (v IPPrefixSlice) Filter(f func(netip.Prefix) bool) []netip.Prefix { - return tsaddr.FilterPrefixesCopy(v.ж.ж, f) -} - -// PrefixesContainsIP reports whether any IPPrefix contains IP. -func (v IPPrefixSlice) ContainsIP(ip netip.Addr) bool { - return tsaddr.PrefixesContainsIP(v.ж.ж, ip) -} - -// PrefixesContainsFunc reports whether f is true for any IPPrefix in the slice. -func (v IPPrefixSlice) ContainsFunc(f func(netip.Prefix) bool) bool { - return slices.ContainsFunc(v.ж.ж, f) -} - -// ContainsExitRoutes reports whether v contains ExitNode Routes. -func (v IPPrefixSlice) ContainsExitRoutes() bool { - return tsaddr.ContainsExitRoutes(v.ж.ж) -} - -// ContainsNonExitSubnetRoutes reports whether v contains Subnet -// Routes other than ExitNode Routes. -func (v IPPrefixSlice) ContainsNonExitSubnetRoutes() bool { - for i := 0; i < v.Len(); i++ { - if v.At(i).Bits() != 0 { - return true - } - } - return false -} - -// MarshalJSON implements json.Marshaler. -func (v IPPrefixSlice) MarshalJSON() ([]byte, error) { - return v.ж.MarshalJSON() -} - -// UnmarshalJSON implements json.Unmarshaler. -func (v *IPPrefixSlice) UnmarshalJSON(b []byte) error { - return v.ж.UnmarshalJSON(b) -} - // MapOf returns a view over m. It is the caller's responsibility to make sure K // and V is immutable, if this is being used to provide a read-only view over m. func MapOf[K comparable, V comparable](m map[K]V) Map[K, V] { diff --git a/types/views/views_test.go b/types/views/views_test.go index c78bd6dde..c04eef62b 100644 --- a/types/views/views_test.go +++ b/types/views/views_test.go @@ -16,10 +16,10 @@ import ( type viewStruct struct { Int int - Addrs IPPrefixSlice + Addrs Slice[netip.Prefix] Strings Slice[string] - AddrsPtr *IPPrefixSlice `json:",omitempty"` - StringsPtr *Slice[string] `json:",omitempty"` + AddrsPtr *Slice[netip.Prefix] `json:",omitempty"` + StringsPtr *Slice[string] `json:",omitempty"` } func BenchmarkSliceIteration(b *testing.B) { @@ -66,7 +66,7 @@ func TestViewsJSON(t *testing.T) { } return } - ipp := IPPrefixSliceOf(mustCIDR("192.168.0.0/24")) + ipp := SliceOf(mustCIDR("192.168.0.0/24")) ss := SliceOf([]string{"bar"}) tests := []struct { name string