diff --git a/util/deephash/deephash.go b/util/deephash/deephash.go index e652d2665..87fde2819 100644 --- a/util/deephash/deephash.go +++ b/util/deephash/deephash.go @@ -14,9 +14,7 @@ // - time.Time are compared based on whether they are the same instant in time // and also in the same zone offset. Monotonic measurements and zone names // are ignored as part of the hash. -// - Types which implement interface { AppendTo([]byte) []byte } use -// the AppendTo method to produce a textual representation of the value. -// Thus, two values are equal if AppendTo produces the same bytes. +// - netip.Addr are compared based on a shallow comparison of the struct. // // WARNING: This package, like most of the tailscale.com Go module, // should be considered Tailscale-internal; we make no API promises. @@ -29,6 +27,7 @@ import ( "fmt" "log" "math" + "net/netip" "reflect" "sync" "time" @@ -210,10 +209,7 @@ type appenderTo interface { AppendTo([]byte) []byte } -var ( - uint8Type = reflect.TypeOf(byte(0)) - timeTimeType = reflect.TypeOf(time.Time{}) -) +var uint8Type = reflect.TypeOf(byte(0)) // typeInfo describes properties of a type. // @@ -302,36 +298,6 @@ func (h *hasher) hashInt64v(v addressableValue) bool { return true } -func hashStructAppenderTo(h *hasher, v addressableValue) bool { - if !v.CanInterface() { - return false // slow path - } - a := v.Addr().Interface().(appenderTo) - size := h.scratch[:8] - record := a.AppendTo(size) - binary.LittleEndian.PutUint64(record, uint64(len(record)-len(size))) - h.HashBytes(record) - return true -} - -// hashPointerAppenderTo hashes v, a reflect.Ptr, that implements appenderTo. -func hashPointerAppenderTo(h *hasher, v addressableValue) bool { - if !v.CanInterface() { - return false // slow path - } - if v.IsNil() { - h.HashUint8(0) // indicates nil - return true - } - h.HashUint8(1) // indicates visiting a pointer - a := v.Interface().(appenderTo) - size := h.scratch[:8] - record := a.AppendTo(size) - binary.LittleEndian.PutUint64(record, uint64(len(record)-len(size))) - h.HashBytes(record) - return true -} - // fieldInfo describes a struct field. type fieldInfo struct { index int // index of field for reflect.Value.Field(n); -1 if invalid @@ -462,21 +428,19 @@ func genTypeHasher(t reflect.Type) typeHasherFunc { eti := getTypeInfo(et) return genHashArray(t, eti) case reflect.Struct: - if t == timeTimeType { + switch t { + case timeTimeType: return (*hasher).hashTimev + case netipAddrType: + return (*hasher).hashAddrv + default: + return genHashStructFields(t) } - if t.Implements(appenderToType) { - return hashStructAppenderTo - } - return genHashStructFields(t) case reflect.Pointer: et := t.Elem() if typeIsMemHashable(et) { return genHashPtrToMemoryRange(et) } - if t.Implements(appenderToType) { - return hashPointerAppenderTo - } if !typeIsRecursive(t) { eti := getTypeInfo(et) return func(h *hasher, v addressableValue) bool { @@ -544,6 +508,30 @@ func (h *hasher) hashTimev(v addressableValue) bool { return true } +// hashAddrv hashes v, of type netip.Addr. +func (h *hasher) hashAddrv(v addressableValue) bool { + // The formatting of netip.Addr covers the + // IP version, the address, and the optional zone name (for v6). + // This is equivalent to a1.MarshalBinary() == a2.MarshalBinary(). + ip := *(*netip.Addr)(v.Addr().UnsafePointer()) + switch { + case !ip.IsValid(): + h.HashUint64(0) + case ip.Is4(): + b := ip.As4() + h.HashUint64(4) + h.HashUint32(binary.LittleEndian.Uint32(b[:])) + case ip.Is6(): + b := ip.As16() + z := ip.Zone() + h.HashUint64(16 + uint64(len(z))) + h.HashUint64(binary.LittleEndian.Uint64(b[:8])) + h.HashUint64(binary.LittleEndian.Uint64(b[8:])) + h.HashString(z) + } + return true +} + // hashSliceMem hashes v, of kind Slice, with a memhash-able element type. func (h *hasher) hashSliceMem(v addressableValue) bool { vLen := v.Len() diff --git a/util/deephash/deephash_test.go b/util/deephash/deephash_test.go index 23f436e4d..b89ef6971 100644 --- a/util/deephash/deephash_test.go +++ b/util/deephash/deephash_test.go @@ -20,6 +20,7 @@ import ( "time" "go4.org/mem" + "go4.org/netipx" "tailscale.com/tailcfg" "tailscale.com/types/dnstype" "tailscale.com/types/ipproto" @@ -116,6 +117,44 @@ func TestHash(t *testing.T) { }(), wantEq: false, }, + {in: tuple{netip.Addr{}, netip.Addr{}}, wantEq: true}, + {in: tuple{netip.Addr{}, netip.AddrFrom4([4]byte{})}, wantEq: false}, + {in: tuple{netip.AddrFrom4([4]byte{}), netip.AddrFrom4([4]byte{})}, wantEq: true}, + {in: tuple{netip.AddrFrom4([4]byte{192, 168, 0, 1}), netip.AddrFrom4([4]byte{192, 168, 0, 1})}, wantEq: true}, + {in: tuple{netip.AddrFrom4([4]byte{192, 168, 0, 1}), netip.AddrFrom4([4]byte{192, 168, 0, 2})}, wantEq: false}, + {in: tuple{netip.AddrFrom4([4]byte{}), netip.AddrFrom16([16]byte{})}, wantEq: false}, + {in: tuple{netip.AddrFrom16([16]byte{}), netip.AddrFrom16([16]byte{})}, wantEq: true}, + {in: tuple{netip.AddrPort{}, netip.AddrPort{}}, wantEq: true}, + {in: tuple{netip.AddrPort{}, netip.AddrPortFrom(netip.AddrFrom4([4]byte{}), 0)}, wantEq: false}, + {in: tuple{netip.AddrPortFrom(netip.AddrFrom4([4]byte{}), 0), netip.AddrPortFrom(netip.AddrFrom4([4]byte{}), 0)}, wantEq: true}, + {in: tuple{netip.AddrPortFrom(netip.AddrFrom4([4]byte{192, 168, 0, 1}), 1234), netip.AddrPortFrom(netip.AddrFrom4([4]byte{192, 168, 0, 1}), 1234)}, wantEq: true}, + {in: tuple{netip.AddrPortFrom(netip.AddrFrom4([4]byte{192, 168, 0, 1}), 1234), netip.AddrPortFrom(netip.AddrFrom4([4]byte{192, 168, 0, 1}), 1235)}, wantEq: false}, + {in: tuple{netip.AddrPortFrom(netip.AddrFrom4([4]byte{192, 168, 0, 1}), 1234), netip.AddrPortFrom(netip.AddrFrom4([4]byte{192, 168, 0, 2}), 1234)}, wantEq: false}, + {in: tuple{netip.Prefix{}, netip.Prefix{}}, wantEq: true}, + {in: tuple{netip.Prefix{}, netip.PrefixFrom(netip.Addr{}, 1)}, wantEq: false}, + {in: tuple{netip.Prefix{}, netip.PrefixFrom(netip.AddrFrom4([4]byte{}), 0)}, wantEq: false}, + {in: tuple{netip.PrefixFrom(netip.AddrFrom4([4]byte{}), 1), netip.PrefixFrom(netip.AddrFrom4([4]byte{}), 1)}, wantEq: true}, + {in: tuple{netip.PrefixFrom(netip.AddrFrom4([4]byte{192, 168, 0, 1}), 1), netip.PrefixFrom(netip.AddrFrom4([4]byte{192, 168, 0, 1}), 1)}, wantEq: true}, + {in: tuple{netip.PrefixFrom(netip.AddrFrom4([4]byte{192, 168, 0, 1}), 1), netip.PrefixFrom(netip.AddrFrom4([4]byte{192, 168, 0, 1}), 0)}, wantEq: false}, + {in: tuple{netip.PrefixFrom(netip.AddrFrom4([4]byte{192, 168, 0, 1}), 1), netip.PrefixFrom(netip.AddrFrom4([4]byte{192, 168, 0, 2}), 1)}, wantEq: false}, + {in: tuple{netipx.IPRange{}, netipx.IPRange{}}, wantEq: true}, + {in: tuple{netipx.IPRange{}, netipx.IPRangeFrom(netip.AddrFrom4([4]byte{}), netip.AddrFrom16([16]byte{}))}, wantEq: false}, + {in: tuple{netipx.IPRangeFrom(netip.AddrFrom4([4]byte{}), netip.AddrFrom16([16]byte{})), netipx.IPRangeFrom(netip.AddrFrom4([4]byte{}), netip.AddrFrom16([16]byte{}))}, wantEq: true}, + {in: tuple{netipx.IPRangeFrom(netip.AddrFrom4([4]byte{192, 168, 0, 1}), netip.AddrFrom4([4]byte{192, 168, 0, 100})), netipx.IPRangeFrom(netip.AddrFrom4([4]byte{192, 168, 0, 1}), netip.AddrFrom4([4]byte{192, 168, 0, 100}))}, wantEq: true}, + {in: tuple{netipx.IPRangeFrom(netip.AddrFrom4([4]byte{192, 168, 0, 1}), netip.AddrFrom4([4]byte{192, 168, 0, 100})), netipx.IPRangeFrom(netip.AddrFrom4([4]byte{192, 168, 0, 1}), netip.AddrFrom4([4]byte{192, 168, 0, 101}))}, wantEq: false}, + {in: tuple{netipx.IPRangeFrom(netip.AddrFrom4([4]byte{192, 168, 0, 1}), netip.AddrFrom4([4]byte{192, 168, 0, 100})), netipx.IPRangeFrom(netip.AddrFrom4([4]byte{192, 168, 0, 2}), netip.AddrFrom4([4]byte{192, 168, 0, 100}))}, wantEq: false}, + {in: tuple{key.DiscoPublic{}, key.DiscoPublic{}}, wantEq: true}, + {in: tuple{key.DiscoPublic{}, key.DiscoPublicFromRaw32(mem.B(func() []byte { + b := make([]byte, 32) + b[0] = 1 + return b + }()))}, wantEq: false}, + {in: tuple{key.NodePublic{}, key.NodePublic{}}, wantEq: true}, + {in: tuple{key.NodePublic{}, key.NodePublicFromRaw32(mem.B(func() []byte { + b := make([]byte, 32) + b[0] = 1 + return b + }()))}, wantEq: false}, } for _, tt := range tests { @@ -405,18 +444,18 @@ func TestGetTypeHasher(t *testing.T) { { name: "packet_filter", val: filterRules, - out: "\x04\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00*\v\x00\x00\x00\x00\x00\x00\x0010.1.3.4/32\v\x00\x00\x00\x00\x00\x00\x0010.0.0.0/24\x03\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\n\x00\x00\x00\x00\x00\x00\x001.2.3.4/32\x01 \x00\x00\x00\x00\x00\x00\x00\x01\x00\x02\x00\x04\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\n\x00\x00\x00\x00\x00\x00\x001.2.3.4/32\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00foo\x01\x00\x00\x00\x00\x00\x00\x00\v\x00\x00\x00\x00\x00\x00\x00foooooooooo\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\f\x00\x00\x00\x00\x00\x00\x00baaaaaarrrrr\x00\x01\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\v\x00\x00\x00\x00\x00\x00\x00foooooooooo\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\f\x00\x00\x00\x00\x00\x00\x00baaaaaarrrrr\x00\x01\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\v\x00\x00\x00\x00\x00\x00\x00foooooooooo\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\f\x00\x00\x00\x00\x00\x00\x00baaaaaarrrrr\x00\x01\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", - out32: "\x04\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00*\v\x00\x00\x00\x00\x00\x00\x0010.1.3.4/32\v\x00\x00\x00\x00\x00\x00\x0010.0.0.0/24\x03\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x02\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\n\x00\x00\x00\x00\x00\x00\x001.2.3.4/32\x01 \x00\x00\x00\x01\x00\x02\x00\x04\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x02\x00\x00\x00\x03\x00\x00\x00\x04\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\n\x00\x00\x00\x00\x00\x00\x001.2.3.4/32\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00foo\x01\x00\x00\x00\x00\x00\x00\x00\v\x00\x00\x00\x00\x00\x00\x00foooooooooo\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\f\x00\x00\x00\x00\x00\x00\x00baaaaaarrrrr\x00\x01\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\v\x00\x00\x00\x00\x00\x00\x00foooooooooo\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\f\x00\x00\x00\x00\x00\x00\x00baaaaaarrrrr\x00\x01\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\v\x00\x00\x00\x00\x00\x00\x00foooooooooo\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\f\x00\x00\x00\x00\x00\x00\x00baaaaaarrrrr\x00\x01\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + out: "\x04\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00*\v\x00\x00\x00\x00\x00\x00\x0010.1.3.4/32\v\x00\x00\x00\x00\x00\x00\x0010.0.0.0/24\x03\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\n\x00\x00\x00\x00\x00\x00\x001.2.3.4/32\x01 \x00\x00\x00\x00\x00\x00\x00\x01\x00\x02\x00\x04\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x01\x02\x03\x04 \x00\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00foo\x01\x00\x00\x00\x00\x00\x00\x00\v\x00\x00\x00\x00\x00\x00\x00foooooooooo\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\f\x00\x00\x00\x00\x00\x00\x00baaaaaarrrrr\x00\x01\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\v\x00\x00\x00\x00\x00\x00\x00foooooooooo\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\f\x00\x00\x00\x00\x00\x00\x00baaaaaarrrrr\x00\x01\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\v\x00\x00\x00\x00\x00\x00\x00foooooooooo\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\f\x00\x00\x00\x00\x00\x00\x00baaaaaarrrrr\x00\x01\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + out32: "\x04\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00*\v\x00\x00\x00\x00\x00\x00\x0010.1.3.4/32\v\x00\x00\x00\x00\x00\x00\x0010.0.0.0/24\x03\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x02\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\n\x00\x00\x00\x00\x00\x00\x001.2.3.4/32\x01 \x00\x00\x00\x01\x00\x02\x00\x04\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x02\x00\x00\x00\x03\x00\x00\x00\x04\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x01\x02\x03\x04 \x00\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00foo\x01\x00\x00\x00\x00\x00\x00\x00\v\x00\x00\x00\x00\x00\x00\x00foooooooooo\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\f\x00\x00\x00\x00\x00\x00\x00baaaaaarrrrr\x00\x01\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\v\x00\x00\x00\x00\x00\x00\x00foooooooooo\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\f\x00\x00\x00\x00\x00\x00\x00baaaaaarrrrr\x00\x01\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\v\x00\x00\x00\x00\x00\x00\x00foooooooooo\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\f\x00\x00\x00\x00\x00\x00\x00baaaaaarrrrr\x00\x01\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", }, { name: "netip.Addr", val: netip.MustParseAddr("fe80::123%foo"), - out: "\r\x00\x00\x00\x00\x00\x00\x00fe80::123%foo", + out: u64(16+3) + u64(0x80fe) + u64(0x2301<<48) + "foo", }, { name: "ptr-netip.Addr", val: &someIP, - out: "\x01\a\x00\x00\x00\x00\x00\x00\x001.2.3.4", + out: u8(1) + u64(4) + u32(0x04030201), }, { name: "ptr-nil-netip.Addr", @@ -834,3 +873,38 @@ func FuzzTime(f *testing.F) { } }) } + +func FuzzAddr(f *testing.F) { + f.Fuzz(func(t *testing.T, + u1a, u1b uint64, zone1 string, + u2a, u2b uint64, zone2 string, + ) { + var b1, b2 [16]byte + binary.LittleEndian.PutUint64(b1[:8], u1a) + binary.LittleEndian.PutUint64(b1[8:], u1b) + binary.LittleEndian.PutUint64(b2[:8], u2a) + binary.LittleEndian.PutUint64(b2[8:], u2b) + + var ips [4]netip.Addr + ips[0] = netip.AddrFrom4(*(*[4]byte)(b1[:])) + ips[1] = netip.AddrFrom4(*(*[4]byte)(b2[:])) + ips[2] = netip.AddrFrom16(b1) + if zone1 != "" { + ips[2] = ips[2].WithZone(zone1) + } + ips[3] = netip.AddrFrom16(b2) + if zone2 != "" { + ips[3] = ips[2].WithZone(zone2) + } + + for _, ip1 := range ips[:] { + for _, ip2 := range ips[:] { + got := Hash(&ip1) == Hash(&ip2) + want := ip1 == ip2 + if got != want { + t.Errorf("netip.Addr(%s) == netip.Addr(%s) mismatches hash equivalent", ip1.String(), ip2.String()) + } + } + } + }) +} diff --git a/util/deephash/types.go b/util/deephash/types.go index 897f05a46..d6ccb5298 100644 --- a/util/deephash/types.go +++ b/util/deephash/types.go @@ -4,11 +4,34 @@ package deephash -import "reflect" +import ( + "net/netip" + "reflect" + "time" +) + +var ( + timeTimeType = reflect.TypeOf((*time.Time)(nil)).Elem() + netipAddrType = reflect.TypeOf((*netip.Addr)(nil)).Elem() +) + +// typeIsSpecialized reports whether this type has specialized hashing. +// These are never memory hashable and never considered recursive. +func typeIsSpecialized(t reflect.Type) bool { + switch t { + case timeTimeType, netipAddrType: + return true + default: + return false + } +} // typeIsMemHashable reports whether t can be hashed by directly hashing its // contiguous bytes in memory (e.g. structs with gaps are not mem-hashable). func typeIsMemHashable(t reflect.Type) bool { + if typeIsSpecialized(t) { + return false + } if t.Size() == 0 { return true } @@ -50,6 +73,11 @@ func typeIsRecursive(t reflect.Type) bool { delete(inStack, t) }() + // Types with specialized hashing are never considered recursive. + if typeIsSpecialized(t) { + return false + } + // Any type that is memory hashable must not be recursive since // cycles can only occur if pointers are involved. if typeIsMemHashable(t) { @@ -72,10 +100,6 @@ func typeIsRecursive(t reflect.Type) bool { case reflect.Map: return visitType(t.Key()) || visitType(t.Elem()) case reflect.Struct: - if t.String() == "intern.Value" { - // Otherwise its interface{} makes this return true. - return false - } for i, numField := 0, t.NumField(); i < numField; i++ { if visitType(t.Field(i).Type) { return true