diff --git a/cmd/tailscale/depaware.txt b/cmd/tailscale/depaware.txt index e3e84e3ce..ad13374f6 100644 --- a/cmd/tailscale/depaware.txt +++ b/cmd/tailscale/depaware.txt @@ -75,7 +75,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep tailscale.com/types/persist from tailscale.com/ipn tailscale.com/types/preftype from tailscale.com/cmd/tailscale/cli+ tailscale.com/types/structs from tailscale.com/ipn+ - tailscale.com/types/views from tailscale.com/tailcfg + tailscale.com/types/views from tailscale.com/tailcfg+ tailscale.com/util/clientmetric from tailscale.com/net/netcheck+ tailscale.com/util/dnsname from tailscale.com/cmd/tailscale/cli+ W tailscale.com/util/endian from tailscale.com/net/netns diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt index 8cd617db9..d4988e57d 100644 --- a/cmd/tailscaled/depaware.txt +++ b/cmd/tailscaled/depaware.txt @@ -240,7 +240,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/types/persist from tailscale.com/control/controlclient+ tailscale.com/types/preftype from tailscale.com/ipn+ tailscale.com/types/structs from tailscale.com/control/controlclient+ - tailscale.com/types/views from tailscale.com/tailcfg + tailscale.com/types/views from tailscale.com/tailcfg+ tailscale.com/util/clientmetric from tailscale.com/ipn/localapi+ L tailscale.com/util/cmpver from tailscale.com/net/dns 💣 tailscale.com/util/deephash from tailscale.com/ipn/ipnlocal+ diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index b484102a5..82f7d717b 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -48,6 +48,7 @@ import ( "tailscale.com/types/netmap" "tailscale.com/types/persist" "tailscale.com/types/preftype" + "tailscale.com/types/views" "tailscale.com/util/deephash" "tailscale.com/util/dnsname" "tailscale.com/util/multierr" @@ -443,13 +444,23 @@ func (b *LocalBackend) populatePeerStatusLocked(sb *ipnstate.StatusBuilder) { exitNodeOption := tsaddr.PrefixesContainsFunc(p.AllowedIPs, func(r netaddr.IPPrefix) bool { return r.Bits() == 0 }) + var tags *views.StringSlice + var primaryRoutes *views.IPPrefixSlice + if p.Tags != nil { + v := views.StringSliceOf(p.Tags) + tags = &v + } + if p.PrimaryRoutes != nil { + v := views.IPPrefixSliceOf(p.PrimaryRoutes) + primaryRoutes = &v + } sb.AddPeer(p.Key, &ipnstate.PeerStatus{ InNetworkMap: true, ID: p.StableID, UserID: p.User, TailscaleIPs: tailscaleIPs, - Tags: p.Tags, - PrimaryRoutes: p.PrimaryRoutes, + Tags: tags, + PrimaryRoutes: primaryRoutes, HostName: p.Hostinfo.Hostname(), DNSName: p.Name, OS: p.Hostinfo.OS(), diff --git a/ipn/ipnstate/ipnstate.go b/ipn/ipnstate/ipnstate.go index 1639d06d3..7a333be3e 100644 --- a/ipn/ipnstate/ipnstate.go +++ b/ipn/ipnstate/ipnstate.go @@ -20,6 +20,7 @@ import ( "inet.af/netaddr" "tailscale.com/tailcfg" "tailscale.com/types/key" + "tailscale.com/types/views" "tailscale.com/util/dnsname" ) @@ -111,12 +112,12 @@ type PeerStatus struct { // Tags are the list of ACL tags applied to this node. // See tailscale.com/tailcfg#Node.Tags for more information. - Tags []string `json:",omitempty"` + Tags *views.StringSlice `json:",omitempty"` // 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 []netaddr.IPPrefix `json:",omitempty"` + PrimaryRoutes *views.IPPrefixSlice `json:",omitempty"` // Endpoints: Addrs []string @@ -274,10 +275,10 @@ func (sb *StatusBuilder) AddPeer(peer key.NodePublic, st *PeerStatus) { if v := st.TailscaleIPs; v != nil { e.TailscaleIPs = v } - if v := st.PrimaryRoutes; v != nil { + if v := st.PrimaryRoutes; v != nil && !v.IsNil() { e.PrimaryRoutes = v } - if v := st.Tags; v != nil { + if v := st.Tags; v != nil && !v.IsNil() { e.Tags = v } if v := st.OS; v != "" { diff --git a/types/views/views.go b/types/views/views.go index c19da23f4..d92b9c342 100644 --- a/types/views/views.go +++ b/types/views/views.go @@ -7,6 +7,9 @@ package views import ( + "encoding/json" + "errors" + "inet.af/netaddr" "tailscale.com/net/tsaddr" ) @@ -21,6 +24,28 @@ type StringSlice struct { // StringSliceOf returns a StringSlice for the provided slice. func StringSliceOf(x []string) StringSlice { return StringSlice{x} } +// MarshalJSON implements json.Marshaler. +func (v StringSlice) MarshalJSON() ([]byte, error) { + return json.Marshal(v.ж) +} + +// UnmarshalJSON implements json.Unmarshaler. +func (v *StringSlice) UnmarshalJSON(b []byte) error { + if v.ж != nil { + return errors.New("StringSlice is already initialized") + } + if len(b) == 0 { + return nil + } + if err := json.Unmarshal(b, &v.ж); err != nil { + return err + } + return nil +} + +// IsNil reports whether the underlying slice is nil. +func (v StringSlice) IsNil() bool { return v.ж == nil } + // Len returns the length of the slice. func (v StringSlice) Len() int { return len(v.ж) } @@ -47,6 +72,9 @@ type IPPrefixSlice struct { // IPPrefixSliceOf returns a IPPrefixSlice for the provided slice. func IPPrefixSliceOf(x []netaddr.IPPrefix) IPPrefixSlice { return IPPrefixSlice{x} } +// IsNil reports whether the underlying slice is nil. +func (v IPPrefixSlice) IsNil() bool { return v.ж == nil } + // Len returns the length of the slice. func (v IPPrefixSlice) Len() int { return len(v.ж) } @@ -72,3 +100,22 @@ func (v IPPrefixSlice) ContainsIP(ip netaddr.IP) bool { func (v IPPrefixSlice) ContainsFunc(f func(netaddr.IPPrefix) bool) bool { return tsaddr.PrefixesContainsFunc(v.ж, f) } + +// MarshalJSON implements json.Marshaler. +func (v IPPrefixSlice) MarshalJSON() ([]byte, error) { + return json.Marshal(v.ж) +} + +// UnmarshalJSON implements json.Unmarshaler. +func (v *IPPrefixSlice) UnmarshalJSON(b []byte) error { + if v.ж != nil { + return errors.New("IPPrefixSlice is already initialized") + } + if len(b) == 0 { + return nil + } + if err := json.Unmarshal(b, &v.ж); err != nil { + return err + } + return nil +} diff --git a/types/views/views_test.go b/types/views/views_test.go new file mode 100644 index 000000000..5f4a8ceee --- /dev/null +++ b/types/views/views_test.go @@ -0,0 +1,73 @@ +// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package views + +import ( + "bytes" + "encoding/json" + "reflect" + "strings" + "testing" + + "inet.af/netaddr" +) + +func TestViewsJSON(t *testing.T) { + mustCIDR := func(cidrs ...string) (out []netaddr.IPPrefix) { + for _, cidr := range cidrs { + out = append(out, netaddr.MustParseIPPrefix(cidr)) + } + return + } + type viewStruct struct { + Addrs IPPrefixSlice + Strings StringSlice + AddrsPtr *IPPrefixSlice `json:",omitempty"` + StringsPtr *StringSlice `json:",omitempty"` + } + tests := []struct { + name string + in viewStruct + wantJSON string + }{ + { + name: "empty", + in: viewStruct{}, + wantJSON: `{"Addrs":null,"Strings":null}`, + }, + { + name: "everything", + in: viewStruct{ + Addrs: IPPrefixSliceOf(mustCIDR("192.168.0.0/24")), + AddrsPtr: &IPPrefixSlice{mustCIDR("192.168.0.0/24")}, + StringsPtr: &StringSlice{[]string{"foo"}}, + Strings: StringSlice{[]string{"bar"}}, + }, + wantJSON: `{"Addrs":["192.168.0.0/24"],"Strings":["bar"],"AddrsPtr":["192.168.0.0/24"],"StringsPtr":["foo"]}`, + }, + } + + var buf bytes.Buffer + encoder := json.NewEncoder(&buf) + encoder.SetIndent("", "") + for _, tc := range tests { + buf.Reset() + if err := encoder.Encode(&tc.in); err != nil { + t.Fatal(err) + } + b := buf.Bytes() + gotJSON := strings.TrimSpace(string(b)) + if tc.wantJSON != gotJSON { + t.Fatalf("JSON: %v; want: %v", gotJSON, tc.wantJSON) + } + var got viewStruct + if err := json.Unmarshal(b, &got); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, tc.in) { + t.Fatalf("unmarshal resulted in different output: %+v; want %+v", got, tc.in) + } + } +}