control: make Hostinfo accessed by pointer

Fix potential races in copying aliased slices by value.

Also few little doc updates.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
pull/103/head
Brad Fitzpatrick 5 years ago
parent 824f825552
commit d8de11a01b

@ -148,7 +148,7 @@ func main() {
c, err := controlclient.New(controlclient.Options{ c, err := controlclient.New(controlclient.Options{
Persist: cfg, Persist: cfg,
ServerURL: *server, ServerURL: *server,
Hostinfo: &hi, Hostinfo: hi,
NewDecompressor: func() (controlclient.Decompressor, error) { NewDecompressor: func() (controlclient.Decompressor, error) {
return zstd.NewReader(nil) return zstd.NewReader(nil)
}, },

@ -47,7 +47,7 @@ func main() {
c, err := controlclient.New(controlclient.Options{ c, err := controlclient.New(controlclient.Options{
Persist: cfg, Persist: cfg,
ServerURL: *server, ServerURL: *server,
Hostinfo: &hi, Hostinfo: hi,
}) })
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)

@ -66,7 +66,7 @@ type Status struct {
URL string URL string
Persist *Persist // locally persisted configuration Persist *Persist // locally persisted configuration
NetMap *NetworkMap // server-pushed configuration NetMap *NetworkMap // server-pushed configuration
Hostinfo tailcfg.Hostinfo // current Hostinfo data Hostinfo *tailcfg.Hostinfo // current Hostinfo data
state state state state
} }
@ -115,7 +115,7 @@ type Client struct {
loggedIn bool // true if currently logged in loggedIn bool // true if currently logged in
loginGoal *LoginGoal // non-nil if some login activity is desired loginGoal *LoginGoal // non-nil if some login activity is desired
synced bool // true if our netmap is up-to-date synced bool // true if our netmap is up-to-date
hostinfo tailcfg.Hostinfo hostinfo *tailcfg.Hostinfo
inPollNetMap bool // true if currently running a PollNetMap inPollNetMap bool // true if currently running a PollNetMap
inSendStatus int // number of sendStatus calls currently in progress inSendStatus int // number of sendStatus calls currently in progress
state state state state
@ -489,7 +489,10 @@ func (c *Client) SetStatusFunc(fn func(Status)) {
c.mu.Unlock() c.mu.Unlock()
} }
func (c *Client) SetHostinfo(hi tailcfg.Hostinfo) { func (c *Client) SetHostinfo(hi *tailcfg.Hostinfo) {
if hi == nil {
panic("nil Hostinfo")
}
c.direct.SetHostinfo(hi) c.direct.SetHostinfo(hi)
// Send new Hostinfo to server // Send new Hostinfo to server
c.cancelMapSafely() c.cancelMapSafely()

@ -85,7 +85,7 @@ type Direct struct {
persist Persist persist Persist
tryingNewKey wgcfg.PrivateKey tryingNewKey wgcfg.PrivateKey
expiry *time.Time expiry *time.Time
hostinfo tailcfg.Hostinfo hostinfo *tailcfg.Hostinfo // always non-nil
endpoints []string endpoints []string
localPort uint16 // or zero to mean auto localPort uint16 // or zero to mean auto
} }
@ -95,7 +95,7 @@ type Options struct {
HTTPC *http.Client // HTTP client used to talk to tailcontrol HTTPC *http.Client // HTTP client used to talk to tailcontrol
ServerURL string // URL of the tailcontrol server ServerURL string // URL of the tailcontrol server
TimeNow func() time.Time // time.Now implementation used by Client TimeNow func() time.Time // time.Now implementation used by Client
Hostinfo *tailcfg.Hostinfo Hostinfo *tailcfg.Hostinfo // non-nil passes ownership, nil means to use default using os.Hostname, etc
NewDecompressor func() (Decompressor, error) NewDecompressor func() (Decompressor, error)
KeepAlive bool KeepAlive bool
Logf logger.Logf Logf logger.Logf
@ -120,9 +120,9 @@ func NewDirect(opts Options) (*Direct, error) {
} }
if opts.Logf == nil { if opts.Logf == nil {
// TODO(apenwarr): remove this default and fail instead. // TODO(apenwarr): remove this default and fail instead.
// TODO(bradfitz): ... but then it shouldn't be in Options.
opts.Logf = log.Printf opts.Logf = log.Printf
} }
c := &Direct{ c := &Direct{
httpc: opts.HTTPC, httpc: opts.HTTPC,
serverURL: opts.ServerURL, serverURL: opts.ServerURL,
@ -135,38 +135,46 @@ func NewDirect(opts Options) (*Direct, error) {
if opts.Hostinfo == nil { if opts.Hostinfo == nil {
c.SetHostinfo(NewHostinfo()) c.SetHostinfo(NewHostinfo())
} else { } else {
c.SetHostinfo(*opts.Hostinfo) c.SetHostinfo(opts.Hostinfo)
} }
return c, nil return c, nil
} }
func NewHostinfo() tailcfg.Hostinfo { func hostinfoOS() string {
hostname, _ := os.Hostname()
os := runtime.GOOS os := runtime.GOOS
switch os { switch os {
case "darwin": case "darwin":
switch runtime.GOARCH { switch runtime.GOARCH {
case "arm", "arm64": case "arm", "arm64":
os = "iOS" return "iOS"
default: default:
os = "macOS" return "macOS"
} }
default:
return os
} }
}
return tailcfg.Hostinfo{ func NewHostinfo() *tailcfg.Hostinfo {
hostname, _ := os.Hostname()
return &tailcfg.Hostinfo{
IPNVersion: version.LONG, IPNVersion: version.LONG,
Hostname: hostname, Hostname: hostname,
OS: os, OS: hostinfoOS(),
} }
} }
func (c *Direct) SetHostinfo(hi tailcfg.Hostinfo) { // SetHostinfo clones the provided Hostinfo and remembers it for the
// next update.
func (c *Direct) SetHostinfo(hi *tailcfg.Hostinfo) {
if hi == nil {
panic("nil Hostinfo")
}
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
c.logf("Hostinfo: %v\n", hi) c.logf("Hostinfo: %v\n", hi)
c.hostinfo = hi c.hostinfo = hi.Copy()
} }
func (c *Direct) GetPersist() Persist { func (c *Direct) GetPersist() Persist {

@ -37,8 +37,11 @@ type EngineStatus struct {
type NetworkMap = controlclient.NetworkMap type NetworkMap = controlclient.NetworkMap
// Notify is a communication from a backend (e.g. tailscaled) to a frontend
// (cmd/tailscale, iOS, macOS, Win Tasktray).
// In any given notification, any or all of these may be nil, meaning // In any given notification, any or all of these may be nil, meaning
// that they have not changed. // that they have not changed.
// They are JSON-encoded on the wire, despite the lack of struct tags.
type Notify struct { type Notify struct {
Version string // version number of IPN backend Version string // version number of IPN backend
ErrMessage *string // critical error message, if any ErrMessage *string // critical error message, if any
@ -49,6 +52,8 @@ type Notify struct {
Engine *EngineStatus // wireguard engine stats Engine *EngineStatus // wireguard engine stats
BrowseToURL *string // UI should open a browser right now BrowseToURL *string // UI should open a browser right now
BackendLogID *string // public logtail id used by backend BackendLogID *string // public logtail id used by backend
// type is mirrored in xcode/Shared/IPN.swift
} }
// StateKey is an opaque identifier for a set of LocalBackend state // StateKey is an opaque identifier for a set of LocalBackend state

@ -24,11 +24,12 @@ import (
) )
// LocalBackend is the scaffolding between the Tailscale cloud control // LocalBackend is the scaffolding between the Tailscale cloud control
// plane and the local network stack. // plane and the local network stack, wiring up NetworkMap updates
// from the cloud to the local WireGuard engine.
type LocalBackend struct { type LocalBackend struct {
logf logger.Logf logf logger.Logf
notify func(n Notify) notify func(Notify)
c *controlclient.Client c *controlclient.Client // TODO: appears to be (inconsistently) guarded by mu
e wgengine.Engine e wgengine.Engine
store StateStore store StateStore
serverURL string serverURL string
@ -42,7 +43,7 @@ type LocalBackend struct {
stateKey StateKey stateKey StateKey
prefs *Prefs prefs *Prefs
state State state State
hiCache tailcfg.Hostinfo hiCache *tailcfg.Hostinfo
netMapCache *controlclient.NetworkMap netMapCache *controlclient.NetworkMap
engineStatus EngineStatus engineStatus EngineStatus
endPoints []string endPoints []string
@ -146,7 +147,9 @@ func (b *LocalBackend) Start(opts Options) error {
hi.FrontendLogID = opts.FrontendLogID hi.FrontendLogID = opts.FrontendLogID
b.mu.Lock() b.mu.Lock()
if b.hiCache != nil {
hi.Services = b.hiCache.Services // keep any previous session hi.Services = b.hiCache.Services // keep any previous session
}
b.hiCache = hi b.hiCache = hi
b.state = NoState b.state = NoState
@ -176,7 +179,7 @@ func (b *LocalBackend) Start(opts Options) error {
}, },
Persist: *persist, Persist: *persist,
ServerURL: b.serverURL, ServerURL: b.serverURL,
Hostinfo: &hi, Hostinfo: hi,
KeepAlive: true, KeepAlive: true,
NewDecompressor: b.newDecompressor, NewDecompressor: b.newDecompressor,
}) })
@ -519,12 +522,12 @@ func (b *LocalBackend) SetPrefs(new *Prefs) {
oldHi := b.hiCache oldHi := b.hiCache
newHi := oldHi.Copy() newHi := oldHi.Copy()
newHi.RoutableIPs = append([]wgcfg.CIDR(nil), b.prefs.AdvertiseRoutes...) newHi.RoutableIPs = append([]wgcfg.CIDR(nil), b.prefs.AdvertiseRoutes...)
b.hiCache = *newHi b.hiCache = newHi
cli := b.c cli := b.c
b.mu.Unlock() b.mu.Unlock()
if cli != nil && !oldHi.Equal(newHi) { if cli != nil && !oldHi.Equal(newHi) {
cli.SetHostinfo(*newHi) cli.SetHostinfo(newHi)
} }
if old.WantRunning != new.WantRunning { if old.WantRunning != new.WantRunning {

@ -208,10 +208,14 @@ type Service struct {
// require changes to Hostinfo.Copy. // require changes to Hostinfo.Copy.
} }
// Hostinfo contains a summary of a Tailscale host.
//
// Because it contains pointers (slices), this type should not be used
// as a value type.
type Hostinfo struct { type Hostinfo struct {
// TODO(crawshaw): mark all these fields ",omitempty" when all the // TODO(crawshaw): mark all these fields ",omitempty" when all the
// iOS apps are updated with the latest swift version of this struct. // iOS apps are updated with the latest swift version of this struct.
IPNVersion string // version number of this code IPNVersion string // version of this code
FrontendLogID string // logtail ID of frontend instance FrontendLogID string // logtail ID of frontend instance
BackendLogID string // logtail ID of backend instance BackendLogID string // logtail ID of backend instance
OS string // operating system the client runs on OS string // operating system the client runs on
@ -225,12 +229,12 @@ type Hostinfo struct {
// Copy makes a deep copy of Hostinfo. // Copy makes a deep copy of Hostinfo.
// The result aliases no memory with the original. // The result aliases no memory with the original.
func (hinfo *Hostinfo) Copy() (res *Hostinfo) { func (h *Hostinfo) Copy() (res *Hostinfo) {
res = new(Hostinfo) res = new(Hostinfo)
*res = *hinfo *res = *h
res.RoutableIPs = append([]wgcfg.CIDR{}, res.RoutableIPs...) res.RoutableIPs = append([]wgcfg.CIDR{}, h.RoutableIPs...)
res.Services = append([]Service{}, res.Services...) res.Services = append([]Service{}, h.Services...)
return res return res
} }
@ -255,19 +259,22 @@ type RegisterRequest struct {
} }
Expiry time.Time // requested key expiry, server policy may override Expiry time.Time // requested key expiry, server policy may override
Followup string // response waits until AuthURL is visited Followup string // response waits until AuthURL is visited
Hostinfo Hostinfo Hostinfo *Hostinfo
} }
// Copy makes a deep copy of RegisterRequest. // Copy makes a deep copy of RegisterRequest.
// The result aliases no memory with the original. // The result aliases no memory with the original.
func (req *RegisterRequest) Copy() *RegisterRequest { func (req *RegisterRequest) Copy() *RegisterRequest {
res := *req res := new(RegisterRequest)
res.Hostinfo = *res.Hostinfo.Copy() *res = *req
if res.Hostinfo != nil {
res.Hostinfo = res.Hostinfo.Copy()
}
if res.Auth.Oauth2Token != nil { if res.Auth.Oauth2Token != nil {
tok := *res.Auth.Oauth2Token tok := *res.Auth.Oauth2Token
res.Auth.Oauth2Token = &tok res.Auth.Oauth2Token = &tok
} }
return &res return res
} }
// RegisterResponse is returned by the server in response to a RegisterRequest. // RegisterResponse is returned by the server in response to a RegisterRequest.
@ -293,7 +300,7 @@ type MapRequest struct {
NodeKey NodeKey NodeKey NodeKey
Endpoints []string Endpoints []string
Stream bool // if true, multiple MapResponse objects are returned Stream bool // if true, multiple MapResponse objects are returned
Hostinfo Hostinfo Hostinfo *Hostinfo
} }
type MapResponse struct { type MapResponse struct {

Loading…
Cancel
Save