From 3417ddc00c8c1c151f2e6fc86f64a3baa3e46263 Mon Sep 17 00:00:00 2001 From: Charlotte Brandhorst-Satzkorn Date: Fri, 16 Jun 2023 10:04:07 -0700 Subject: [PATCH] tailcfg: add location field to hostinfo This change adds Location field to HostInfo. Location contains the option for a Country, CountryCode, City, CityCode and a Priority. Neither of these fields are populated by default. The Priority field is used to determine the priority an exit node should be given for use, if the field is set. The higher the value set, the higher priority the node should be given for use. Updates tailscale/corp#12146 Signed-off-by: Charlotte Brandhorst-Satzkorn --- tailcfg/tailcfg.go | 25 +++++++++++++- tailcfg/tailcfg_clone.go | 36 +++++++++++++++++++- tailcfg/tailcfg_test.go | 1 + tailcfg/tailcfg_view.go | 73 ++++++++++++++++++++++++++++++++++++++-- 4 files changed, 131 insertions(+), 4 deletions(-) diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go index 4c6f9a39f..d344e2bd4 100644 --- a/tailcfg/tailcfg.go +++ b/tailcfg/tailcfg.go @@ -3,7 +3,7 @@ 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:generate go run tailscale.com/cmd/viewer --type=User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse,DERPRegion,DERPMap,DERPNode,SSHRule,SSHAction,SSHPrincipal,ControlDialPlan,Location --clonefunc import ( "bytes" @@ -531,6 +531,24 @@ type Service struct { // TODO(apenwarr): add "tags" here for each service? } +// Location represents geographical location data about a +// Tailscale host. Location is optional and only set if +// explicitly declared by a node. +type Location struct { + Country string `json:",omitempty"` // User friendly country name, with proper capitalization, e.g "Canada" + CountryCode string `json:",omitempty"` // ISO 3166-1 alpha-2 in lower case, e.g "ca" + City string `json:",omitempty"` // User friendly city name, with proper capitalization, e.g. "Squamish" + CityCode string `json:",omitempty"` + + // Priority determines the priority an exit node is given when the + // location data between two or more nodes is tied. + // A higher value indicates that the exit node is more preferable + // for use. + // A value of 0 means the exit node does not have a priority + // preference. A negative int is not allowed. + Priority int `json:",omitempty"` +} + // Hostinfo contains a summary of a Tailscale host. // // Because it contains pointers (slices), this type should not be used @@ -585,6 +603,11 @@ type Hostinfo struct { 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 + // Location represents geographical location data about a + // Tailscale host. Location is optional and only set if + // explicitly declared by a node. + Location *Location `json:",omitempty"` + // NOTE: any new fields containing pointers in this type // require changes to Hostinfo.Equal. } diff --git a/tailcfg/tailcfg_clone.go b/tailcfg/tailcfg_clone.go index 9d72124b4..7c68d1d80 100644 --- a/tailcfg/tailcfg_clone.go +++ b/tailcfg/tailcfg_clone.go @@ -119,6 +119,10 @@ func (src *Hostinfo) Clone() *Hostinfo { dst.Services = append(src.Services[:0:0], src.Services...) dst.NetInfo = src.NetInfo.Clone() dst.SSH_HostKeys = append(src.SSH_HostKeys[:0:0], src.SSH_HostKeys...) + if dst.Location != nil { + dst.Location = new(Location) + *dst.Location = *src.Location + } return dst } @@ -157,6 +161,7 @@ var _HostinfoCloneNeedsRegeneration = Hostinfo(struct { Cloud string Userspace opt.Bool UserspaceRouter opt.Bool + Location *Location }{}) // Clone makes a deep copy of NetInfo. @@ -458,9 +463,29 @@ var _ControlDialPlanCloneNeedsRegeneration = ControlDialPlan(struct { Candidates []ControlIPCandidate }{}) +// Clone makes a deep copy of Location. +// The result aliases no memory with the original. +func (src *Location) Clone() *Location { + if src == nil { + return nil + } + dst := new(Location) + *dst = *src + return dst +} + +// A compilation failure here means this code must be regenerated, with the command at the top of this file. +var _LocationCloneNeedsRegeneration = Location(struct { + Country string + CountryCode string + City string + CityCode string + Priority int +}{}) + // Clone duplicates src into dst and reports whether it succeeded. // To succeed, must be of types <*T, *T> or <*T, **T>, -// where T is one of User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse,DERPRegion,DERPMap,DERPNode,SSHRule,SSHAction,SSHPrincipal,ControlDialPlan. +// where T is one of User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse,DERPRegion,DERPMap,DERPNode,SSHRule,SSHAction,SSHPrincipal,ControlDialPlan,Location. func Clone(dst, src any) bool { switch src := src.(type) { case *User: @@ -589,6 +614,15 @@ func Clone(dst, src any) bool { *dst = src.Clone() return true } + case *Location: + switch dst := dst.(type) { + case *Location: + *dst = *src.Clone() + return true + case **Location: + *dst = src.Clone() + return true + } } return false } diff --git a/tailcfg/tailcfg_test.go b/tailcfg/tailcfg_test.go index b0e3f982e..2db6cb864 100644 --- a/tailcfg/tailcfg_test.go +++ b/tailcfg/tailcfg_test.go @@ -65,6 +65,7 @@ func TestHostinfoEqual(t *testing.T) { "Cloud", "Userspace", "UserspaceRouter", + "Location", } if have := fieldsOf(reflect.TypeOf(Hostinfo{})); !reflect.DeepEqual(have, hiHandles) { t.Errorf("Hostinfo.Equal check might be out of sync\nfields: %q\nhandled: %q\n", diff --git a/tailcfg/tailcfg_view.go b/tailcfg/tailcfg_view.go index 9c195da1c..7837a5a5c 100644 --- a/tailcfg/tailcfg_view.go +++ b/tailcfg/tailcfg_view.go @@ -20,7 +20,7 @@ import ( "tailscale.com/types/views" ) -//go:generate go run tailscale.com/cmd/cloner -clonefunc=true -type=User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse,DERPRegion,DERPMap,DERPNode,SSHRule,SSHAction,SSHPrincipal,ControlDialPlan +//go:generate go run tailscale.com/cmd/cloner -clonefunc=true -type=User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse,DERPRegion,DERPMap,DERPNode,SSHRule,SSHAction,SSHPrincipal,ControlDialPlan,Location // View returns a readonly view of User. func (p *User) View() UserView { @@ -303,7 +303,15 @@ func (v HostinfoView) SSH_HostKeys() views.Slice[string] { return views.SliceOf( 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) Equal(v2 HostinfoView) bool { return v.ж.Equal(v2.ж) } +func (v HostinfoView) Location() *Location { + if v.ж.Location == nil { + return nil + } + x := *v.ж.Location + return &x +} + +func (v HostinfoView) Equal(v2 HostinfoView) 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 _HostinfoViewNeedsRegeneration = Hostinfo(struct { @@ -340,6 +348,7 @@ var _HostinfoViewNeedsRegeneration = Hostinfo(struct { Cloud string Userspace opt.Bool UserspaceRouter opt.Bool + Location *Location }{}) // View returns a readonly view of NetInfo. @@ -1077,3 +1086,63 @@ func (v ControlDialPlanView) Candidates() views.Slice[ControlIPCandidate] { var _ControlDialPlanViewNeedsRegeneration = ControlDialPlan(struct { Candidates []ControlIPCandidate }{}) + +// View returns a readonly view of Location. +func (p *Location) View() LocationView { + return LocationView{ж: p} +} + +// LocationView provides a read-only view over Location. +// +// Its methods should only be called if `Valid()` returns true. +type LocationView struct { + // ж is the underlying mutable value, named with a hard-to-type + // character that looks pointy like a pointer. + // It is named distinctively to make you think of how dangerous it is to escape + // to callers. You must not let callers be able to mutate it. + ж *Location +} + +// Valid reports whether underlying value is non-nil. +func (v LocationView) Valid() bool { return v.ж != nil } + +// AsStruct returns a clone of the underlying value which aliases no memory with +// the original. +func (v LocationView) AsStruct() *Location { + if v.ж == nil { + return nil + } + return v.ж.Clone() +} + +func (v LocationView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) } + +func (v *LocationView) UnmarshalJSON(b []byte) error { + if v.ж != nil { + return errors.New("already initialized") + } + if len(b) == 0 { + return nil + } + var x Location + if err := json.Unmarshal(b, &x); err != nil { + return err + } + v.ж = &x + return nil +} + +func (v LocationView) Country() string { return v.ж.Country } +func (v LocationView) CountryCode() string { return v.ж.CountryCode } +func (v LocationView) City() string { return v.ж.City } +func (v LocationView) CityCode() string { return v.ж.CityCode } +func (v LocationView) Priority() int { return v.ж.Priority } + +// A compilation failure here means this code must be regenerated, with the command at the top of this file. +var _LocationViewNeedsRegeneration = Location(struct { + Country string + CountryCode string + City string + CityCode string + Priority int +}{})