@ -14,6 +14,7 @@ import (
"fmt"
"fmt"
"io"
"io"
"log"
"log"
"math/rand/v2"
"net"
"net"
"net/http"
"net/http"
"net/netip"
"net/netip"
@ -52,6 +53,7 @@ import (
"tailscale.com/types/ptr"
"tailscale.com/types/ptr"
"tailscale.com/types/tkatype"
"tailscale.com/types/tkatype"
"tailscale.com/util/clientmetric"
"tailscale.com/util/clientmetric"
"tailscale.com/util/eventbus"
"tailscale.com/util/multierr"
"tailscale.com/util/multierr"
"tailscale.com/util/singleflight"
"tailscale.com/util/singleflight"
"tailscale.com/util/syspolicy/pkey"
"tailscale.com/util/syspolicy/pkey"
@ -74,16 +76,17 @@ type Direct struct {
netMon * netmon . Monitor // non-nil
netMon * netmon . Monitor // non-nil
health * health . Tracker
health * health . Tracker
discoPubKey key . DiscoPublic
discoPubKey key . DiscoPublic
busClient * eventbus . Client
clientVersionPub * eventbus . Publisher [ tailcfg . ClientVersion ]
autoUpdatePub * eventbus . Publisher [ AutoUpdate ]
controlTimePub * eventbus . Publisher [ ControlTime ]
getMachinePrivKey func ( ) ( key . MachinePrivate , error )
getMachinePrivKey func ( ) ( key . MachinePrivate , error )
debugFlags [ ] string
debugFlags [ ] string
skipIPForwardingCheck bool
skipIPForwardingCheck bool
pinger Pinger
pinger Pinger
polc policyclient . Client // always non-nil
popBrowser func ( url string ) // or nil
popBrowser func ( url string ) // or nil
polc policyclient . Client // always non-nil
c2nHandler http . Handler // or nil
c2nHandler http . Handler // or nil
onClientVersion func ( * tailcfg . ClientVersion ) // or nil
onControlTime func ( time . Time ) // or nil
onTailnetDefaultAutoUpdate func ( bool ) // or nil
panicOnUse bool // if true, panic if client is used (for testing)
panicOnUse bool // if true, panic if client is used (for testing)
closedCtx context . Context // alive until Direct.Close is called
closedCtx context . Context // alive until Direct.Close is called
closeCtx context . CancelFunc // cancels closedCtx
closeCtx context . CancelFunc // cancels closedCtx
@ -107,6 +110,8 @@ type Direct struct {
tkaHead string
tkaHead string
lastPingURL string // last PingRequest.URL received, for dup suppression
lastPingURL string // last PingRequest.URL received, for dup suppression
connectionHandleForTest string // sent in MapRequest.ConnectionHandleForTest
connectionHandleForTest string // sent in MapRequest.ConnectionHandleForTest
controlClientID int64 // Random ID used to differentiate clients for consumers of messages.
}
}
// Observer is implemented by users of the control client (such as LocalBackend)
// Observer is implemented by users of the control client (such as LocalBackend)
@ -134,12 +139,10 @@ type Options struct {
DebugFlags [ ] string // debug settings to send to control
DebugFlags [ ] string // debug settings to send to control
HealthTracker * health . Tracker
HealthTracker * health . Tracker
PopBrowserURL func ( url string ) // optional func to open browser
PopBrowserURL func ( url string ) // optional func to open browser
OnClientVersion func ( * tailcfg . ClientVersion ) // optional func to inform GUI of client version status
OnControlTime func ( time . Time ) // optional func to notify callers of new time from control
OnTailnetDefaultAutoUpdate func ( bool ) // optional func to inform GUI of default auto-update setting for the tailnet
Dialer * tsdial . Dialer // non-nil
Dialer * tsdial . Dialer // non-nil
C2NHandler http . Handler // or nil
C2NHandler http . Handler // or nil
ControlKnobs * controlknobs . Knobs // or nil to ignore
ControlKnobs * controlknobs . Knobs // or nil to ignore
Bus * eventbus . Bus
// Observer is called when there's a change in status to report
// Observer is called when there's a change in status to report
// from the control client.
// from the control client.
@ -304,9 +307,6 @@ func NewDirect(opts Options) (*Direct, error) {
pinger : opts . Pinger ,
pinger : opts . Pinger ,
polc : cmp . Or ( opts . PolicyClient , policyclient . Client ( policyclient . NoPolicyClient { } ) ) ,
polc : cmp . Or ( opts . PolicyClient , policyclient . Client ( policyclient . NoPolicyClient { } ) ) ,
popBrowser : opts . PopBrowserURL ,
popBrowser : opts . PopBrowserURL ,
onClientVersion : opts . OnClientVersion ,
onTailnetDefaultAutoUpdate : opts . OnTailnetDefaultAutoUpdate ,
onControlTime : opts . OnControlTime ,
c2nHandler : opts . C2NHandler ,
c2nHandler : opts . C2NHandler ,
dialer : opts . Dialer ,
dialer : opts . Dialer ,
dnsCache : dnsCache ,
dnsCache : dnsCache ,
@ -314,6 +314,8 @@ func NewDirect(opts Options) (*Direct, error) {
}
}
c . closedCtx , c . closeCtx = context . WithCancel ( context . Background ( ) )
c . closedCtx , c . closeCtx = context . WithCancel ( context . Background ( ) )
c . controlClientID = rand . Int64 ( )
if opts . Hostinfo == nil {
if opts . Hostinfo == nil {
c . SetHostinfo ( hostinfo . New ( ) )
c . SetHostinfo ( hostinfo . New ( ) )
} else {
} else {
@ -331,6 +333,12 @@ func NewDirect(opts Options) (*Direct, error) {
if strings . Contains ( opts . ServerURL , "controlplane.tailscale.com" ) && envknob . Bool ( "TS_PANIC_IF_HIT_MAIN_CONTROL" ) {
if strings . Contains ( opts . ServerURL , "controlplane.tailscale.com" ) && envknob . Bool ( "TS_PANIC_IF_HIT_MAIN_CONTROL" ) {
c . panicOnUse = true
c . panicOnUse = true
}
}
c . busClient = opts . Bus . Client ( "controlClient.direct" )
c . clientVersionPub = eventbus . Publish [ tailcfg . ClientVersion ] ( c . busClient )
c . autoUpdatePub = eventbus . Publish [ AutoUpdate ] ( c . busClient )
c . controlTimePub = eventbus . Publish [ ControlTime ] ( c . busClient )
return c , nil
return c , nil
}
}
@ -340,6 +348,7 @@ func (c *Direct) Close() error {
c . mu . Lock ( )
c . mu . Lock ( )
defer c . mu . Unlock ( )
defer c . mu . Unlock ( )
c . busClient . Close ( )
if c . noiseClient != nil {
if c . noiseClient != nil {
if err := c . noiseClient . Close ( ) ; err != nil {
if err := c . noiseClient . Close ( ) ; err != nil {
return err
return err
@ -826,6 +835,23 @@ func (c *Direct) SendUpdate(ctx context.Context) error {
return c . sendMapRequest ( ctx , false , nil )
return c . sendMapRequest ( ctx , false , nil )
}
}
// ClientID returns the ControlClientID of the controlClient
func ( c * Direct ) ClientID ( ) int64 {
return c . controlClientID
}
// AutoUpdate wraps a bool for naming on the eventbus
type AutoUpdate struct {
ClientID int64 // The ID field is used for consumers to differentiate instances of Direct
Value bool
}
// ControlTime wraps a [time.Time] for naming on the eventbus
type ControlTime struct {
ClientID int64 // The ID field is used for consumers to differentiate instances of Direct
Value time . Time
}
// If we go more than watchdogTimeout without hearing from the server,
// If we go more than watchdogTimeout without hearing from the server,
// end the long poll. We should be receiving a keep alive ping
// end the long poll. We should be receiving a keep alive ping
// every minute.
// every minute.
@ -1085,14 +1111,12 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap
c . logf ( "netmap: control says to open URL %v; no popBrowser func" , u )
c . logf ( "netmap: control says to open URL %v; no popBrowser func" , u )
}
}
}
}
if resp . ClientVersion != nil && c . onClientVersion != nil {
if resp . ClientVersion != nil {
c . onClientVersion ( resp . ClientVersion )
c . clientVersionPub . Publish ( * resp . ClientVersion )
}
}
if resp . ControlTime != nil && ! resp . ControlTime . IsZero ( ) {
if resp . ControlTime != nil && ! resp . ControlTime . IsZero ( ) {
c . logf . JSON ( 1 , "controltime" , resp . ControlTime . UTC ( ) )
c . logf . JSON ( 1 , "controltime" , resp . ControlTime . UTC ( ) )
if c . onControlTime != nil {
c . controlTimePub . Publish ( ControlTime { c . controlClientID , * resp . ControlTime } )
c . onControlTime ( * resp . ControlTime )
}
}
}
if resp . KeepAlive {
if resp . KeepAlive {
vlogf ( "netmap: got keep-alive" )
vlogf ( "netmap: got keep-alive" )
@ -1112,9 +1136,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap
continue
continue
}
}
if au , ok := resp . DefaultAutoUpdate . Get ( ) ; ok {
if au , ok := resp . DefaultAutoUpdate . Get ( ) ; ok {
if c . onTailnetDefaultAutoUpdate != nil {
c . autoUpdatePub . Publish ( AutoUpdate { c . controlClientID , au } )
c . onTailnetDefaultAutoUpdate ( au )
}
}
}
metricMapResponseMap . Add ( 1 )
metricMapResponseMap . Add ( 1 )