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 <charlotte@tailscale.com>
pull/8432/head
Charlotte Brandhorst-Satzkorn 1 year ago committed by Charlotte Brandhorst-Satzkorn
parent 2a9817da39
commit 3417ddc00c

@ -3,7 +3,7 @@
package tailcfg 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 ( import (
"bytes" "bytes"
@ -531,6 +531,24 @@ type Service struct {
// TODO(apenwarr): add "tags" here for each service? // 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. // Hostinfo contains a summary of a Tailscale host.
// //
// Because it contains pointers (slices), this type should not be used // 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 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 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 // NOTE: any new fields containing pointers in this type
// require changes to Hostinfo.Equal. // require changes to Hostinfo.Equal.
} }

@ -119,6 +119,10 @@ func (src *Hostinfo) Clone() *Hostinfo {
dst.Services = append(src.Services[:0:0], src.Services...) dst.Services = append(src.Services[:0:0], src.Services...)
dst.NetInfo = src.NetInfo.Clone() dst.NetInfo = src.NetInfo.Clone()
dst.SSH_HostKeys = append(src.SSH_HostKeys[:0:0], src.SSH_HostKeys...) 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 return dst
} }
@ -157,6 +161,7 @@ var _HostinfoCloneNeedsRegeneration = Hostinfo(struct {
Cloud string Cloud string
Userspace opt.Bool Userspace opt.Bool
UserspaceRouter opt.Bool UserspaceRouter opt.Bool
Location *Location
}{}) }{})
// Clone makes a deep copy of NetInfo. // Clone makes a deep copy of NetInfo.
@ -458,9 +463,29 @@ var _ControlDialPlanCloneNeedsRegeneration = ControlDialPlan(struct {
Candidates []ControlIPCandidate 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. // Clone duplicates src into dst and reports whether it succeeded.
// To succeed, <src, dst> must be of types <*T, *T> or <*T, **T>, // To succeed, <src, dst> 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 { func Clone(dst, src any) bool {
switch src := src.(type) { switch src := src.(type) {
case *User: case *User:
@ -589,6 +614,15 @@ func Clone(dst, src any) bool {
*dst = src.Clone() *dst = src.Clone()
return true return true
} }
case *Location:
switch dst := dst.(type) {
case *Location:
*dst = *src.Clone()
return true
case **Location:
*dst = src.Clone()
return true
}
} }
return false return false
} }

@ -65,6 +65,7 @@ func TestHostinfoEqual(t *testing.T) {
"Cloud", "Cloud",
"Userspace", "Userspace",
"UserspaceRouter", "UserspaceRouter",
"Location",
} }
if have := fieldsOf(reflect.TypeOf(Hostinfo{})); !reflect.DeepEqual(have, hiHandles) { 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", t.Errorf("Hostinfo.Equal check might be out of sync\nfields: %q\nhandled: %q\n",

@ -20,7 +20,7 @@ import (
"tailscale.com/types/views" "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. // View returns a readonly view of User.
func (p *User) View() UserView { 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) Cloud() string { return v.ж.Cloud }
func (v HostinfoView) Userspace() opt.Bool { return v.ж.Userspace } func (v HostinfoView) Userspace() opt.Bool { return v.ж.Userspace }
func (v HostinfoView) UserspaceRouter() opt.Bool { return v.ж.UserspaceRouter } 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. // A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _HostinfoViewNeedsRegeneration = Hostinfo(struct { var _HostinfoViewNeedsRegeneration = Hostinfo(struct {
@ -340,6 +348,7 @@ var _HostinfoViewNeedsRegeneration = Hostinfo(struct {
Cloud string Cloud string
Userspace opt.Bool Userspace opt.Bool
UserspaceRouter opt.Bool UserspaceRouter opt.Bool
Location *Location
}{}) }{})
// View returns a readonly view of NetInfo. // View returns a readonly view of NetInfo.
@ -1077,3 +1086,63 @@ func (v ControlDialPlanView) Candidates() views.Slice[ControlIPCandidate] {
var _ControlDialPlanViewNeedsRegeneration = ControlDialPlan(struct { var _ControlDialPlanViewNeedsRegeneration = ControlDialPlan(struct {
Candidates []ControlIPCandidate 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
}{})

Loading…
Cancel
Save