diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go index 2404b6bb8..e0e60f923 100644 --- a/tailcfg/tailcfg.go +++ b/tailcfg/tailcfg.go @@ -577,6 +577,68 @@ func (h *Hostinfo) Equal(h2 *Hostinfo) bool { 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". +func (h *Hostinfo) HowUnequal(h2 *Hostinfo) (path []string) { + return appendStructPtrDiff(nil, "", reflect.ValueOf(h), reflect.ValueOf(h2)) +} + +func appendStructPtrDiff(base []string, pfx string, p1, p2 reflect.Value) (ret []string) { + ret = base + if p1.IsNil() && p2.IsNil() { + return base + } + mkPath := func(b string) string { + if pfx == "" { + return b + } + return pfx + "." + b + } + if p1.IsNil() || p2.IsNil() { + return append(base, mkPath("nil")) + } + v1, v2 := p1.Elem(), p2.Elem() + t := v1.Type() + for i, n := 0, t.NumField(); i < n; i++ { + sf := t.Field(i) + switch sf.Type.Kind() { + case reflect.String: + if v1.Field(i).String() != v2.Field(i).String() { + ret = append(ret, mkPath(sf.Name)) + } + continue + case reflect.Bool: + if v1.Field(i).Bool() != v2.Field(i).Bool() { + ret = append(ret, mkPath(sf.Name)) + } + continue + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + if v1.Field(i).Int() != v2.Field(i).Int() { + ret = append(ret, mkPath(sf.Name)) + } + continue + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + if v1.Field(i).Uint() != v2.Field(i).Uint() { + ret = append(ret, mkPath(sf.Name)) + } + continue + case reflect.Slice, reflect.Map: + if !reflect.DeepEqual(v1.Field(i).Interface(), v2.Field(i).Interface()) { + ret = append(ret, mkPath(sf.Name)) + } + continue + case reflect.Ptr: + if sf.Type.Elem().Kind() == reflect.Struct { + ret = appendStructPtrDiff(ret, sf.Name, v1.Field(i), v2.Field(i)) + continue + } + } + panic(fmt.Sprintf("unsupported type at %s: %s", mkPath(sf.Name), sf.Type.String())) + } + return ret +} + // SignatureType specifies a scheme for signing RegisterRequest messages. It // specifies the crypto algorithms to use, the contents of what is signed, and // any other relevant details. Historically, requests were unsigned so the zero diff --git a/tailcfg/tailcfg_test.go b/tailcfg/tailcfg_test.go index 071a88a90..37275c59a 100644 --- a/tailcfg/tailcfg_test.go +++ b/tailcfg/tailcfg_test.go @@ -190,6 +190,82 @@ func TestHostinfoEqual(t *testing.T) { } } +func TestHostinfoHowEqual(t *testing.T) { + tests := []struct { + a, b *Hostinfo + want []string + }{ + { + a: nil, + b: nil, + want: nil, + }, + { + a: new(Hostinfo), + b: nil, + want: []string{"nil"}, + }, + { + a: nil, + b: new(Hostinfo), + want: []string{"nil"}, + }, + { + a: new(Hostinfo), + b: new(Hostinfo), + want: nil, + }, + { + a: &Hostinfo{ + IPNVersion: "1", + ShieldsUp: false, + RoutableIPs: []netaddr.IPPrefix{netaddr.MustParseIPPrefix("1.2.3.0/24")}, + }, + b: &Hostinfo{ + IPNVersion: "2", + ShieldsUp: true, + RoutableIPs: []netaddr.IPPrefix{netaddr.MustParseIPPrefix("1.2.3.0/25")}, + }, + want: []string{"IPNVersion", "ShieldsUp", "RoutableIPs"}, + }, + { + a: &Hostinfo{ + IPNVersion: "1", + }, + b: &Hostinfo{ + IPNVersion: "2", + NetInfo: new(NetInfo), + }, + want: []string{"IPNVersion", "NetInfo.nil"}, + }, + { + a: &Hostinfo{ + IPNVersion: "1", + NetInfo: &NetInfo{ + WorkingIPv6: "true", + HavePortMap: true, + LinkType: "foo", + PreferredDERP: 123, + DERPLatency: map[string]float64{ + "foo": 1.0, + }, + }, + }, + b: &Hostinfo{ + IPNVersion: "2", + NetInfo: &NetInfo{}, + }, + want: []string{"IPNVersion", "NetInfo.WorkingIPv6", "NetInfo.HavePortMap", "NetInfo.PreferredDERP", "NetInfo.LinkType", "NetInfo.DERPLatency"}, + }, + } + for i, tt := range tests { + got := tt.a.HowUnequal(tt.b) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("%d. got %q; want %q", i, got, tt.want) + } + } +} + func TestNodeEqual(t *testing.T) { nodeHandles := []string{ "ID", "StableID", "Name", "User", "Sharer",