summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorsalman <salman@tailscale.com>2023-05-05 16:47:28 +0100
committersalman <salman@tailscale.com>2023-05-06 11:36:42 +0100
commit8e6f564f7e23eded646484e2074c41fdd4c3e171 (patch)
treed671b48831f408f7c9f514ce54878e1464042e56
parent5783adcc6fc469c0791ca339876d6847cfcc1b01 (diff)
downloadtailscale-s/eq.tar.xz
tailscale-s/eq.zip
cmd/equaler: add command to generate Equal() methodss/eq
The generator is still crude and do not cover most types, but it covers all the ones needed by the tailcfg package. It's a start. Fixes #8077. Signed-off-by: salman <salman@tailscale.com>
-rw-r--r--cmd/equaler/equaler.go177
-rw-r--r--tailcfg/tailcfg.go122
-rw-r--r--tailcfg/tailcfg_equal.go244
-rw-r--r--tailcfg/tailcfg_test.go2
-rw-r--r--tailcfg/tailcfg_view.go1
-rw-r--r--util/codegen/codegen.go14
-rw-r--r--wgengine/magicsock/magicsock.go2
7 files changed, 436 insertions, 126 deletions
diff --git a/cmd/equaler/equaler.go b/cmd/equaler/equaler.go
new file mode 100644
index 000000000..27412bb23
--- /dev/null
+++ b/cmd/equaler/equaler.go
@@ -0,0 +1,177 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Equaler is a tool to automate the creation of an Equals method.
+//
+// This tool assumes that if a type you give it contains another named struct
+// type, that type will also have an Equal method, and that all fields are
+// comparable unless explicitly excluded.
+package main
+
+import (
+ "bytes"
+ "flag"
+ "fmt"
+ "go/token"
+ "go/types"
+ "log"
+ "os"
+ "strings"
+
+ "golang.org/x/exp/slices"
+ "tailscale.com/util/codegen"
+)
+
+var (
+ flagTypes = flag.String("type", "", "comma-separated list of types; required")
+ flagBuildTags = flag.String("tags", "", "compiler build tags to apply")
+)
+
+func main() {
+ log.SetFlags(0)
+ log.SetPrefix("equaler: ")
+ flag.Parse()
+ if len(*flagTypes) == 0 {
+ flag.Usage()
+ os.Exit(2)
+ }
+ typeNames := strings.Split(*flagTypes, ",")
+
+ pkg, namedTypes, err := codegen.LoadTypes(*flagBuildTags, ".")
+ if err != nil {
+ log.Fatal(err)
+ }
+ it := codegen.NewImportTracker(pkg.Types)
+ buf := new(bytes.Buffer)
+ for _, typeName := range typeNames {
+ typ, ok := namedTypes[typeName]
+ if !ok {
+ log.Fatalf("could not find type %s", typeName)
+ }
+ gen(buf, it, typ, typeNames)
+ }
+
+ cloneOutput := pkg.Name + "_equal.go"
+ if err := codegen.WritePackageFile("tailscale.com/cmd/equaler", pkg, cloneOutput, it, buf); err != nil {
+ log.Fatal(err)
+ }
+}
+
+func gen(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named, typeNames []string) {
+ t, ok := typ.Underlying().(*types.Struct)
+ if !ok {
+ return
+ }
+
+ name := typ.Obj().Name()
+ fmt.Fprintf(buf, "// Equal reports whether a and b are equal.\n")
+ fmt.Fprintf(buf, "func (a *%s) Equal(b *%s) bool {\n", name, name)
+ writef := func(format string, args ...any) {
+ fmt.Fprintf(buf, "\t"+format+"\n", args...)
+ }
+ writef("if a == b {")
+ writef("\treturn true")
+ writef("}")
+
+ writef("return a != nil && b != nil &&")
+ for i := 0; i < t.NumFields(); i++ {
+ fname := t.Field(i).Name()
+ ft := t.Field(i).Type()
+
+ // Fields which are explicitly ignored are skipped.
+ if codegen.HasNoEqual(t.Tag(i)) {
+ writef("\t// Skipping %s because of codegen:noequal", fname)
+ continue
+ }
+
+ // Fields which are named types that have an Equal() method, get that method used
+ if named, _ := ft.(*types.Named); named != nil {
+ if implementsEqual(ft) || slices.Contains(typeNames, named.Obj().Name()) {
+ writef("\ta.%s.Equal(b.%s) &&", fname, fname)
+ continue
+ }
+ }
+
+ // Fields which are just values are directly compared, unless they have an Equal() method.
+ if !codegen.ContainsPointers(ft) {
+ writef("\ta.%s == b.%s &&", fname, fname)
+ continue
+ }
+
+ switch ft := ft.Underlying().(type) {
+ case *types.Pointer:
+ if named, _ := ft.Elem().(*types.Named); named != nil {
+ if slices.Contains(typeNames, named.Obj().Name()) || implementsEqual(ft) {
+ writef("\t((a.%s == nil) == (b.%s == nil)) && (a.%s == nil || a.%s.Equal(b.%s)) &&", fname, fname, fname, fname, fname)
+ continue
+ }
+ if implementsEqual(ft.Elem()) {
+ writef("\t((a.%s == nil) == (b.%s == nil)) && (a.%s == nil || a.%s.Equal(*b.%s)) &&", fname, fname, fname, fname, fname)
+ continue
+ }
+ }
+ if !codegen.ContainsPointers(ft.Elem()) {
+ writef("\t((a.%s == nil) == (b.%s == nil)) && (a.%s == nil || *a.%s == *b.%s) &&", fname, fname, fname, fname, fname)
+ continue
+ }
+ log.Fatalf("unimplemented: %s (%T)", fname, ft)
+ case *types.Slice:
+ // Empty slices and nil slices are different.
+ writef("\t((a.%s == nil) == (b.%s == nil)) &&", fname, fname)
+ if named, _ := ft.Elem().(*types.Named); named != nil {
+ if implementsEqual(ft.Elem()) {
+ it.Import("golang.org/x/exp/slices")
+ writef("\tslices.EqualFunc(a.%s, b.%s, func(aa %s, bb %s) bool {return aa.Equal(bb)}) &&", fname, fname, named.Obj().Name(), named.Obj().Name())
+ continue
+ }
+ if slices.Contains(typeNames, named.Obj().Name()) || implementsEqual(types.NewPointer(ft.Elem())) {
+ it.Import("golang.org/x/exp/slices")
+ writef("\tslices.EqualFunc(a.%s, b.%s, func(aa %s, bb %s) bool {return aa.Equal(&bb)}) &&", fname, fname, named.Obj().Name(), named.Obj().Name())
+ continue
+ }
+ }
+ if !codegen.ContainsPointers(ft.Elem()) {
+ it.Import("golang.org/x/exp/slices")
+ writef("\tslices.Equal(a.%s, b.%s) &&", fname, fname)
+ continue
+ }
+ log.Fatalf("unimplemented: %s (%T)", fname, ft)
+ case *types.Map:
+ if !codegen.ContainsPointers(ft.Elem()) {
+ it.Import("golang.org/x/exp/maps")
+ writef("\tmaps.Equal(a.%s, b.%s) &&", fname, fname)
+ continue
+ }
+ log.Fatalf("unimplemented: %s (%T)", fname, ft)
+ default:
+ log.Fatalf("unimplemented: %s (%T)", fname, ft)
+ }
+ }
+ writef("\ttrue")
+ fmt.Fprintf(buf, "}\n\n")
+
+ buf.Write(codegen.AssertStructUnchanged(t, name, "Equal", it))
+}
+
+// hasBasicUnderlying reports true when typ.Underlying() is a slice or a map.
+func hasBasicUnderlying(typ types.Type) bool {
+ switch typ.Underlying().(type) {
+ case *types.Slice, *types.Map:
+ return true
+ default:
+ return false
+ }
+}
+
+// implementsEqual reports whether typ has an Equal(typ) bool method.
+func implementsEqual(typ types.Type) bool {
+ return types.Implements(typ, types.NewInterfaceType(
+ []*types.Func{types.NewFunc(
+ token.NoPos, nil, "Equal", types.NewSignatureType(
+ types.NewVar(token.NoPos, nil, "a", typ),
+ nil, nil,
+ types.NewTuple(types.NewVar(token.NoPos, nil, "b", typ)),
+ types.NewTuple(types.NewVar(token.NoPos, nil, "", types.Typ[types.Bool])), false))},
+ []types.Type{},
+ ))
+}
diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go
index b77193f2b..6efd17f80 100644
--- a/tailcfg/tailcfg.go
+++ b/tailcfg/tailcfg.go
@@ -5,8 +5,9 @@ package tailcfg
//go:generate go run tailscale.com/cmd/viewer --type=User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse,DERPRegion,DERPMap,DERPNode,SSHRule,SSHAction,SSHPrincipal,ControlDialPlan --clonefunc
+//go:generage go run tailscale.com/cmd/equaler --type Node,Hostinfo,NetInfo,Service
+
import (
- "bytes"
"encoding/hex"
"errors"
"fmt"
@@ -497,7 +498,7 @@ const (
// Service represents a service running on a node.
type Service struct {
- _ structs.Incomparable
+ _ structs.Incomparable `codegen:"noequal"`
// Proto is the type of service. It's usually the constant TCP
// or UDP ("tcp" or "udp"), but it can also be one of the
@@ -582,9 +583,6 @@ type Hostinfo struct {
Cloud string `json:",omitempty"`
Userspace opt.Bool `json:",omitempty"` // if the client is running in userspace (netstack) mode
UserspaceRouter opt.Bool `json:",omitempty"` // if the client's subnet router is running in userspace (netstack) mode
-
- // NOTE: any new fields containing pointers in this type
- // require changes to Hostinfo.Equal.
}
// TailscaleSSHEnabled reports whether or not this node is acting as a
@@ -664,9 +662,7 @@ type NetInfo struct {
// This should only be updated rarely, or when there's a
// material change, as any change here also gets uploaded to
// the control plane.
- DERPLatency map[string]float64 `json:",omitempty"`
-
- // Update BasicallyEqual when adding fields.
+ DERPLatency map[string]float64 `json:",omitempty" codegen:"noequal"`
}
func (ni *NetInfo) String() string {
@@ -704,40 +700,6 @@ func conciseOptBool(b opt.Bool, trueVal string) string {
return ""
}
-// BasicallyEqual reports whether ni and ni2 are basically equal, ignoring
-// changes in DERP ServerLatency & RegionLatency.
-func (ni *NetInfo) BasicallyEqual(ni2 *NetInfo) bool {
- if (ni == nil) != (ni2 == nil) {
- return false
- }
- if ni == nil {
- return true
- }
- return ni.MappingVariesByDestIP == ni2.MappingVariesByDestIP &&
- ni.HairPinning == ni2.HairPinning &&
- ni.WorkingIPv6 == ni2.WorkingIPv6 &&
- ni.OSHasIPv6 == ni2.OSHasIPv6 &&
- ni.WorkingUDP == ni2.WorkingUDP &&
- ni.WorkingICMPv4 == ni2.WorkingICMPv4 &&
- ni.HavePortMap == ni2.HavePortMap &&
- ni.UPnP == ni2.UPnP &&
- ni.PMP == ni2.PMP &&
- ni.PCP == ni2.PCP &&
- ni.PreferredDERP == ni2.PreferredDERP &&
- ni.LinkType == ni2.LinkType
-}
-
-// Equal reports whether h and h2 are equal.
-func (h *Hostinfo) Equal(h2 *Hostinfo) bool {
- if h == nil && h2 == nil {
- return true
- }
- if (h == nil) != (h2 == nil) {
- return false
- }
- return reflect.DeepEqual(h, h2)
-}
-
// HowUnequal returns a list of paths through Hostinfo where h and h2 differ.
// If they differ in nil-ness, the path is "nil", otherwise the path is like
// "ShieldsUp" or "NetInfo.nil" or "NetInfo.PCP".
@@ -1689,82 +1651,6 @@ func (id UserID) String() string { return fmt.Sprintf("userid:%x", int64(id)) }
func (id LoginID) String() string { return fmt.Sprintf("loginid:%x", int64(id)) }
func (id NodeID) String() string { return fmt.Sprintf("nodeid:%x", int64(id)) }
-// Equal reports whether n and n2 are equal.
-func (n *Node) Equal(n2 *Node) bool {
- if n == nil && n2 == nil {
- return true
- }
- return n != nil && n2 != nil &&
- n.ID == n2.ID &&
- n.StableID == n2.StableID &&
- n.Name == n2.Name &&
- n.User == n2.User &&
- n.Sharer == n2.Sharer &&
- n.UnsignedPeerAPIOnly == n2.UnsignedPeerAPIOnly &&
- n.Key == n2.Key &&
- n.KeyExpiry.Equal(n2.KeyExpiry) &&
- bytes.Equal(n.KeySignature, n2.KeySignature) &&
- n.Machine == n2.Machine &&
- n.DiscoKey == n2.DiscoKey &&
- eqPtr(n.Online, n2.Online) &&
- eqCIDRs(n.Addresses, n2.Addresses) &&
- eqCIDRs(n.AllowedIPs, n2.AllowedIPs) &&
- eqCIDRs(n.PrimaryRoutes, n2.PrimaryRoutes) &&
- eqStrings(n.Endpoints, n2.Endpoints) &&
- n.DERP == n2.DERP &&
- n.Cap == n2.Cap &&
- n.Hostinfo.Equal(n2.Hostinfo) &&
- n.Created.Equal(n2.Created) &&
- eqTimePtr(n.LastSeen, n2.LastSeen) &&
- n.MachineAuthorized == n2.MachineAuthorized &&
- eqStrings(n.Capabilities, n2.Capabilities) &&
- n.ComputedName == n2.ComputedName &&
- n.computedHostIfDifferent == n2.computedHostIfDifferent &&
- n.ComputedNameWithHost == n2.ComputedNameWithHost &&
- eqStrings(n.Tags, n2.Tags) &&
- n.Expired == n2.Expired &&
- eqPtr(n.SelfNodeV4MasqAddrForThisPeer, n2.SelfNodeV4MasqAddrForThisPeer) &&
- n.IsWireGuardOnly == n2.IsWireGuardOnly
-}
-
-func eqPtr[T comparable](a, b *T) bool {
- if a == b { // covers nil
- return true
- }
- if a == nil || b == nil {
- return false
- }
- return *a == *b
-}
-
-func eqStrings(a, b []string) bool {
- if len(a) != len(b) || ((a == nil) != (b == nil)) {
- return false
- }
- for i, v := range a {
- if v != b[i] {
- return false
- }
- }
- return true
-}
-
-func eqCIDRs(a, b []netip.Prefix) bool {
- if len(a) != len(b) || ((a == nil) != (b == nil)) {
- return false
- }
- for i, v := range a {
- if v != b[i] {
- return false
- }
- }
- return true
-}
-
-func eqTimePtr(a, b *time.Time) bool {
- return ((a == nil) == (b == nil)) && (a == nil || a.Equal(*b))
-}
-
// Oauth2Token is a copy of golang.org/x/oauth2.Token, to avoid the
// go.mod dependency on App Engine and grpc, which was causing problems.
// All we actually needed was this struct on the client side.
diff --git a/tailcfg/tailcfg_equal.go b/tailcfg/tailcfg_equal.go
new file mode 100644
index 000000000..c01454890
--- /dev/null
+++ b/tailcfg/tailcfg_equal.go
@@ -0,0 +1,244 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Code generated by tailscale.com/cmd/equaler; DO NOT EDIT.
+
+package tailcfg
+
+import (
+ "net/netip"
+ "time"
+
+ "golang.org/x/exp/slices"
+ "tailscale.com/types/key"
+ "tailscale.com/types/opt"
+ "tailscale.com/types/structs"
+ "tailscale.com/types/tkatype"
+)
+
+// Equal reports whether a and b are equal.
+func (a *Node) Equal(b *Node) bool {
+ if a == b {
+ return true
+ }
+ return a != nil && b != nil &&
+ a.ID == b.ID &&
+ a.StableID == b.StableID &&
+ a.Name == b.Name &&
+ a.User == b.User &&
+ a.Sharer == b.Sharer &&
+ a.Key == b.Key &&
+ a.KeyExpiry.Equal(b.KeyExpiry) &&
+ ((a.KeySignature == nil) == (b.KeySignature == nil)) &&
+ slices.Equal(a.KeySignature, b.KeySignature) &&
+ a.Machine == b.Machine &&
+ a.DiscoKey == b.DiscoKey &&
+ ((a.Addresses == nil) == (b.Addresses == nil)) &&
+ slices.Equal(a.Addresses, b.Addresses) &&
+ ((a.AllowedIPs == nil) == (b.AllowedIPs == nil)) &&
+ slices.Equal(a.AllowedIPs, b.AllowedIPs) &&
+ ((a.Endpoints == nil) == (b.Endpoints == nil)) &&
+ slices.Equal(a.Endpoints, b.Endpoints) &&
+ a.DERP == b.DERP &&
+ a.Hostinfo.Equal(b.Hostinfo) &&
+ a.Created.Equal(b.Created) &&
+ a.Cap == b.Cap &&
+ ((a.Tags == nil) == (b.Tags == nil)) &&
+ slices.Equal(a.Tags, b.Tags) &&
+ ((a.PrimaryRoutes == nil) == (b.PrimaryRoutes == nil)) &&
+ slices.Equal(a.PrimaryRoutes, b.PrimaryRoutes) &&
+ ((a.LastSeen == nil) == (b.LastSeen == nil)) && (a.LastSeen == nil || a.LastSeen.Equal(*b.LastSeen)) &&
+ ((a.Online == nil) == (b.Online == nil)) && (a.Online == nil || *a.Online == *b.Online) &&
+ a.KeepAlive == b.KeepAlive &&
+ a.MachineAuthorized == b.MachineAuthorized &&
+ ((a.Capabilities == nil) == (b.Capabilities == nil)) &&
+ slices.Equal(a.Capabilities, b.Capabilities) &&
+ a.UnsignedPeerAPIOnly == b.UnsignedPeerAPIOnly &&
+ a.ComputedName == b.ComputedName &&
+ a.computedHostIfDifferent == b.computedHostIfDifferent &&
+ a.ComputedNameWithHost == b.ComputedNameWithHost &&
+ a.DataPlaneAuditLogID == b.DataPlaneAuditLogID &&
+ a.Expired == b.Expired &&
+ ((a.SelfNodeV4MasqAddrForThisPeer == nil) == (b.SelfNodeV4MasqAddrForThisPeer == nil)) && (a.SelfNodeV4MasqAddrForThisPeer == nil || *a.SelfNodeV4MasqAddrForThisPeer == *b.SelfNodeV4MasqAddrForThisPeer) &&
+ a.IsWireGuardOnly == b.IsWireGuardOnly &&
+ true
+}
+
+// A compilation failure here means this code must be regenerated, with the command at the top of this file.
+var _NodeEqualNeedsRegeneration = Node(struct {
+ ID NodeID
+ StableID StableNodeID
+ Name string
+ User UserID
+ Sharer UserID
+ Key key.NodePublic
+ KeyExpiry time.Time
+ KeySignature tkatype.MarshaledSignature
+ Machine key.MachinePublic
+ DiscoKey key.DiscoPublic
+ Addresses []netip.Prefix
+ AllowedIPs []netip.Prefix
+ Endpoints []string
+ DERP string
+ Hostinfo HostinfoView
+ Created time.Time
+ Cap CapabilityVersion
+ Tags []string
+ PrimaryRoutes []netip.Prefix
+ LastSeen *time.Time
+ Online *bool
+ KeepAlive bool
+ MachineAuthorized bool
+ Capabilities []string
+ UnsignedPeerAPIOnly bool
+ ComputedName string
+ computedHostIfDifferent string
+ ComputedNameWithHost string
+ DataPlaneAuditLogID string
+ Expired bool
+ SelfNodeV4MasqAddrForThisPeer *netip.Addr
+ IsWireGuardOnly bool
+}{})
+
+// Equal reports whether a and b are equal.
+func (a *Hostinfo) Equal(b *Hostinfo) bool {
+ if a == b {
+ return true
+ }
+ return a != nil && b != nil &&
+ a.IPNVersion == b.IPNVersion &&
+ a.FrontendLogID == b.FrontendLogID &&
+ a.BackendLogID == b.BackendLogID &&
+ a.OS == b.OS &&
+ a.OSVersion == b.OSVersion &&
+ a.Container == b.Container &&
+ a.Env == b.Env &&
+ a.Distro == b.Distro &&
+ a.DistroVersion == b.DistroVersion &&
+ a.DistroCodeName == b.DistroCodeName &&
+ a.App == b.App &&
+ a.Desktop == b.Desktop &&
+ a.Package == b.Package &&
+ a.DeviceModel == b.DeviceModel &&
+ a.PushDeviceToken == b.PushDeviceToken &&
+ a.Hostname == b.Hostname &&
+ a.ShieldsUp == b.ShieldsUp &&
+ a.ShareeNode == b.ShareeNode &&
+ a.NoLogsNoSupport == b.NoLogsNoSupport &&
+ a.WireIngress == b.WireIngress &&
+ a.AllowsUpdate == b.AllowsUpdate &&
+ a.Machine == b.Machine &&
+ a.GoArch == b.GoArch &&
+ a.GoArchVar == b.GoArchVar &&
+ a.GoVersion == b.GoVersion &&
+ ((a.RoutableIPs == nil) == (b.RoutableIPs == nil)) &&
+ slices.Equal(a.RoutableIPs, b.RoutableIPs) &&
+ ((a.RequestTags == nil) == (b.RequestTags == nil)) &&
+ slices.Equal(a.RequestTags, b.RequestTags) &&
+ ((a.Services == nil) == (b.Services == nil)) &&
+ slices.EqualFunc(a.Services, b.Services, func(aa Service, bb Service) bool { return aa.Equal(&bb) }) &&
+ ((a.NetInfo == nil) == (b.NetInfo == nil)) && (a.NetInfo == nil || a.NetInfo.Equal(b.NetInfo)) &&
+ ((a.SSH_HostKeys == nil) == (b.SSH_HostKeys == nil)) &&
+ slices.Equal(a.SSH_HostKeys, b.SSH_HostKeys) &&
+ a.Cloud == b.Cloud &&
+ a.Userspace == b.Userspace &&
+ a.UserspaceRouter == b.UserspaceRouter &&
+ true
+}
+
+// A compilation failure here means this code must be regenerated, with the command at the top of this file.
+var _HostinfoEqualNeedsRegeneration = Hostinfo(struct {
+ IPNVersion string
+ FrontendLogID string
+ BackendLogID string
+ OS string
+ OSVersion string
+ Container opt.Bool
+ Env string
+ Distro string
+ DistroVersion string
+ DistroCodeName string
+ App string
+ Desktop opt.Bool
+ Package string
+ DeviceModel string
+ PushDeviceToken string
+ Hostname string
+ ShieldsUp bool
+ ShareeNode bool
+ NoLogsNoSupport bool
+ WireIngress bool
+ AllowsUpdate bool
+ Machine string
+ GoArch string
+ GoArchVar string
+ GoVersion string
+ RoutableIPs []netip.Prefix
+ RequestTags []string
+ Services []Service
+ NetInfo *NetInfo
+ SSH_HostKeys []string
+ Cloud string
+ Userspace opt.Bool
+ UserspaceRouter opt.Bool
+}{})
+
+// Equal reports whether a and b are equal.
+func (a *NetInfo) Equal(b *NetInfo) bool {
+ if a == b {
+ return true
+ }
+ return a != nil && b != nil &&
+ a.MappingVariesByDestIP == b.MappingVariesByDestIP &&
+ a.HairPinning == b.HairPinning &&
+ a.WorkingIPv6 == b.WorkingIPv6 &&
+ a.OSHasIPv6 == b.OSHasIPv6 &&
+ a.WorkingUDP == b.WorkingUDP &&
+ a.WorkingICMPv4 == b.WorkingICMPv4 &&
+ a.HavePortMap == b.HavePortMap &&
+ a.UPnP == b.UPnP &&
+ a.PMP == b.PMP &&
+ a.PCP == b.PCP &&
+ a.PreferredDERP == b.PreferredDERP &&
+ a.LinkType == b.LinkType &&
+ // Skipping DERPLatency because of codegen:noequal
+ true
+}
+
+// A compilation failure here means this code must be regenerated, with the command at the top of this file.
+var _NetInfoEqualNeedsRegeneration = NetInfo(struct {
+ MappingVariesByDestIP opt.Bool
+ HairPinning opt.Bool
+ WorkingIPv6 opt.Bool
+ OSHasIPv6 opt.Bool
+ WorkingUDP opt.Bool
+ WorkingICMPv4 opt.Bool
+ HavePortMap bool
+ UPnP opt.Bool
+ PMP opt.Bool
+ PCP opt.Bool
+ PreferredDERP int
+ LinkType string
+ DERPLatency map[string]float64
+}{})
+
+// Equal reports whether a and b are equal.
+func (a *Service) Equal(b *Service) bool {
+ if a == b {
+ return true
+ }
+ return a != nil && b != nil &&
+ // Skipping _ because of codegen:noequal
+ a.Proto == b.Proto &&
+ a.Port == b.Port &&
+ a.Description == b.Description &&
+ true
+}
+
+// A compilation failure here means this code must be regenerated, with the command at the top of this file.
+var _ServiceEqualNeedsRegeneration = Service(struct {
+ _ structs.Incomparable
+ Proto ServiceProto
+ Port uint16
+ Description string
+}{})
diff --git a/tailcfg/tailcfg_test.go b/tailcfg/tailcfg_test.go
index b0e3f982e..bc94e9843 100644
--- a/tailcfg/tailcfg_test.go
+++ b/tailcfg/tailcfg_test.go
@@ -572,7 +572,7 @@ func TestNetInfoFields(t *testing.T) {
"DERPLatency",
}
if have := fieldsOf(reflect.TypeOf(NetInfo{})); !reflect.DeepEqual(have, handled) {
- t.Errorf("NetInfo.Clone/BasicallyEqually check might be out of sync\nfields: %q\nhandled: %q\n",
+ t.Errorf("NetInfo.Clone/Equal check might be out of sync\nfields: %q\nhandled: %q\n",
have, handled)
}
}
diff --git a/tailcfg/tailcfg_view.go b/tailcfg/tailcfg_view.go
index b4ff3b738..957605255 100644
--- a/tailcfg/tailcfg_view.go
+++ b/tailcfg/tailcfg_view.go
@@ -402,6 +402,7 @@ func (v NetInfoView) LinkType() string { return v.ж.LinkType }
func (v NetInfoView) DERPLatency() views.Map[string, float64] { return views.MapOf(v.ж.DERPLatency) }
func (v NetInfoView) String() string { return v.ж.String() }
+func (v NetInfoView) Equal(v2 NetInfoView) bool { return v.ж.Equal(v2.ж) }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _NetInfoViewNeedsRegeneration = NetInfo(struct {
diff --git a/util/codegen/codegen.go b/util/codegen/codegen.go
index b1e012fad..277d39097 100644
--- a/util/codegen/codegen.go
+++ b/util/codegen/codegen.go
@@ -16,6 +16,7 @@ import (
"reflect"
"strings"
+ "golang.org/x/exp/slices"
"golang.org/x/tools/go/packages"
"golang.org/x/tools/imports"
"tailscale.com/util/mak"
@@ -47,12 +48,13 @@ func LoadTypes(buildTags string, pkgName string) (*packages.Package, map[string]
// HasNoClone reports whether the provided tag has `codegen:noclone`.
func HasNoClone(structTag string) bool {
val := reflect.StructTag(structTag).Get("codegen")
- for _, v := range strings.Split(val, ",") {
- if v == "noclone" {
- return true
- }
- }
- return false
+ return slices.Contains(strings.Split(val, ","), "noclone")
+}
+
+// HasNoEqual reports whether the provided tag has `codegen:noequal`.
+func HasNoEqual(structTag string) bool {
+ val := reflect.StructTag(structTag).Get("codegen")
+ return slices.Contains(strings.Split(val, ","), "noequal")
}
const copyrightHeader = `// Copyright (c) Tailscale Inc & AUTHORS
diff --git a/wgengine/magicsock/magicsock.go b/wgengine/magicsock/magicsock.go
index 3223441c4..e1a6a68db 100644
--- a/wgengine/magicsock/magicsock.go
+++ b/wgengine/magicsock/magicsock.go
@@ -951,7 +951,7 @@ func (c *Conn) pickDERPFallback() int {
func (c *Conn) callNetInfoCallback(ni *tailcfg.NetInfo) {
c.mu.Lock()
defer c.mu.Unlock()
- if ni.BasicallyEqual(c.netInfoLast) {
+ if ni.Equal(c.netInfoLast) {
return
}
c.callNetInfoCallbackLocked(ni)