diff --git a/cmd/tailscale/cli/cli.go b/cmd/tailscale/cli/cli.go index ce41e5965..e46757d16 100644 --- a/cmd/tailscale/cli/cli.go +++ b/cmd/tailscale/cli/cli.go @@ -23,11 +23,13 @@ import ( "tailscale.com/client/tailscale" "tailscale.com/envknob" "tailscale.com/paths" + "tailscale.com/tstime" "tailscale.com/version/distro" ) var Stderr io.Writer = os.Stderr var Stdout io.Writer = os.Stdout +var clock tstime.Clock = &tstime.StdClock{} // global tstime.clock variable for tailscale cli package. func errf(format string, a ...any) { fmt.Fprintf(Stderr, format, a...) diff --git a/cmd/tailscale/cli/debug.go b/cmd/tailscale/cli/debug.go index 851ab5a93..70e2032ce 100644 --- a/cmd/tailscale/cli/debug.go +++ b/cmd/tailscale/cli/debug.go @@ -37,6 +37,7 @@ import ( "tailscale.com/paths" "tailscale.com/safesocket" "tailscale.com/tailcfg" + "tailscale.com/tstime" "tailscale.com/types/key" "tailscale.com/types/logger" "tailscale.com/util/must" @@ -678,6 +679,7 @@ func runTS2021(ctx context.Context, args []string) error { ProtocolVersion: uint16(ts2021Args.version), Dialer: dialFunc, Logf: logf, + Clock: &tstime.StdClock{}, }).Dial(ctx) log.Printf("controlhttp.Dial = %p, %v", conn, err) if err != nil { diff --git a/cmd/tailscale/cli/netcheck.go b/cmd/tailscale/cli/netcheck.go index bac5a0a48..6b9f7c0d0 100644 --- a/cmd/tailscale/cli/netcheck.go +++ b/cmd/tailscale/cli/netcheck.go @@ -22,6 +22,7 @@ import ( "tailscale.com/net/netmon" "tailscale.com/net/portmapper" "tailscale.com/tailcfg" + "tailscale.com/tstime" "tailscale.com/types/logger" ) @@ -54,7 +55,8 @@ func runNetcheck(ctx context.Context, args []string) error { c := &netcheck.Client{ UDPBindAddr: envknob.String("TS_DEBUG_NETCHECK_UDP_BIND"), PortMapper: portmapper.NewClient(logf, netMon, nil, nil), - UseDNSCache: false, // always resolve, don't cache + UseDNSCache: false, // always resolve, don't cache, + Clock: &tstime.StdClock{}, } if netcheckArgs.verbose { c.Logf = logger.WithPrefix(log.Printf, "netcheck: ") diff --git a/cmd/tailscale/cli/up.go b/cmd/tailscale/cli/up.go index 06d9b5ff8..3f4096bf8 100644 --- a/cmd/tailscale/cli/up.go +++ b/cmd/tailscale/cli/up.go @@ -37,6 +37,7 @@ import ( "tailscale.com/net/tsaddr" "tailscale.com/safesocket" "tailscale.com/tailcfg" + "tailscale.com/tstime" "tailscale.com/types/logger" "tailscale.com/types/preftype" "tailscale.com/util/dnsname" @@ -692,9 +693,9 @@ func runUp(ctx context.Context, cmd string, args []string, upArgs upArgsT) (retE // shuts down. (Issue 2333) var timeoutCh <-chan time.Time if upArgs.timeout > 0 { - timeoutTimer := time.NewTimer(upArgs.timeout) + var timeoutTimer tstime.TimerController + timeoutTimer, timeoutCh = clock.NewTimer(upArgs.timeout) defer timeoutTimer.Stop() - timeoutCh = timeoutTimer.C } select { case <-running: diff --git a/control/controlclient/auto.go b/control/controlclient/auto.go index dab37a510..7c143980d 100644 --- a/control/controlclient/auto.go +++ b/control/controlclient/auto.go @@ -15,6 +15,7 @@ import ( "tailscale.com/logtail/backoff" "tailscale.com/net/sockstats" "tailscale.com/tailcfg" + "tailscale.com/tstime" "tailscale.com/types/empty" "tailscale.com/types/key" "tailscale.com/types/logger" @@ -48,7 +49,7 @@ var _ Client = (*Auto)(nil) // It's a concrete implementation of the Client interface. type Auto struct { direct *Direct // our interface to the server APIs - timeNow func() time.Time + clock tstime.Clock logf logger.Logf expiry *time.Time closed bool @@ -107,12 +108,12 @@ func NewNoStart(opts Options) (_ *Auto, err error) { if opts.Logf == nil { opts.Logf = func(fmt string, args ...any) {} } - if opts.TimeNow == nil { - opts.TimeNow = time.Now + if opts.Clock == nil { + opts.Clock = &tstime.StdClock{} } c := &Auto{ direct: direct, - timeNow: opts.TimeNow, + clock: opts.Clock, logf: opts.Logf, newMapCh: make(chan struct{}, 1), quit: make(chan struct{}), @@ -702,14 +703,14 @@ func (c *Auto) Logout(ctx context.Context) error { c.mu.Unlock() c.cancelAuth() - timer := time.NewTimer(10 * time.Second) + timer, timerChannel := c.clock.NewTimer(10 * time.Second) defer timer.Stop() select { case err := <-errc: return err case <-ctx.Done(): return ctx.Err() - case <-timer.C: + case <-timerChannel: return context.DeadlineExceeded } } @@ -770,7 +771,7 @@ func (c *Auto) TestOnlySetAuthKey(authkey string) { } func (c *Auto) TestOnlyTimeNow() time.Time { - return c.timeNow() + return c.clock.Now() } // SetDNS sends the SetDNSRequest request to the control plane server, diff --git a/control/controlclient/direct.go b/control/controlclient/direct.go index 006f2614a..54d5ea895 100644 --- a/control/controlclient/direct.go +++ b/control/controlclient/direct.go @@ -45,6 +45,7 @@ import ( "tailscale.com/syncs" "tailscale.com/tailcfg" "tailscale.com/tka" + "tailscale.com/tstime" "tailscale.com/types/key" "tailscale.com/types/logger" "tailscale.com/types/netmap" @@ -63,7 +64,7 @@ type Direct struct { dialer *tsdial.Dialer dnsCache *dnscache.Resolver serverURL string // URL of the tailcontrol server - timeNow func() time.Time + clock tstime.Clock lastPrintMap time.Time newDecompressor func() (Decompressor, error) keepAlive bool @@ -105,8 +106,8 @@ type Options struct { 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 - TimeNow func() time.Time // time.Now implementation used by Client - Hostinfo *tailcfg.Hostinfo // non-nil passes ownership, nil means to use default using os.Hostname, etc + Clock tstime.Clock + Hostinfo *tailcfg.Hostinfo // non-nil passes ownership, nil means to use default using os.Hostname, etc DiscoPublicKey key.DiscoPublic NewDecompressor func() (Decompressor, error) KeepAlive bool @@ -191,8 +192,8 @@ func NewDirect(opts Options) (*Direct, error) { if err != nil { return nil, err } - if opts.TimeNow == nil { - opts.TimeNow = time.Now + if opts.Clock == nil { + opts.Clock = &tstime.StdClock{} } if opts.Logf == nil { // TODO(apenwarr): remove this default and fail instead. @@ -235,7 +236,7 @@ func NewDirect(opts Options) (*Direct, error) { httpc: httpc, getMachinePrivKey: opts.GetMachinePrivateKey, serverURL: opts.ServerURL, - timeNow: opts.TimeNow, + clock: opts.Clock, logf: opts.Logf, newDecompressor: opts.NewDecompressor, keepAlive: opts.KeepAlive, @@ -432,7 +433,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new authKey, isWrapped, wrappedSig, wrappedKey := decodeWrappedAuthkey(c.authKey, c.logf) hi := c.hostInfoLocked() backendLogID := hi.BackendLogID - expired := c.expiry != nil && !c.expiry.IsZero() && c.expiry.Before(c.timeNow()) + expired := c.expiry != nil && !c.expiry.IsZero() && c.expiry.Before(c.clock.Now()) c.mu.Unlock() machinePrivKey, err := c.getMachinePrivKey() @@ -947,7 +948,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool return nil } - timeout := time.NewTimer(pollTimeout) + timeout, timeoutChannel := c.clock.NewTimer(pollTimeout) timeoutReset := make(chan struct{}) pollDone := make(chan struct{}) defer close(pollDone) @@ -957,14 +958,14 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool case <-pollDone: vlogf("netmap: ending timeout goroutine") return - case <-timeout.C: + case <-timeoutChannel: c.logf("map response long-poll timed out!") cancel() return case <-timeoutReset: if !timeout.Stop() { select { - case <-timeout.C: + case <-timeoutChannel: case <-pollDone: vlogf("netmap: ending timeout goroutine") return @@ -1089,7 +1090,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool go dumpGoroutinesToURL(c.httpc, resp.Debug.GoroutineDumpURL) } if sleep := time.Duration(resp.Debug.SleepSeconds * float64(time.Second)); sleep > 0 { - if err := sleepAsRequested(ctx, c.logf, timeoutReset, sleep); err != nil { + if err := sleepAsRequested(ctx, c.logf, timeoutReset, sleep, c.clock); err != nil { return err } } @@ -1119,7 +1120,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool // This is handy for debugging, and our logs processing // pipeline depends on it. (TODO: Remove this dependency.) // Code elsewhere prints netmap diffs every time they are received. - now := c.timeNow() + now := c.clock.Now() if now.Sub(c.lastPrintMap) >= 5*time.Minute { c.lastPrintMap = now c.logf("[v1] new network map[%d]:\n%s", i, nm.VeryConcise()) @@ -1459,7 +1460,7 @@ func answerC2NPing(logf logger.Logf, c2nHandler http.Handler, c *http.Client, pr } } -func sleepAsRequested(ctx context.Context, logf logger.Logf, timeoutReset chan<- struct{}, d time.Duration) error { +func sleepAsRequested(ctx context.Context, logf logger.Logf, timeoutReset chan<- struct{}, d time.Duration, clock tstime.Clock) error { const maxSleep = 5 * time.Minute if d > maxSleep { logf("sleeping for %v, capped from server-requested %v ...", maxSleep, d) @@ -1468,20 +1469,20 @@ func sleepAsRequested(ctx context.Context, logf logger.Logf, timeoutReset chan<- logf("sleeping for server-requested %v ...", d) } - ticker := time.NewTicker(pollTimeout / 2) + ticker, tickerChannel := clock.NewTicker(pollTimeout / 2) defer ticker.Stop() - timer := time.NewTimer(d) + timer, timerChannel := clock.NewTimer(d) defer timer.Stop() for { select { case <-ctx.Done(): return ctx.Err() - case <-timer.C: + case <-timerChannel: return nil - case <-ticker.C: + case <-tickerChannel: select { case timeoutReset <- struct{}{}: - case <-timer.C: + case <-timerChannel: return nil case <-ctx.Done(): return ctx.Err() diff --git a/control/controlclient/noise.go b/control/controlclient/noise.go index 4a8335133..e3a8a9b9e 100644 --- a/control/controlclient/noise.go +++ b/control/controlclient/noise.go @@ -23,6 +23,7 @@ import ( "tailscale.com/net/netmon" "tailscale.com/net/tsdial" "tailscale.com/tailcfg" + "tailscale.com/tstime" "tailscale.com/types/key" "tailscale.com/types/logger" "tailscale.com/util/mak" @@ -450,6 +451,7 @@ func (nc *NoiseClient) dial(ctx context.Context) (*noiseConn, error) { DialPlan: dialPlan, Logf: nc.logf, NetMon: nc.netMon, + Clock: &tstime.StdClock{}, }).Dial(ctx) if err != nil { return nil, err diff --git a/control/controlhttp/client.go b/control/controlhttp/client.go index b0d91bada..facd0f1b1 100644 --- a/control/controlhttp/client.go +++ b/control/controlhttp/client.go @@ -147,13 +147,13 @@ func (a *Dialer) dial(ctx context.Context) (*ClientConn, error) { // before we do anything. if c.DialStartDelaySec > 0 { a.logf("[v2] controlhttp: waiting %.2f seconds before dialing %q @ %v", c.DialStartDelaySec, a.Hostname, c.IP) - tmr := time.NewTimer(time.Duration(c.DialStartDelaySec * float64(time.Second))) + tmr, tmrChannel := a.Clock.NewTimer(time.Duration(c.DialStartDelaySec * float64(time.Second))) defer tmr.Stop() select { case <-ctx.Done(): err = ctx.Err() return - case <-tmr.C: + case <-tmrChannel: } } @@ -319,7 +319,7 @@ func (a *Dialer) dialHost(ctx context.Context, addr netip.Addr) (*ClientConn, er // In case outbound port 80 blocked or MITM'ed poorly, start a backup timer // to dial port 443 if port 80 doesn't either succeed or fail quickly. - try443Timer := time.AfterFunc(a.httpsFallbackDelay(), func() { try(u443) }) + try443Timer := a.Clock.AfterFunc(a.httpsFallbackDelay(), func() { try(u443) }) defer try443Timer.Stop() var err80, err443 error diff --git a/control/controlhttp/constants.go b/control/controlhttp/constants.go index b838f84c4..76c76699f 100644 --- a/control/controlhttp/constants.go +++ b/control/controlhttp/constants.go @@ -11,6 +11,7 @@ import ( "tailscale.com/net/dnscache" "tailscale.com/net/netmon" "tailscale.com/tailcfg" + "tailscale.com/tstime" "tailscale.com/types/key" "tailscale.com/types/logger" ) @@ -89,6 +90,8 @@ type Dialer struct { drainFinished chan struct{} omitCertErrorLogging bool testFallbackDelay time.Duration + + Clock tstime.Clock } func strDef(v1, v2 string) string { diff --git a/control/controlhttp/http_test.go b/control/controlhttp/http_test.go index 3cf790d8c..c5fcdf5c3 100644 --- a/control/controlhttp/http_test.go +++ b/control/controlhttp/http_test.go @@ -25,6 +25,7 @@ import ( "tailscale.com/net/socks5" "tailscale.com/net/tsdial" "tailscale.com/tailcfg" + "tailscale.com/tstest" "tailscale.com/types/key" "tailscale.com/types/logger" ) @@ -204,6 +205,7 @@ func testControlHTTP(t *testing.T, param httpTestParam) { Logf: t.Logf, omitCertErrorLogging: true, testFallbackDelay: 50 * time.Millisecond, + Clock: &tstest.Clock{}, } if proxy != nil { @@ -660,6 +662,7 @@ func TestDialPlan(t *testing.T) { drainFinished: drained, omitCertErrorLogging: true, testFallbackDelay: 50 * time.Millisecond, + Clock: &tstest.Clock{}, } conn, err := a.dial(ctx) diff --git a/derp/derp_test.go b/derp/derp_test.go index 62d4e0a00..6aa9e4a59 100644 --- a/derp/derp_test.go +++ b/derp/derp_test.go @@ -27,6 +27,7 @@ import ( "golang.org/x/time/rate" "tailscale.com/disco" "tailscale.com/net/memnet" + "tailscale.com/tstest" "tailscale.com/types/key" "tailscale.com/types/logger" ) @@ -267,6 +268,7 @@ func TestSendRecv(t *testing.T) { } func TestSendFreeze(t *testing.T) { + clock := &tstest.Clock{} serverPrivateKey := key.NewNode() s := NewServer(serverPrivateKey, t.Logf) defer s.Close() @@ -398,14 +400,14 @@ func TestSendFreeze(t *testing.T) { } drain := func(t *testing.T, name string) bool { t.Helper() - timer := time.NewTimer(1 * time.Second) + timer, timerChannel := clock.NewTimer(1 * time.Second) defer timer.Stop() // Ensure ch has at least one element. ch := chs(name) select { case <-ch: - case <-timer.C: + case <-timerChannel: t.Errorf("no packet received by %s", name) return false } diff --git a/derp/derphttp/derphttp_client.go b/derp/derphttp/derphttp_client.go index 07317fcbf..89cad7111 100644 --- a/derp/derphttp/derphttp_client.go +++ b/derp/derphttp/derphttp_client.go @@ -38,6 +38,7 @@ import ( "tailscale.com/net/tshttpproxy" "tailscale.com/syncs" "tailscale.com/tailcfg" + "tailscale.com/tstime" "tailscale.com/types/key" "tailscale.com/types/logger" "tailscale.com/util/cmpx" @@ -83,6 +84,7 @@ type Client struct { serverPubKey key.NodePublic tlsState *tls.ConnectionState pingOut map[derp.PingMessage]chan<- bool // chan to send to on pong + clock tstime.Clock } func (c *Client) String() string { @@ -101,6 +103,7 @@ func NewRegionClient(privateKey key.NodePrivate, logf logger.Logf, netMon *netmo getRegion: getRegion, ctx: ctx, cancelCtx: cancel, + clock: &tstime.StdClock{}, } return c } @@ -108,7 +111,7 @@ func NewRegionClient(privateKey key.NodePrivate, logf logger.Logf, netMon *netmo // NewNetcheckClient returns a Client that's only able to have its DialRegionTLS method called. // It's used by the netcheck package. func NewNetcheckClient(logf logger.Logf) *Client { - return &Client{logf: logf} + return &Client{logf: logf, clock: &tstime.StdClock{}} } // NewClient returns a new DERP-over-HTTP client. It connects lazily. @@ -129,6 +132,7 @@ func NewClient(privateKey key.NodePrivate, serverURL string, logf logger.Logf) ( url: u, ctx: ctx, cancelCtx: cancel, + clock: &tstime.StdClock{}, } return c, nil } @@ -644,14 +648,14 @@ func (c *Client) dialNode(ctx context.Context, n *tailcfg.DERPNode) (net.Conn, e nwait++ go func() { if proto == "tcp4" && c.preferIPv6() { - t := time.NewTimer(200 * time.Millisecond) + t, tChannel := c.clock.NewTimer(200 * time.Millisecond) select { case <-ctx.Done(): // Either user canceled original context, // it timed out, or the v6 dial succeeded. t.Stop() return - case <-t.C: + case <-tChannel: // Start v4 dial } } diff --git a/derp/derphttp/mesh_client.go b/derp/derphttp/mesh_client.go index 4454136ab..eb74a6fd9 100644 --- a/derp/derphttp/mesh_client.go +++ b/derp/derphttp/mesh_client.go @@ -91,11 +91,11 @@ func (c *Client) RunWatchConnectionLoop(ctx context.Context, ignoreServerKey key } sleep := func(d time.Duration) { - t := time.NewTimer(d) + t, tChannel := c.clock.NewTimer(d) select { case <-ctx.Done(): t.Stop() - case <-t.C: + case <-tChannel: } } diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index 86a550a05..24904c7ea 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -60,6 +60,7 @@ import ( "tailscale.com/tailcfg" "tailscale.com/tka" "tailscale.com/tsd" + "tailscale.com/tstime" "tailscale.com/types/dnstype" "tailscale.com/types/empty" "tailscale.com/types/key" @@ -259,6 +260,7 @@ type LocalBackend struct { // tkaSyncLock MUST be taken before mu (or inversely, mu must not be held // at the moment that tkaSyncLock is taken). tkaSyncLock sync.Mutex + clock tstime.Clock } // clientGen is a func that creates a control plane client. @@ -311,6 +313,7 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, sys *tsd.System, lo em: newExpiryManager(logf), gotPortPollRes: make(chan struct{}), loginFlags: loginFlags, + clock: &tstime.StdClock{}, } netMon := sys.NetMon.Get() @@ -1380,12 +1383,12 @@ func (b *LocalBackend) Start(opts ipn.Options) error { // HTTP request (via doSetHostinfoFilterServices > // cli.SetHostinfo). In practice this is very quick. t0 := time.Now() - timer := time.NewTimer(time.Second) + timer, timerChannel := b.clock.NewTimer(time.Second) select { case <-b.gotPortPollRes: b.logf("[v1] got initial portlist info in %v", time.Since(t0).Round(time.Millisecond)) timer.Stop() - case <-timer.C: + case <-timerChannel: b.logf("timeout waiting for initial portlist") } }) diff --git a/logtail/backoff/backoff.go b/logtail/backoff/backoff.go index ffec64ecd..951359a0f 100644 --- a/logtail/backoff/backoff.go +++ b/logtail/backoff/backoff.go @@ -9,6 +9,7 @@ import ( "math/rand" "time" + "tailscale.com/tstime" "tailscale.com/types/logger" ) @@ -23,10 +24,7 @@ type Backoff struct { // logf is the function used for log messages when backing off. logf logger.Logf - // NewTimer is the function that acts like time.NewTimer. - // It's for use in unit tests. - NewTimer func(time.Duration) *time.Timer - + Clock tstime.Clock // use tstime.Clock for NewTimer // LogLongerThan sets the minimum time of a single backoff interval // before we mention it in the log. LogLongerThan time.Duration @@ -40,7 +38,7 @@ func NewBackoff(name string, logf logger.Logf, maxBackoff time.Duration) *Backof name: name, logf: logf, maxBackoff: maxBackoff, - NewTimer: time.NewTimer, + Clock: &tstime.StdClock{}, } } @@ -72,10 +70,10 @@ func (b *Backoff) BackOff(ctx context.Context, err error) { if d >= b.LogLongerThan { b.logf("%s: [v1] backoff: %d msec", b.name, d.Milliseconds()) } - t := b.NewTimer(d) + t, tChannel := b.Clock.NewTimer(d) select { case <-ctx.Done(): t.Stop() - case <-t.C: + case <-tChannel: } } diff --git a/net/dns/resolver/forwarder.go b/net/dns/resolver/forwarder.go index 85670e1d6..49fc75784 100644 --- a/net/dns/resolver/forwarder.go +++ b/net/dns/resolver/forwarder.go @@ -29,6 +29,7 @@ import ( "tailscale.com/net/netns" "tailscale.com/net/sockstats" "tailscale.com/net/tsdial" + "tailscale.com/tstime" "tailscale.com/types/dnstype" "tailscale.com/types/logger" "tailscale.com/types/nettype" @@ -200,6 +201,7 @@ type forwarder struct { // /etc/resolv.conf is missing/corrupt, and the peerapi ExitDNS stub // resolver lookup. cloudHostFallback []resolverAndDelay + clock tstime.Clock } func init() { @@ -212,6 +214,7 @@ func newForwarder(logf logger.Logf, netMon *netmon.Monitor, linkSel ForwardLinkS netMon: netMon, linkSel: linkSel, dialer: dialer, + clock: &tstime.StdClock{}, } f.ctx, f.ctxCancel = context.WithCancel(context.Background()) return f @@ -695,9 +698,9 @@ func (f *forwarder) forwardWithDestChan(ctx context.Context, query packet, respo for i := range resolvers { go func(rr *resolverAndDelay) { if rr.startDelay > 0 { - timer := time.NewTimer(rr.startDelay) + timer, timerChannel := f.clock.NewTimer(rr.startDelay) select { - case <-timer.C: + case <-timerChannel: case <-ctx.Done(): timer.Stop() return diff --git a/net/dnscache/dnscache.go b/net/dnscache/dnscache.go index f3fd50fb9..a183f8497 100644 --- a/net/dnscache/dnscache.go +++ b/net/dnscache/dnscache.go @@ -22,6 +22,7 @@ import ( "tailscale.com/envknob" "tailscale.com/net/netmon" + "tailscale.com/tstime" "tailscale.com/types/logger" "tailscale.com/util/cloudenv" "tailscale.com/util/singleflight" @@ -397,6 +398,7 @@ func (d *dialer) DialContext(ctx context.Context, network, address string) (retC address: address, host: host, port: port, + clock: &tstime.StdClock{}, } defer func() { // On failure, consider that our DNS might be wrong and ask the DNS fallback mechanism for @@ -471,6 +473,7 @@ type dialCall struct { mu sync.Mutex // lock ordering: dialer.mu, then dialCall.mu fails map[netip.Addr]error // set of IPs that failed to dial thus far + clock tstime.Clock } // dnsWasTrustworthy reports whether we think the IP address(es) we @@ -585,9 +588,9 @@ func (dc *dialCall) raceDial(ctx context.Context, ips []netip.Addr) (net.Conn, e go func() { for i, ip := range ips { if i != 0 { - timer := time.NewTimer(fallbackDelay) + timer, timerChannel := dc.clock.NewTimer(fallbackDelay) select { - case <-timer.C: + case <-timerChannel: case <-failBoost: timer.Stop() case <-ctx.Done(): diff --git a/net/netcheck/netcheck.go b/net/netcheck/netcheck.go index b0a93454a..32cd00244 100644 --- a/net/netcheck/netcheck.go +++ b/net/netcheck/netcheck.go @@ -37,6 +37,7 @@ import ( "tailscale.com/net/stun" "tailscale.com/syncs" "tailscale.com/tailcfg" + "tailscale.com/tstime" "tailscale.com/types/logger" "tailscale.com/types/nettype" "tailscale.com/types/opt" @@ -166,8 +167,7 @@ type Client struct { // If nil, the interface will be looked up dynamically. NetMon *netmon.Monitor - // TimeNow, if non-nil, is used instead of time.Now. - TimeNow func() time.Time + Clock tstime.Clock // use tstime.Clock.Now() instead of time.Now // GetSTUNConn4 optionally provides a func to return the // connection to use for sending & receiving IPv4 packets. If @@ -1013,11 +1013,11 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (_ *Report, }(probeSet) } - stunTimer := time.NewTimer(stunProbeTimeout) + stunTimer, stunTimerChannel := c.Clock.NewTimer(stunProbeTimeout) defer stunTimer.Stop() select { - case <-stunTimer.C: + case <-stunTimerChannel: case <-ctx.Done(): case <-wg.DoneChan(): // All of our probes finished, so if we have >0 responses, we @@ -1436,8 +1436,8 @@ func (c *Client) logConciseReport(r *Report, dm *tailcfg.DERPMap) { } func (c *Client) timeNow() time.Time { - if c.TimeNow != nil { - return c.TimeNow() + if c.Clock != nil { + return c.Clock.Now() } return time.Now() } @@ -1531,9 +1531,9 @@ func (rs *reportState) runProbe(ctx context.Context, dm *tailcfg.DERPMap, probe } if probe.delay > 0 { - delayTimer := time.NewTimer(probe.delay) + delayTimer, delayTimerChannel := c.Clock.NewTimer(probe.delay) select { - case <-delayTimer.C: + case <-delayTimerChannel: case <-ctx.Done(): delayTimer.Stop() return diff --git a/net/netcheck/netcheck_test.go b/net/netcheck/netcheck_test.go index 1de3693ce..9d874fdd9 100644 --- a/net/netcheck/netcheck_test.go +++ b/net/netcheck/netcheck_test.go @@ -161,6 +161,7 @@ func TestBasic(t *testing.T) { c := &Client{ Logf: t.Logf, UDPBindAddr: "127.0.0.1:0", + Clock: &tstest.Clock{}, } ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) @@ -200,7 +201,8 @@ func TestWorksWhenUDPBlocked(t *testing.T) { dm.Regions[1].Nodes[0].STUNOnly = true c := &Client{ - Logf: t.Logf, + Logf: t.Logf, + Clock: &tstest.Clock{}, } ctx, cancel := context.WithTimeout(context.Background(), 250*time.Millisecond) defer cancel() @@ -340,10 +342,10 @@ func TestAddReportHistoryAndSetPreferredDERP(t *testing.T) { t.Run(tt.name, func(t *testing.T) { fakeTime := time.Unix(123, 0) c := &Client{ - TimeNow: func() time.Time { return fakeTime }, + Clock: tstest.NewClock(tstest.ClockOpts{Start: fakeTime}), } for _, s := range tt.steps { - fakeTime = fakeTime.Add(s.after) + c.Clock.(*tstest.Clock).Advance(s.after) c.addReportHistoryAndSetPreferredDERP(s.r) } lastReport := tt.steps[len(tt.steps)-1].r @@ -800,6 +802,7 @@ func TestNoCaptivePortalWhenUDP(t *testing.T) { // Set the delay long enough that we have time to cancel it // when our STUN probe succeeds. testCaptivePortalDelay: 10 * time.Second, + Clock: &tstest.Clock{}, } ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) diff --git a/tsnet/tsnet.go b/tsnet/tsnet.go index d819e5a79..b5587501d 100644 --- a/tsnet/tsnet.go +++ b/tsnet/tsnet.go @@ -49,6 +49,7 @@ import ( "tailscale.com/net/tsdial" "tailscale.com/smallzstd" "tailscale.com/tsd" + "tailscale.com/tstime" "tailscale.com/types/logger" "tailscale.com/types/logid" "tailscale.com/types/nettype" @@ -1029,7 +1030,8 @@ func (s *Server) listen(network, addr string, lnOn listenOn) (net.Listener, erro keys: keys, addr: addr, - conn: make(chan net.Conn), + conn: make(chan net.Conn), + clock: &tstime.StdClock{}, } s.mu.Lock() for _, key := range keys { @@ -1061,6 +1063,7 @@ type listener struct { addr string conn chan net.Conn closed bool // guarded by s.mu + clock tstime.Clock } func (ln *listener) Accept() (net.Conn, error) { @@ -1096,11 +1099,11 @@ func (ln *listener) closeLocked() error { } func (ln *listener) handle(c net.Conn) { - t := time.NewTimer(time.Second) + t, tChannel := ln.clock.NewTimer(time.Second) defer t.Stop() select { case ln.conn <- c: - case <-t.C: + case <-tChannel: // TODO(bradfitz): this isn't ideal. Think about how // we how we want to do pushback. c.Close() diff --git a/types/logger/logger.go b/types/logger/logger.go index 1df273b3e..488fbbf6e 100644 --- a/types/logger/logger.go +++ b/types/logger/logger.go @@ -21,6 +21,7 @@ import ( "context" "tailscale.com/envknob" + "tailscale.com/tstime" ) // Logf is the basic Tailscale logger type: a printf-like func. @@ -47,6 +48,8 @@ var jencPool = &sync.Pool{New: func() any { return je }} +var clock tstime.Clock = &tstime.StdClock{} + // JSON marshals v as JSON and writes it to logf formatted with the annotation to make logtail // treat it as a structured log. // @@ -259,10 +262,10 @@ func SlowLoggerWithClock(ctx context.Context, logf Logf, f time.Duration, burst // Otherwise, sleep for 2x the duration so that we don't // immediately sleep again on the next call. - tmr := time.NewTimer(2 * f) + tmr, tmrChannel := clock.NewTimer(2 * f) defer tmr.Stop() select { - case curr := <-tmr.C: + case curr := <-tmrChannel: tb.AdvanceTo(curr) case <-ctx.Done(): return diff --git a/wgengine/magicsock/magicsock.go b/wgengine/magicsock/magicsock.go index 2136bdb1d..f480f8287 100644 --- a/wgengine/magicsock/magicsock.go +++ b/wgengine/magicsock/magicsock.go @@ -673,6 +673,7 @@ func NewConn(opts Options) (*Conn, error) { SkipExternalNetwork: inTest(), PortMapper: c.portMapper, UseDNSCache: true, + Clock: &tstime.StdClock{}, } c.ignoreSTUNPackets() diff --git a/wgengine/watchdog.go b/wgengine/watchdog.go index 19505be89..476ec4dd5 100644 --- a/wgengine/watchdog.go +++ b/wgengine/watchdog.go @@ -18,6 +18,7 @@ import ( "tailscale.com/ipn/ipnstate" "tailscale.com/net/dns" "tailscale.com/tailcfg" + "tailscale.com/tstime" "tailscale.com/types/key" "tailscale.com/types/netmap" "tailscale.com/wgengine/capture" @@ -40,6 +41,7 @@ func NewWatchdog(e Engine) Engine { fatalf: log.Fatalf, maxWait: 45 * time.Second, inFlight: make(map[inFlightKey]time.Time), + clock: &tstime.StdClock{}, } } @@ -58,6 +60,8 @@ type watchdogEngine struct { inFlightMu sync.Mutex inFlight map[inFlightKey]time.Time inFlightCtr uint64 + + clock tstime.Clock } func (e *watchdogEngine) watchdogErr(name string, fn func() error) error { @@ -82,12 +86,12 @@ func (e *watchdogEngine) watchdogErr(name string, fn func() error) error { go func() { errCh <- fn() }() - t := time.NewTimer(e.maxWait) + t, tChannel := e.clock.NewTimer(e.maxWait) select { case err := <-errCh: t.Stop() return err - case <-t.C: + case <-tChannel: buf := new(strings.Builder) pprof.Lookup("goroutine").WriteTo(buf, 1) e.logf("wgengine watchdog stacks:\n%s", buf.String())