mirror of
https://github.com/tailscale/tailscale.git
synced 2025-10-27 20:19:31 +00:00
control/controlclient: introduce eventbus messages instead of callbacks (#16956)
This is a small introduction of the eventbus into controlclient that communicates with mainly ipnlocal. While ipnlocal is a complicated part of the codebase, the subscribers here are from the perspective of ipnlocal already called async. Updates #15160 Signed-off-by: Claus Lensbøl <claus@tailscale.com>
This commit is contained in:
@@ -205,7 +205,6 @@ func NewNoStart(opts Options) (_ *Auto, err error) {
|
||||
}
|
||||
})
|
||||
return c, nil
|
||||
|
||||
}
|
||||
|
||||
// SetPaused controls whether HTTP activity should be paused.
|
||||
@@ -424,6 +423,11 @@ func (c *Auto) unpausedChanLocked() <-chan bool {
|
||||
return unpaused
|
||||
}
|
||||
|
||||
// ClientID returns the ClientID of the direct controlClient
|
||||
func (c *Auto) ClientID() int64 {
|
||||
return c.direct.ClientID()
|
||||
}
|
||||
|
||||
// mapRoutineState is the state of Auto.mapRoutine while it's running.
|
||||
type mapRoutineState struct {
|
||||
c *Auto
|
||||
|
||||
@@ -81,6 +81,9 @@ type Client interface {
|
||||
// in a separate http request. It has nothing to do with the rest of
|
||||
// the state machine.
|
||||
UpdateEndpoints(endpoints []tailcfg.Endpoint)
|
||||
// ClientID returns the ClientID of a client. This ID is meant to
|
||||
// distinguish one client from another.
|
||||
ClientID() int64
|
||||
}
|
||||
|
||||
// UserVisibleError is an error that should be shown to users.
|
||||
|
||||
@@ -35,6 +35,7 @@ import (
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/util/eventbus/eventbustest"
|
||||
)
|
||||
|
||||
func fieldsOf(t reflect.Type) (fields []string) {
|
||||
@@ -218,6 +219,8 @@ func TestDirectProxyManual(t *testing.T) {
|
||||
t.Skip("skipping without --live-network-test")
|
||||
}
|
||||
|
||||
bus := eventbustest.NewBus(t)
|
||||
|
||||
dialer := &tsdial.Dialer{}
|
||||
dialer.SetNetMon(netmon.NewStatic())
|
||||
|
||||
@@ -239,6 +242,7 @@ func TestDirectProxyManual(t *testing.T) {
|
||||
},
|
||||
Dialer: dialer,
|
||||
ControlKnobs: &controlknobs.Knobs{},
|
||||
Bus: bus,
|
||||
}
|
||||
d, err := NewDirect(opts)
|
||||
if err != nil {
|
||||
@@ -263,6 +267,8 @@ func TestHTTPSWithProxy(t *testing.T) { testHTTPS(t, true) }
|
||||
func testHTTPS(t *testing.T, withProxy bool) {
|
||||
bakedroots.ResetForTest(t, tlstest.TestRootCA())
|
||||
|
||||
bus := eventbustest.NewBus(t)
|
||||
|
||||
controlLn, err := tls.Listen("tcp", "127.0.0.1:0", tlstest.ControlPlane.ServerTLSConfig())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -327,6 +333,7 @@ func testHTTPS(t *testing.T, withProxy bool) {
|
||||
t.Logf("PopBrowserURL: %q", url)
|
||||
},
|
||||
Dialer: dialer,
|
||||
Bus: bus,
|
||||
}
|
||||
d, err := NewDirect(opts)
|
||||
if err != nil {
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"math/rand/v2"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
@@ -52,6 +53,7 @@ import (
|
||||
"tailscale.com/types/ptr"
|
||||
"tailscale.com/types/tkatype"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/eventbus"
|
||||
"tailscale.com/util/multierr"
|
||||
"tailscale.com/util/singleflight"
|
||||
"tailscale.com/util/syspolicy/pkey"
|
||||
@@ -63,30 +65,31 @@ import (
|
||||
|
||||
// Direct is the client that connects to a tailcontrol server for a node.
|
||||
type Direct struct {
|
||||
httpc *http.Client // HTTP client used to talk to tailcontrol
|
||||
interceptedDial *atomic.Bool // if non-nil, pointer to bool whether ScreenTime intercepted our dial
|
||||
dialer *tsdial.Dialer
|
||||
dnsCache *dnscache.Resolver
|
||||
controlKnobs *controlknobs.Knobs // always non-nil
|
||||
serverURL string // URL of the tailcontrol server
|
||||
clock tstime.Clock
|
||||
logf logger.Logf
|
||||
netMon *netmon.Monitor // non-nil
|
||||
health *health.Tracker
|
||||
discoPubKey key.DiscoPublic
|
||||
getMachinePrivKey func() (key.MachinePrivate, error)
|
||||
debugFlags []string
|
||||
skipIPForwardingCheck bool
|
||||
pinger Pinger
|
||||
polc policyclient.Client // always non-nil
|
||||
popBrowser func(url string) // 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)
|
||||
closedCtx context.Context // alive until Direct.Close is called
|
||||
closeCtx context.CancelFunc // cancels closedCtx
|
||||
httpc *http.Client // HTTP client used to talk to tailcontrol
|
||||
interceptedDial *atomic.Bool // if non-nil, pointer to bool whether ScreenTime intercepted our dial
|
||||
dialer *tsdial.Dialer
|
||||
dnsCache *dnscache.Resolver
|
||||
controlKnobs *controlknobs.Knobs // always non-nil
|
||||
serverURL string // URL of the tailcontrol server
|
||||
clock tstime.Clock
|
||||
logf logger.Logf
|
||||
netMon *netmon.Monitor // non-nil
|
||||
health *health.Tracker
|
||||
discoPubKey key.DiscoPublic
|
||||
busClient *eventbus.Client
|
||||
clientVersionPub *eventbus.Publisher[tailcfg.ClientVersion]
|
||||
autoUpdatePub *eventbus.Publisher[AutoUpdate]
|
||||
controlTimePub *eventbus.Publisher[ControlTime]
|
||||
getMachinePrivKey func() (key.MachinePrivate, error)
|
||||
debugFlags []string
|
||||
skipIPForwardingCheck bool
|
||||
pinger Pinger
|
||||
popBrowser func(url string) // or nil
|
||||
polc policyclient.Client // always non-nil
|
||||
c2nHandler http.Handler // or nil
|
||||
panicOnUse bool // if true, panic if client is used (for testing)
|
||||
closedCtx context.Context // alive until Direct.Close is called
|
||||
closeCtx context.CancelFunc // cancels closedCtx
|
||||
|
||||
dialPlan ControlDialPlanner // can be nil
|
||||
|
||||
@@ -107,6 +110,8 @@ type Direct struct {
|
||||
tkaHead string
|
||||
lastPingURL string // last PingRequest.URL received, for dup suppression
|
||||
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)
|
||||
@@ -120,26 +125,24 @@ type Observer interface {
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
Persist persist.Persist // initial persistent data
|
||||
GetMachinePrivateKey func() (key.MachinePrivate, error) // returns the machine key to use
|
||||
ServerURL string // URL of the tailcontrol server
|
||||
AuthKey string // optional node auth key for auto registration
|
||||
Clock tstime.Clock
|
||||
Hostinfo *tailcfg.Hostinfo // non-nil passes ownership, nil means to use default using os.Hostname, etc
|
||||
DiscoPublicKey key.DiscoPublic
|
||||
PolicyClient policyclient.Client // or nil for none
|
||||
Logf logger.Logf
|
||||
HTTPTestClient *http.Client // optional HTTP client to use (for tests only)
|
||||
NoiseTestClient *http.Client // optional HTTP client to use for noise RPCs (tests only)
|
||||
DebugFlags []string // debug settings to send to control
|
||||
HealthTracker *health.Tracker
|
||||
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
|
||||
C2NHandler http.Handler // or nil
|
||||
ControlKnobs *controlknobs.Knobs // or nil to ignore
|
||||
Persist persist.Persist // initial persistent data
|
||||
GetMachinePrivateKey func() (key.MachinePrivate, error) // returns the machine key to use
|
||||
ServerURL string // URL of the tailcontrol server
|
||||
AuthKey string // optional node auth key for auto registration
|
||||
Clock tstime.Clock
|
||||
Hostinfo *tailcfg.Hostinfo // non-nil passes ownership, nil means to use default using os.Hostname, etc
|
||||
DiscoPublicKey key.DiscoPublic
|
||||
PolicyClient policyclient.Client // or nil for none
|
||||
Logf logger.Logf
|
||||
HTTPTestClient *http.Client // optional HTTP client to use (for tests only)
|
||||
NoiseTestClient *http.Client // optional HTTP client to use for noise RPCs (tests only)
|
||||
DebugFlags []string // debug settings to send to control
|
||||
HealthTracker *health.Tracker
|
||||
PopBrowserURL func(url string) // optional func to open browser
|
||||
Dialer *tsdial.Dialer // non-nil
|
||||
C2NHandler http.Handler // or nil
|
||||
ControlKnobs *controlknobs.Knobs // or nil to ignore
|
||||
Bus *eventbus.Bus
|
||||
|
||||
// Observer is called when there's a change in status to report
|
||||
// from the control client.
|
||||
@@ -287,33 +290,32 @@ func NewDirect(opts Options) (*Direct, error) {
|
||||
}
|
||||
|
||||
c := &Direct{
|
||||
httpc: httpc,
|
||||
interceptedDial: interceptedDial,
|
||||
controlKnobs: opts.ControlKnobs,
|
||||
getMachinePrivKey: opts.GetMachinePrivateKey,
|
||||
serverURL: opts.ServerURL,
|
||||
clock: opts.Clock,
|
||||
logf: opts.Logf,
|
||||
persist: opts.Persist.View(),
|
||||
authKey: opts.AuthKey,
|
||||
discoPubKey: opts.DiscoPublicKey,
|
||||
debugFlags: opts.DebugFlags,
|
||||
netMon: netMon,
|
||||
health: opts.HealthTracker,
|
||||
skipIPForwardingCheck: opts.SkipIPForwardingCheck,
|
||||
pinger: opts.Pinger,
|
||||
polc: cmp.Or(opts.PolicyClient, policyclient.Client(policyclient.NoPolicyClient{})),
|
||||
popBrowser: opts.PopBrowserURL,
|
||||
onClientVersion: opts.OnClientVersion,
|
||||
onTailnetDefaultAutoUpdate: opts.OnTailnetDefaultAutoUpdate,
|
||||
onControlTime: opts.OnControlTime,
|
||||
c2nHandler: opts.C2NHandler,
|
||||
dialer: opts.Dialer,
|
||||
dnsCache: dnsCache,
|
||||
dialPlan: opts.DialPlan,
|
||||
httpc: httpc,
|
||||
interceptedDial: interceptedDial,
|
||||
controlKnobs: opts.ControlKnobs,
|
||||
getMachinePrivKey: opts.GetMachinePrivateKey,
|
||||
serverURL: opts.ServerURL,
|
||||
clock: opts.Clock,
|
||||
logf: opts.Logf,
|
||||
persist: opts.Persist.View(),
|
||||
authKey: opts.AuthKey,
|
||||
discoPubKey: opts.DiscoPublicKey,
|
||||
debugFlags: opts.DebugFlags,
|
||||
netMon: netMon,
|
||||
health: opts.HealthTracker,
|
||||
skipIPForwardingCheck: opts.SkipIPForwardingCheck,
|
||||
pinger: opts.Pinger,
|
||||
polc: cmp.Or(opts.PolicyClient, policyclient.Client(policyclient.NoPolicyClient{})),
|
||||
popBrowser: opts.PopBrowserURL,
|
||||
c2nHandler: opts.C2NHandler,
|
||||
dialer: opts.Dialer,
|
||||
dnsCache: dnsCache,
|
||||
dialPlan: opts.DialPlan,
|
||||
}
|
||||
c.closedCtx, c.closeCtx = context.WithCancel(context.Background())
|
||||
|
||||
c.controlClientID = rand.Int64()
|
||||
|
||||
if opts.Hostinfo == nil {
|
||||
c.SetHostinfo(hostinfo.New())
|
||||
} 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") {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -340,6 +348,7 @@ func (c *Direct) Close() error {
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.busClient.Close()
|
||||
if c.noiseClient != nil {
|
||||
if err := c.noiseClient.Close(); err != nil {
|
||||
return err
|
||||
@@ -826,6 +835,23 @@ func (c *Direct) SendUpdate(ctx context.Context) error {
|
||||
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,
|
||||
// end the long poll. We should be receiving a keep alive ping
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
if resp.ClientVersion != nil && c.onClientVersion != nil {
|
||||
c.onClientVersion(resp.ClientVersion)
|
||||
if resp.ClientVersion != nil {
|
||||
c.clientVersionPub.Publish(*resp.ClientVersion)
|
||||
}
|
||||
if resp.ControlTime != nil && !resp.ControlTime.IsZero() {
|
||||
c.logf.JSON(1, "controltime", resp.ControlTime.UTC())
|
||||
if c.onControlTime != nil {
|
||||
c.onControlTime(*resp.ControlTime)
|
||||
}
|
||||
c.controlTimePub.Publish(ControlTime{c.controlClientID, *resp.ControlTime})
|
||||
}
|
||||
if resp.KeepAlive {
|
||||
vlogf("netmap: got keep-alive")
|
||||
@@ -1112,9 +1136,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap
|
||||
continue
|
||||
}
|
||||
if au, ok := resp.DefaultAutoUpdate.Get(); ok {
|
||||
if c.onTailnetDefaultAutoUpdate != nil {
|
||||
c.onTailnetDefaultAutoUpdate(au)
|
||||
}
|
||||
c.autoUpdatePub.Publish(AutoUpdate{c.controlClientID, au})
|
||||
}
|
||||
|
||||
metricMapResponseMap.Add(1)
|
||||
|
||||
@@ -17,12 +17,14 @@ import (
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/util/eventbus/eventbustest"
|
||||
)
|
||||
|
||||
func TestNewDirect(t *testing.T) {
|
||||
hi := hostinfo.New()
|
||||
ni := tailcfg.NetInfo{LinkType: "wired"}
|
||||
hi.NetInfo = &ni
|
||||
bus := eventbustest.NewBus(t)
|
||||
|
||||
k := key.NewMachine()
|
||||
opts := Options{
|
||||
@@ -32,6 +34,7 @@ func TestNewDirect(t *testing.T) {
|
||||
return k, nil
|
||||
},
|
||||
Dialer: tsdial.NewDialer(netmon.NewStatic()),
|
||||
Bus: bus,
|
||||
}
|
||||
c, err := NewDirect(opts)
|
||||
if err != nil {
|
||||
@@ -99,6 +102,7 @@ func TestTsmpPing(t *testing.T) {
|
||||
hi := hostinfo.New()
|
||||
ni := tailcfg.NetInfo{LinkType: "wired"}
|
||||
hi.NetInfo = &ni
|
||||
bus := eventbustest.NewBus(t)
|
||||
|
||||
k := key.NewMachine()
|
||||
opts := Options{
|
||||
@@ -108,6 +112,7 @@ func TestTsmpPing(t *testing.T) {
|
||||
return k, nil
|
||||
},
|
||||
Dialer: tsdial.NewDialer(netmon.NewStatic()),
|
||||
Bus: bus,
|
||||
}
|
||||
|
||||
c, err := NewDirect(opts)
|
||||
|
||||
Reference in New Issue
Block a user