diff options
| author | Claire Wang <claire@tailscale.com> | 2023-08-23 10:40:42 -0400 |
|---|---|---|
| committer | Claire Wang <claire@tailscale.com> | 2023-08-23 10:40:42 -0400 |
| commit | 7b419461eb2c99b86b52e611221028e58af2203b (patch) | |
| tree | 717732c44a96aac5cb61ee38ef4758c6dba77b50 | |
| parent | 000c0a70f676814496d6adecf54cfcd23fe0c121 (diff) | |
| download | tailscale-clairew/tstime-net.tar.xz tailscale-clairew/tstime-net.zip | |
net: use tstimeclairew/tstime-net
Updates #8587
Signed-off-by: Claire Wang <claire@tailscale.com>
32 files changed, 170 insertions, 116 deletions
diff --git a/net/connstats/stats.go b/net/connstats/stats.go index dbcd946b8..092710d56 100644 --- a/net/connstats/stats.go +++ b/net/connstats/stats.go @@ -14,6 +14,7 @@ import ( "golang.org/x/sync/errgroup" "tailscale.com/net/packet" "tailscale.com/net/tsaddr" + "tailscale.com/tstime" "tailscale.com/types/netlogtype" ) @@ -30,6 +31,7 @@ type Statistics struct { shutdownCtx context.Context shutdown context.CancelFunc group errgroup.Group + clock tstime.Clock } type connCnts struct { @@ -45,7 +47,7 @@ type connCnts struct { // The dump function is called from a single goroutine. // Shutdown must be called to cleanup resources. func NewStatistics(maxPeriod time.Duration, maxConns int, dump func(start, end time.Time, virtual, physical map[netlogtype.Connection]netlogtype.Counts)) *Statistics { - s := &Statistics{maxConns: maxConns} + s := &Statistics{maxConns: maxConns, clock: tstime.StdClock{}} s.connCntsCh = make(chan connCnts, 256) s.shutdownCtx, s.shutdown = context.WithCancel(context.Background()) s.group.Go(func() error { @@ -53,9 +55,10 @@ func NewStatistics(maxPeriod time.Duration, maxConns int, dump func(start, end t // where waking up a process every maxPeriod when there is no activity // is a drain on battery life. Switch this instead to instead use // a time.Timer that is triggered upon network activity. - ticker := new(time.Ticker) + var ticker tstime.TickerController + var tickerChannel <-chan time.Time if maxPeriod > 0 { - ticker = time.NewTicker(maxPeriod) + ticker, tickerChannel = s.clock.NewTicker(maxPeriod) defer ticker.Stop() } @@ -63,7 +66,7 @@ func NewStatistics(maxPeriod time.Duration, maxConns int, dump func(start, end t var cc connCnts select { case cc = <-s.connCntsCh: - case <-ticker.C: + case <-tickerChannel: cc = s.extract() case <-s.shutdownCtx.Done(): cc = s.extract() @@ -182,7 +185,7 @@ func (s *Statistics) preInsertConn() bool { // Initialize the maps if nil. if s.virtual == nil && s.physical == nil { - s.start = time.Now().UTC() + s.start = s.clock.Now().UTC() s.virtual = make(map[netlogtype.Connection]netlogtype.Counts) s.physical = make(map[netlogtype.Connection]netlogtype.Counts) } @@ -200,7 +203,7 @@ func (s *Statistics) extractLocked() connCnts { if len(s.virtual)+len(s.physical) == 0 { return connCnts{} } - s.end = time.Now().UTC() + s.end = s.clock.Now().UTC() cc := s.connCnts s.connCnts = connCnts{} return cc diff --git a/net/dns/direct.go b/net/dns/direct.go index e9279d13a..95e1174c9 100644 --- a/net/dns/direct.go +++ b/net/dns/direct.go @@ -22,11 +22,14 @@ import ( "tailscale.com/health" "tailscale.com/net/dns/resolvconffile" + "tailscale.com/tstime" "tailscale.com/types/logger" "tailscale.com/util/dnsname" "tailscale.com/version/distro" ) +var clock = tstime.StdClock{} + // writeResolvConf writes DNS configuration in resolv.conf format to the given writer. func writeResolvConf(w io.Writer, servers []netip.Addr, domains []dnsname.FQDN) error { c := &resolvconffile.Config{ @@ -387,9 +390,9 @@ func (m *directManager) SetDNS(config OSConfig) (err error) { // cause a disruptive DNS outage each time we reset an empty // OS configuration. if changed && isResolvedRunning() && !runningAsGUIDesktopUser() { - t0 := time.Now() + t0 := clock.Now() err := restartResolved() - d := time.Since(t0).Round(time.Millisecond) + d := clock.Since(t0).Round(time.Millisecond) if err != nil { m.logf("error restarting resolved after %v: %v", d, err) } else { diff --git a/net/dns/manager.go b/net/dns/manager.go index f177b5777..8d5cb38d2 100644 --- a/net/dns/manager.go +++ b/net/dns/manager.go @@ -355,7 +355,7 @@ func (s *dnsTCPSession) handleWrites() { return // connection closed or timeout, teardown time case resp := <-s.responses: - s.conn.SetWriteDeadline(time.Now().Add(idleTimeoutTCP)) + s.conn.SetWriteDeadline(clock.Now().Add(idleTimeoutTCP)) if err := binary.Write(s.conn, binary.BigEndian, uint16(len(resp))); err != nil { s.m.logf("tcp write (len): %v", err) return @@ -392,7 +392,7 @@ func (s *dnsTCPSession) handleReads() { return default: - s.conn.SetReadDeadline(time.Now().Add(idleTimeoutTCP)) + s.conn.SetReadDeadline(clock.Now().Add(idleTimeoutTCP)) var reqLen uint16 if err := binary.Read(s.conn, binary.BigEndian, &reqLen); err != nil { if err == io.EOF || err == io.ErrClosedPipe { diff --git a/net/dns/manager_windows.go b/net/dns/manager_windows.go index 4c05b7718..ef27b9615 100644 --- a/net/dns/manager_windows.go +++ b/net/dns/manager_windows.go @@ -342,24 +342,24 @@ func (m *windowsManager) SetDNS(cfg OSConfig) error { // any cached split-horizon queries that are no longer the correct // answer. go func() { - t0 := time.Now() + t0 := clock.Now() m.logf("running ipconfig /registerdns ...") cmd := exec.Command("ipconfig", "/registerdns") cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} err := cmd.Run() - d := time.Since(t0).Round(time.Millisecond) + d := clock.Since(t0).Round(time.Millisecond) if err != nil { m.logf("error running ipconfig /registerdns after %v: %v", d, err) } else { m.logf("ran ipconfig /registerdns in %v", d) } - t0 = time.Now() + t0 = clock.Now() m.logf("running ipconfig /flushdns ...") cmd = exec.Command("ipconfig", "/flushdns") cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} err = cmd.Run() - d = time.Since(t0).Round(time.Millisecond) + d = clock.Since(t0).Round(time.Millisecond) if err != nil { m.logf("error running ipconfig /flushdns after %v: %v", d, err) } else { diff --git a/net/dns/recursive/recursive.go b/net/dns/recursive/recursive.go index 5b585483c..023638319 100644 --- a/net/dns/recursive/recursive.go +++ b/net/dns/recursive/recursive.go @@ -17,6 +17,7 @@ import ( "github.com/miekg/dns" "tailscale.com/envknob" "tailscale.com/net/netns" + "tailscale.com/tstime" "tailscale.com/types/logger" "tailscale.com/util/dnsname" "tailscale.com/util/mak" @@ -96,6 +97,8 @@ var rootServersV6 = []netip.Addr{ var debug = envknob.RegisterBool("TS_DEBUG_RECURSIVE_DNS") +var clock = tstime.StdClock{} + // Resolver is a recursive DNS resolver that is designed for looking up A and AAAA records. type Resolver struct { // Dialer is used to create outbound connections. If nil, a zero @@ -114,7 +117,7 @@ type Resolver struct { testQueryHook func(name dnsname.FQDN, nameserver netip.Addr, protocol string, qtype dns.Type) (*dns.Msg, error) testExchangeHook func(nameserver netip.Addr, network string, msg *dns.Msg) (*dns.Msg, error) rootServers []netip.Addr - timeNow func() time.Time + clock tstime.Clock // Caching // NOTE(andrew): if we make resolution parallel, this needs a mutex @@ -150,10 +153,10 @@ type dnsMsgWithExpiry struct { } func (r *Resolver) now() time.Time { - if r.timeNow != nil { - return r.timeNow() + if r.clock != nil { + return r.clock.Now() } - return time.Now() + return clock.Now() } func (r *Resolver) logf(format string, args ...any) { diff --git a/net/dns/recursive/recursive_test.go b/net/dns/recursive/recursive_test.go index 0bfba383a..136e0aad7 100644 --- a/net/dns/recursive/recursive_test.go +++ b/net/dns/recursive/recursive_test.go @@ -41,8 +41,8 @@ func newResolver(tb testing.TB) *Resolver { Step: 50 * time.Millisecond, }) return &Resolver{ - Logf: tb.Logf, - timeNow: clock.Now, + Logf: tb.Logf, + clock: clock, } } diff --git a/net/dns/resolver/debug.go b/net/dns/resolver/debug.go index f334af5c9..55c7cc317 100644 --- a/net/dns/resolver/debug.go +++ b/net/dns/resolver/debug.go @@ -54,7 +54,7 @@ func (fl *fwdLog) addName(name string) { if len(fl.ent) == 0 { return } - fl.ent[fl.pos] = fwdLogEntry{Domain: name, Time: time.Now()} + fl.ent[fl.pos] = fwdLogEntry{Domain: name, Time: clock.Now()} fl.pos++ if fl.pos == len(fl.ent) { fl.pos = 0 @@ -66,7 +66,7 @@ func (fl *fwdLog) ServeHTTP(w http.ResponseWriter, r *http.Request) { defer fl.mu.Unlock() fmt.Fprintf(w, "<html><h1>DNS forwards</h1>") - now := time.Now() + now := clock.Now() for i := 0; i < len(fl.ent); i++ { ent := fl.ent[(i+fl.pos)%len(fl.ent)] if ent.Domain == "" { diff --git a/net/dns/resolver/forwarder.go b/net/dns/resolver/forwarder.go index 85670e1d6..ce157db74 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" @@ -202,8 +203,10 @@ type forwarder struct { cloudHostFallback []resolverAndDelay } +var clock = tstime.StdClock{} + func init() { - rand.Seed(time.Now().UnixNano()) + rand.Seed(clock.Now().UnixNano()) } func newForwarder(logf logger.Logf, netMon *netmon.Monitor, linkSel ForwardLinkSelector, dialer *tsdial.Dialer) *forwarder { @@ -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 := 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..b23878903 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" @@ -34,6 +35,8 @@ var single = &Resolver{ Forward: &net.Resolver{PreferGo: preferGoResolver()}, } +var clock = tstime.StdClock{} + func preferGoResolver() bool { // There does not appear to be a local resolver running // on iOS, and NetworkExtension is good at isolating DNS. @@ -251,7 +254,7 @@ func (r *Resolver) LookupIP(ctx context.Context, host string) (ip, v6 netip.Addr func (r *Resolver) lookupIPCache(host string) (ip, ip6 netip.Addr, allIPs []netip.Addr, ok bool) { r.mu.Lock() defer r.mu.Unlock() - if ent, ok := r.ipCache[host]; ok && ent.expires.After(time.Now()) { + if ent, ok := r.ipCache[host]; ok && ent.expires.After(clock.Now()) { return ent.ip, ent.ip6, ent.allIPs, true } return zaddr, zaddr, nil, false @@ -359,7 +362,7 @@ func (r *Resolver) addIPCache(host string, ip, ip6 netip.Addr, allIPs []netip.Ad ip: ip, ip6: ip6, allIPs: allIPs, - expires: time.Now().Add(d), + expires: clock.Now().Add(d), } } @@ -510,7 +513,7 @@ func (dc *dialCall) noteDialResult(ip netip.Addr, err error) { d := dc.d d.mu.Lock() defer d.mu.Unlock() - d.pastConnect[ip] = time.Now() + d.pastConnect[ip] = clock.Now() return } dc.mu.Lock() @@ -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 := clock.NewTimer(fallbackDelay) select { - case <-timer.C: + case <-timerChannel: case <-failBoost: timer.Stop() case <-ctx.Done(): diff --git a/net/dnscache/messagecache.go b/net/dnscache/messagecache.go index ebbf20faa..c3359221d 100644 --- a/net/dnscache/messagecache.go +++ b/net/dnscache/messagecache.go @@ -13,6 +13,7 @@ import ( "github.com/golang/groupcache/lru" "golang.org/x/net/dns/dnsmessage" + "tailscale.com/tstime" "tailscale.com/util/cmpx" ) @@ -26,8 +27,8 @@ import ( // It's safe for concurrent use. type MessageCache struct { // Clock is a clock, for testing. - // If nil, time.Now is used. - Clock func() time.Time + // If nil, clock.Now is used. + Clock tstime.Clock mu sync.Mutex cacheSizeSet int // 0 means default @@ -36,9 +37,9 @@ type MessageCache struct { func (c *MessageCache) now() time.Time { if c.Clock != nil { - return c.Clock() + return c.Clock.Now() } - return time.Now() + return clock.Now() } // SetMaxCacheSize sets the maximum number of DNS cache entries that diff --git a/net/dnscache/messagecache_test.go b/net/dnscache/messagecache_test.go index 41fc33448..68110bf28 100644 --- a/net/dnscache/messagecache_test.go +++ b/net/dnscache/messagecache_test.go @@ -21,7 +21,7 @@ func TestMessageCache(t *testing.T) { clock := tstest.NewClock(tstest.ClockOpts{ Start: time.Date(1987, 11, 1, 0, 0, 0, 0, time.UTC), }) - mc := &MessageCache{Clock: clock.Now} + mc := &MessageCache{Clock: clock} mc.SetMaxCacheSize(2) clock.Advance(time.Second) diff --git a/net/memnet/pipe.go b/net/memnet/pipe.go index 471635083..683ad92a4 100644 --- a/net/memnet/pipe.go +++ b/net/memnet/pipe.go @@ -13,6 +13,8 @@ import ( "os" "sync" "time" + + "tailscale.com/tstime" ) const debugPipe = false @@ -31,6 +33,8 @@ type Pipe struct { writeTimeout time.Time cancelReadTimer func() cancelWriteTimer func() + + clock tstime.Clock } // NewPipe creates a Pipe with a buffer size fixed at maxBuf. @@ -38,6 +42,7 @@ func NewPipe(name string, maxBuf int) *Pipe { p := &Pipe{ name: name, maxBuf: maxBuf, + clock: tstime.StdClock{}, } p.cnd = sync.NewCond(&p.mu) return p @@ -48,7 +53,7 @@ func NewPipe(name string, maxBuf int) *Pipe { func (p *Pipe) readOrBlock(b []byte) (int, error) { p.mu.Lock() defer p.mu.Unlock() - if !p.readTimeout.IsZero() && !time.Now().Before(p.readTimeout) { + if !p.readTimeout.IsZero() && !p.clock.Now().Before(p.readTimeout) { return 0, os.ErrDeadlineExceeded } if p.blocked { @@ -97,7 +102,7 @@ func (p *Pipe) writeOrBlock(b []byte) (int, error) { if p.closed { return 0, net.ErrClosed } - if !p.writeTimeout.IsZero() && !time.Now().Before(p.writeTimeout) { + if !p.writeTimeout.IsZero() && !p.clock.Now().Before(p.writeTimeout) { return 0, os.ErrDeadlineExceeded } if p.blocked { @@ -164,7 +169,7 @@ func (p *Pipe) deadlineTimer(t time.Time) func() { if t.IsZero() { return nil } - if t.Before(time.Now()) { + if t.Before(p.clock.Now()) { p.cnd.Broadcast() return nil } diff --git a/net/netcheck/netcheck.go b/net/netcheck/netcheck.go index a863a5a19..63647a6d6 100644 --- a/net/netcheck/netcheck.go +++ b/net/netcheck/netcheck.go @@ -36,6 +36,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" @@ -50,6 +51,8 @@ var ( debugNetcheck = envknob.RegisterBool("TS_DEBUG_NETCHECK") ) +var clock = tstime.StdClock{} + // The various default timeouts for things. const ( // overallProbeTimeout is the maximum amount of time netcheck will @@ -173,8 +176,7 @@ type Client struct { // present anyway. NetMon *netmon.Monitor - // TimeNow, if non-nil, is used instead of time.Now. - TimeNow func() time.Time + Clock tstime.Clock // SendPacket is required to send a packet to the specified address. For // convenience it shares a signature with WriteToUDPAddrPort. @@ -925,11 +927,11 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (_ *Report, }(probeSet) } - stunTimer := time.NewTimer(stunProbeTimeout) + stunTimer, stunTimerChannel := 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 @@ -1348,10 +1350,10 @@ 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() + return clock.Now() } const ( @@ -1490,9 +1492,9 @@ func (rs *reportState) runProbe(ctx context.Context, dm *tailcfg.DERPMap, probe } if probe.delay > 0 { - delayTimer := time.NewTimer(probe.delay) + delayTimer, delayTimerChannel := clock.NewTimer(probe.delay) select { - case <-delayTimer.C: + case <-delayTimerChannel: case <-ctx.Done(): delayTimer.Stop() return @@ -1513,11 +1515,11 @@ func (rs *reportState) runProbe(ctx context.Context, dm *tailcfg.DERPMap, probe txID := stun.NewTxID() req := stun.Request(txID) - sent := time.Now() // after DNS lookup above + sent := clock.Now() // after DNS lookup above rs.mu.Lock() rs.inFlight[txID] = func(ipp netip.AddrPort) { - rs.addNodeLatency(node, ipp, time.Since(sent)) + rs.addNodeLatency(node, ipp, clock.Since(sent)) cancelSet() // abort other nodes in this set } rs.mu.Unlock() diff --git a/net/netcheck/netcheck_test.go b/net/netcheck/netcheck_test.go index 1ed728a00..8adde53ba 100644 --- a/net/netcheck/netcheck_test.go +++ b/net/netcheck/netcheck_test.go @@ -398,12 +398,14 @@ func TestAddReportHistoryAndSetPreferredDERP(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { fakeTime := time.Unix(123, 0) + clock := tstest.NewClock(tstest.ClockOpts{Start: fakeTime}) c := &Client{ - TimeNow: func() time.Time { return fakeTime }, + Clock: clock, } dm := &tailcfg.DERPMap{HomeParams: tt.homeParams} for _, s := range tt.steps { fakeTime = fakeTime.Add(s.after) + clock.Advance(s.after) c.addReportHistoryAndSetPreferredDERP(s.r, dm.View()) } lastReport := tt.steps[len(tt.steps)-1].r diff --git a/net/netmon/netmon.go b/net/netmon/netmon.go index 4de8a47f4..9ab641831 100644 --- a/net/netmon/netmon.go +++ b/net/netmon/netmon.go @@ -15,6 +15,7 @@ import ( "time" "tailscale.com/net/interfaces" + "tailscale.com/tstime" "tailscale.com/types/logger" "tailscale.com/util/set" ) @@ -65,9 +66,10 @@ type Monitor struct { started bool closed bool goroutines sync.WaitGroup - wallTimer *time.Timer // nil until Started; re-armed AfterFunc per tick + wallTimer tstime.TimerController // nil until Started; re-armed AfterFunc per tick lastWall time.Time timeJumped bool // whether we need to send a changed=true after a big time jump + clock tstime.Clock } // ChangeFunc is a callback function registered with Monitor that's called when the @@ -81,11 +83,12 @@ type ChangeFunc func(changed bool, state *interfaces.State) func New(logf logger.Logf) (*Monitor, error) { logf = logger.WithPrefix(logf, "monitor: ") m := &Monitor{ - logf: logf, - change: make(chan struct{}, 1), - stop: make(chan struct{}), - lastWall: wallTime(), + logf: logf, + change: make(chan struct{}, 1), + stop: make(chan struct{}), + clock: tstime.StdClock{}, } + m.lastWall = m.wallTime() st, err := m.interfaceStateUncached() if err != nil { return nil, err @@ -180,7 +183,7 @@ func (m *Monitor) Start() { m.started = true if shouldMonitorTimeJump { - m.wallTimer = time.AfterFunc(pollWallTimeInterval, m.pollWallTime) + m.wallTimer = m.clock.AfterFunc(pollWallTimeInterval, m.pollWallTime) } if m.om == nil { @@ -342,10 +345,10 @@ func jsonSummary(x any) any { return j } -func wallTime() time.Time { +func (m *Monitor) wallTime() time.Time { // From time package's docs: "The canonical way to strip a // monotonic clock reading is to use t = t.Round(0)." - return time.Now().Round(0) + return m.clock.Now().Round(0) } func (m *Monitor) pollWallTime() { @@ -374,7 +377,7 @@ func (m *Monitor) checkWallTimeAdvanceLocked() bool { if !shouldMonitorTimeJump { panic("unreachable") // if callers are correct } - now := wallTime() + now := m.wallTime() if now.Sub(m.lastWall) > pollWallTimeInterval*3/2 { m.timeJumped = true // it is reset by debounce. } diff --git a/net/netmon/netmon_test.go b/net/netmon/netmon_test.go index 7d2516404..cdbd494a4 100644 --- a/net/netmon/netmon_test.go +++ b/net/netmon/netmon_test.go @@ -10,10 +10,12 @@ import ( "time" "tailscale.com/net/interfaces" + "tailscale.com/tstest" ) func TestMonitorStartClose(t *testing.T) { mon, err := New(t.Logf) + mon.clock = &tstest.Clock{} if err != nil { t.Fatal(err) } diff --git a/net/netmon/netmon_windows.go b/net/netmon/netmon_windows.go index 836922206..34e08889d 100644 --- a/net/netmon/netmon_windows.go +++ b/net/netmon/netmon_windows.go @@ -12,11 +12,13 @@ import ( "golang.zx2c4.com/wireguard/windows/tunnel/winipcfg" "tailscale.com/net/tsaddr" + "tailscale.com/tstime" "tailscale.com/types/logger" ) var ( errClosed = errors.New("closed") + clock = tstime.StdClock{} ) type eventMessage struct { @@ -104,11 +106,11 @@ func (m *winMon) Receive() (message, error) { return nil, errClosed } - t0 := time.Now() + t0 := clock.Now() select { case msg := <-m.messagec: - now := time.Now() + now := clock.Now() m.mu.Lock() sinceLast := now.Sub(m.lastLog) m.lastLog = now @@ -120,7 +122,7 @@ func (m *winMon) Receive() (message, error) { // route updates after connecting to a large tailnet // and all the IPv4 /32 routes. if sinceLast > 5*time.Second || !strings.HasPrefix(msg.eventType, "ts") { - m.logf("got windows change event after %v: evt=%s", time.Since(t0).Round(time.Millisecond), msg.eventType) + m.logf("got windows change event after %v: evt=%s", clock.Since(t0).Round(time.Millisecond), msg.eventType) } return msg, nil case <-m.ctx.Done(): diff --git a/net/netmon/polling.go b/net/netmon/polling.go index 9332bdde9..dfa068fe1 100644 --- a/net/netmon/polling.go +++ b/net/netmon/polling.go @@ -14,9 +14,12 @@ import ( "time" "tailscale.com/net/interfaces" + "tailscale.com/tstime" "tailscale.com/types/logger" ) +var clock = tstime.StdClock{} + func newPollingMon(logf logger.Logf, m *Monitor) (osMon, error) { return &pollingMon{ logf: logf, @@ -75,7 +78,7 @@ func (pm *pollingMon) Receive() (message, error) { pm.logf("monitor polling: Cloud Run detected, reduce polling interval to 24h") d = 24 * time.Hour } - ticker := time.NewTicker(d) + ticker, tickerChannel := clock.NewTicker(d) defer ticker.Stop() base := pm.m.InterfaceState() for { @@ -83,7 +86,7 @@ func (pm *pollingMon) Receive() (message, error) { return unspecifiedMessage{}, nil } select { - case <-ticker.C: + case <-tickerChannel: case <-pm.stop: return nil, errors.New("stopped") } diff --git a/net/ping/ping.go b/net/ping/ping.go index 170d87fb9..e13d03469 100644 --- a/net/ping/ping.go +++ b/net/ping/ping.go @@ -22,6 +22,7 @@ import ( "golang.org/x/net/icmp" "golang.org/x/net/ipv4" "golang.org/x/net/ipv6" + "tailscale.com/tstime" "tailscale.com/types/logger" "tailscale.com/util/mak" "tailscale.com/util/multierr" @@ -59,7 +60,7 @@ type Pinger struct { closed atomic.Bool Logf logger.Logf Verbose bool - timeNow func() time.Time + clock tstime.Clock id uint16 // uint16 per RFC 792 wg sync.WaitGroup @@ -81,11 +82,11 @@ func New(ctx context.Context, logf logger.Logf, lp ListenPacketer) *Pinger { } return &Pinger{ - lp: lp, - Logf: logf, - timeNow: time.Now, - id: binary.LittleEndian.Uint16(id[:]), - pings: make(map[uint16]outstanding), + lp: lp, + Logf: logf, + clock: tstime.StdClock{}, + id: binary.LittleEndian.Uint16(id[:]), + pings: make(map[uint16]outstanding), } } @@ -197,7 +198,7 @@ loop: continue } - p.handleResponse(buf[:n], p.timeNow(), typ) + p.handleResponse(buf[:n], p.clock.Now(), typ) } } @@ -322,7 +323,7 @@ func (p *Pinger) Send(ctx context.Context, dest net.Addr, data []byte) (time.Dur p.pings[seq] = outstanding{ch: ch, data: data} p.mu.Unlock() - start := p.timeNow() + start := p.clock.Now() n, err := conn.WriteTo(b, dest) if err != nil { return 0, err diff --git a/net/ping/ping_test.go b/net/ping/ping_test.go index bbedbcad8..e60ea0eb8 100644 --- a/net/ping/ping_test.go +++ b/net/ping/ping_test.go @@ -284,7 +284,7 @@ func (u *udpingPacketConn) WriteTo(body []byte, dest net.Addr) (int, error) { func mockPinger(t *testing.T, clock *tstest.Clock) (*Pinger, func()) { p := New(context.Background(), t.Logf, nil) - p.timeNow = clock.Now + p.clock = clock p.Verbose = true p.id = 1234 diff --git a/net/portmapper/pcp.go b/net/portmapper/pcp.go index c1d62b02b..caa06ee35 100644 --- a/net/portmapper/pcp.go +++ b/net/portmapper/pcp.go @@ -128,7 +128,7 @@ func parsePCPMapResponse(resp []byte) (*pcpMapping, error) { external := netip.AddrPortFrom(externalIP, externalPort) lifetime := time.Second * time.Duration(res.Lifetime) - now := time.Now() + now := clock.Now() mapping := &pcpMapping{ external: external, renewAfter: now.Add(lifetime / 2), diff --git a/net/portmapper/portmapper.go b/net/portmapper/portmapper.go index 2e500b555..c402b75c3 100644 --- a/net/portmapper/portmapper.go +++ b/net/portmapper/portmapper.go @@ -24,6 +24,7 @@ import ( "tailscale.com/net/netmon" "tailscale.com/net/netns" "tailscale.com/net/sockstats" + "tailscale.com/tstime" "tailscale.com/types/logger" "tailscale.com/types/nettype" "tailscale.com/util/clientmetric" @@ -93,6 +94,8 @@ type Client struct { localPort uint16 mapping mapping // non-nil if we have a mapping + + clock tstime.Clock } // mapping represents a created port-mapping over some protocol. It specifies a lease duration, @@ -117,7 +120,7 @@ type mapping interface { func (c *Client) HaveMapping() bool { c.mu.Lock() defer c.mu.Unlock() - return c.mapping != nil && c.mapping.GoodUntil().After(time.Now()) + return c.mapping != nil && c.mapping.GoodUntil().After(c.clock.Now()) } // pmpMapping is an already-created PMP mapping. @@ -170,6 +173,7 @@ func NewClient(logf logger.Logf, netMon *netmon.Monitor, debug *DebugKnobs, onCh netMon: netMon, ipAndGateway: interfaces.LikelyHomeRouterIP, onChange: onChange, + clock: clock, } if debug != nil { ret.debug = *debug @@ -305,7 +309,7 @@ func (c *Client) sawPMPRecently() bool { } func (c *Client) sawPMPRecentlyLocked() bool { - return c.pmpPubIP.IsValid() && c.pmpPubIPTime.After(time.Now().Add(-trustServiceStillAvailableDuration)) + return c.pmpPubIP.IsValid() && c.pmpPubIPTime.After(c.clock.Now().Add(-trustServiceStillAvailableDuration)) } func (c *Client) sawPCPRecently() bool { @@ -315,13 +319,13 @@ func (c *Client) sawPCPRecently() bool { } func (c *Client) sawPCPRecentlyLocked() bool { - return c.pcpSawTime.After(time.Now().Add(-trustServiceStillAvailableDuration)) + return c.pcpSawTime.After(c.clock.Now().Add(-trustServiceStillAvailableDuration)) } func (c *Client) sawUPnPRecently() bool { c.mu.Lock() defer c.mu.Unlock() - return c.uPnPSawTime.After(time.Now().Add(-trustServiceStillAvailableDuration)) + return c.uPnPSawTime.After(c.clock.Now().Add(-trustServiceStillAvailableDuration)) } // closeCloserOnContextDone starts a new goroutine to call c.Close @@ -373,7 +377,7 @@ func (c *Client) GetCachedMappingOrStartCreatingOne() (external netip.AddrPort, defer c.mu.Unlock() // Do we have an existing mapping that's valid? - now := time.Now() + now := c.clock.Now() if m := c.mapping; m != nil { if now.Before(m.GoodUntil()) { if now.After(m.RenewAfter()) { @@ -443,7 +447,7 @@ func (c *Client) createOrGetMapping(ctx context.Context) (external netip.AddrPor var prevPort uint16 // Do we have an existing mapping that's valid? - now := time.Now() + now := c.clock.Now() if m := c.mapping; m != nil { if now.Before(m.RenewAfter()) { defer c.mu.Unlock() @@ -497,7 +501,7 @@ func (c *Client) createOrGetMapping(ctx context.Context) (external netip.AddrPor } defer uc.Close() - uc.SetReadDeadline(time.Now().Add(portMapServiceTimeout)) + uc.SetReadDeadline(c.clock.Now().Add(portMapServiceTimeout)) defer closeCloserOnContextDone(ctx, uc)() pxpAddr := netip.AddrPortFrom(gw, c.pxpPort()) @@ -570,7 +574,7 @@ func (c *Client) createOrGetMapping(ctx context.Context) (external netip.AddrPor if pres.OpCode == pmpOpReply|pmpOpMapUDP { m.external = netip.AddrPortFrom(m.external.Addr(), pres.ExternalPort) d := time.Duration(pres.MappingValidSeconds) * time.Second - now := time.Now() + now := c.clock.Now() m.goodUntil = now.Add(d) m.renewAfter = now.Add(d / 2) // renew in half the time m.epoch = pres.SecondsSinceEpoch @@ -704,7 +708,7 @@ func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) { if err == nil { c.mu.Lock() defer c.mu.Unlock() - c.lastProbe = time.Now() + c.lastProbe = c.clock.Now() } }() @@ -824,7 +828,7 @@ func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) { c.logf("[v1] UPnP reply %+v, %q", meta, buf[:n]) res.UPnP = true c.mu.Lock() - c.uPnPSawTime = time.Now() + c.uPnPSawTime = c.clock.Now() if c.uPnPMeta != meta { c.logf("UPnP meta changed: %+v", meta) c.uPnPMeta = meta @@ -854,7 +858,7 @@ func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) { if pres.OpCode == pcpOpReply|pcpOpAnnounce { pcpHeard = true c.mu.Lock() - c.pcpSawTime = time.Now() + c.pcpSawTime = c.clock.Now() c.mu.Unlock() switch pres.ResultCode { case pcpCodeOK: @@ -893,7 +897,7 @@ func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) { res.PMP = true c.mu.Lock() c.pmpPubIP = pres.PublicAddr - c.pmpPubIPTime = time.Now() + c.pmpPubIPTime = c.clock.Now() c.pmpLastEpoch = pres.SecondsSinceEpoch c.mu.Unlock() continue diff --git a/net/portmapper/upnp.go b/net/portmapper/upnp.go index 34cae5840..431eeed17 100644 --- a/net/portmapper/upnp.go +++ b/net/portmapper/upnp.go @@ -24,9 +24,12 @@ import ( "github.com/tailscale/goupnp/dcps/internetgateway2" "tailscale.com/control/controlknobs" "tailscale.com/net/netns" + "tailscale.com/tstime" "tailscale.com/types/logger" ) +var clock = tstime.StdClock{} + // References: // // WANIP Connection v2: http://upnp.org/specs/gw/UPnP-gw-WANIPConnection-v2-Service.pdf @@ -278,7 +281,7 @@ func (c *Client) getUPnPPortMapping( return netip.AddrPort{}, false } - now := time.Now() + now := c.clock.Now() upnp := &upnpMapping{ gw: gw, internal: internal, diff --git a/net/proxymux/mux.go b/net/proxymux/mux.go index ff5aaff3b..bcd2810db 100644 --- a/net/proxymux/mux.go +++ b/net/proxymux/mux.go @@ -13,8 +13,12 @@ import ( "net" "sync" "time" + + "tailscale.com/tstime" ) +var clock = tstime.StdClock{} + // SplitSOCKSAndHTTP accepts connections on ln and passes connections // through to either socksListener or httpListener, depending the // first byte sent by the client. @@ -48,7 +52,7 @@ func splitSOCKSAndHTTPListener(ln net.Listener, sl, hl *listener) { } func routeConn(c net.Conn, socksListener, httpListener *listener) { - if err := c.SetReadDeadline(time.Now().Add(15 * time.Second)); err != nil { + if err := c.SetReadDeadline(clock.Now().Add(15 * time.Second)); err != nil { c.Close() return } diff --git a/net/sockstats/sockstats_tsgo.go b/net/sockstats/sockstats_tsgo.go index 37edddddf..f3828824c 100644 --- a/net/sockstats/sockstats_tsgo.go +++ b/net/sockstats/sockstats_tsgo.go @@ -13,10 +13,10 @@ import ( "sync" "sync/atomic" "syscall" - "time" "tailscale.com/net/interfaces" "tailscale.com/net/netmon" + "tailscale.com/tstime" "tailscale.com/types/logger" "tailscale.com/util/clientmetric" ) @@ -318,7 +318,7 @@ type radioMonitor struct { // startTime is the time we started tracking radio usage. startTime int64 - now func() time.Time + clock tstime.Clock } // radioSampleSize is the number of samples to store and report for cellular radio usage. @@ -329,14 +329,16 @@ const radioSampleSize = 3600 // 1 hour // Otherwise, all clients will report 100% radio usage on startup. var initStallPeriod int64 = 120 // 2 minutes +var clock = tstime.StdClock{} + var radio = &radioMonitor{ - now: time.Now, - startTime: time.Now().Unix(), + clock: clock, + startTime: clock.Now().Unix(), } // radioActivity should be called whenever network activity occurs on a cellular network interface. func (rm *radioMonitor) active() { - t := rm.now().Unix() + t := rm.clock.Now().Unix() rm.usage[t%radioSampleSize] = t } @@ -392,7 +394,7 @@ func (rm *radioMonitor) radioHighPercent() int64 { // forEachSample calls f for each sample in the past hour (or less if less time // has passed -- the evaluated period is returned, measured in seconds) func (rm *radioMonitor) forEachSample(f func(c int, isActive bool)) (periodLength int64) { - now := rm.now().Unix() + now := rm.clock.Now().Unix() periodLength = radioSampleSize if t := now - rm.startTime; t < periodLength { if t <= 0 { diff --git a/net/tlsdial/tlsdial.go b/net/tlsdial/tlsdial.go index d571d38a6..df5569017 100644 --- a/net/tlsdial/tlsdial.go +++ b/net/tlsdial/tlsdial.go @@ -20,10 +20,10 @@ import ( "os" "sync" "sync/atomic" - "time" "tailscale.com/envknob" "tailscale.com/health" + "tailscale.com/tstime" ) var counterFallbackOK int32 // atomic @@ -41,6 +41,8 @@ var debug = envknob.RegisterBool("TS_DEBUG_TLS_DIAL") // Headscale, etc. var tlsdialWarningPrinted sync.Map // map[string]bool +var clock = tstime.StdClock{} + // Config returns a tls.Config for connecting to a server. // If base is non-nil, it's cloned as the base config before // being configured and returned. @@ -166,7 +168,7 @@ func SetConfigExpectedCert(c *tls.Config, certDNSName string) { certs[i] = cert } opts := x509.VerifyOptions{ - CurrentTime: time.Now(), + CurrentTime: clock.Now(), DNSName: certDNSName, Intermediates: x509.NewCertPool(), } diff --git a/net/tshttpproxy/tshttpproxy.go b/net/tshttpproxy/tshttpproxy.go index 24b0050e9..21b859b57 100644 --- a/net/tshttpproxy/tshttpproxy.go +++ b/net/tshttpproxy/tshttpproxy.go @@ -19,6 +19,7 @@ import ( "time" "golang.org/x/net/http/httpproxy" + "tailscale.com/tstime" ) // InvalidateCache invalidates the package-level cache for ProxyFromEnvironment. @@ -37,6 +38,8 @@ var ( proxyFunc func(*url.URL) (*url.URL, error) ) +var clock = tstime.StdClock{} + func getProxyFunc() func(*url.URL) (*url.URL, error) { // Create config/proxyFunc if it's not created mu.Lock() @@ -54,7 +57,7 @@ func getProxyFunc() func(*url.URL) (*url.URL, error) { func setNoProxyUntil(d time.Duration) { mu.Lock() defer mu.Unlock() - noProxyUntil = time.Now().Add(d) + noProxyUntil = clock.Now().Add(d) } var _ = setNoProxyUntil // quiet staticcheck; Windows uses the above, more might later @@ -130,7 +133,7 @@ func ProxyFromEnvironment(req *http.Request) (*url.URL, error) { mu.Lock() noProxyTime := noProxyUntil mu.Unlock() - if time.Now().Before(noProxyTime) { + if clock.Now().Before(noProxyTime) { return nil, nil } diff --git a/net/tshttpproxy/tshttpproxy_windows.go b/net/tshttpproxy/tshttpproxy_windows.go index 06a1f5ae4..36e49f2d8 100644 --- a/net/tshttpproxy/tshttpproxy_windows.go +++ b/net/tshttpproxy/tshttpproxy_windows.go @@ -131,9 +131,9 @@ func proxyFromWinHTTP(ctx context.Context, urlStr string) (proxy *url.URL, err e } defer whi.Close() - t0 := time.Now() + t0 := clock.Now() v, err := whi.GetProxyForURL(urlStr) - td := time.Since(t0).Round(time.Millisecond) + td := clock.Since(t0).Round(time.Millisecond) if err := ctx.Err(); err != nil { log.Printf("tshttpproxy: winhttp: context canceled, ignoring GetProxyForURL(%q) after %v", urlStr, td) return nil, err diff --git a/net/tstun/ifstatus_windows.go b/net/tstun/ifstatus_windows.go index fd9fc2112..f9ace2ed8 100644 --- a/net/tstun/ifstatus_windows.go +++ b/net/tstun/ifstatus_windows.go @@ -79,9 +79,9 @@ func waitInterfaceUp(iface tun.Device, timeout time.Duration, logf logger.Logf) } defer cb.Unregister() - t0 := time.Now() + t0 := clock.Now() expires := t0.Add(timeout) - ticker := time.NewTicker(10 * time.Second) + ticker, tickerChannel := clock.NewTicker(10 * time.Second) defer ticker.Stop() for { @@ -89,19 +89,19 @@ func waitInterfaceUp(iface tun.Device, timeout time.Duration, logf logger.Logf) select { case <-iw.sig: - iw.logf("TUN interface is up after %v", time.Since(t0)) + iw.logf("TUN interface is up after %v", clock.Since(t0)) return nil - case <-ticker.C: + case <-tickerChannel: } if iw.isUp() { // Very unlikely to happen - either NotifyIpInterfaceChange doesn't work // or it came up in the same moment as tick. Indicate this in the log message. - iw.logf("TUN interface is up after %v (on poll, without notification)", time.Since(t0)) + iw.logf("TUN interface is up after %v (on poll, without notification)", clock.Since(t0)) return nil } - if expires.Before(time.Now()) { + if expires.Before(clock.Now()) { iw.logf("timeout waiting %v for TUN interface to come up", timeout) return fmt.Errorf("timeout waiting for TUN interface to come up") } diff --git a/net/tstun/tun.go b/net/tstun/tun.go index b31ffa7ca..d6c7b9cfc 100644 --- a/net/tstun/tun.go +++ b/net/tstun/tun.go @@ -14,12 +14,15 @@ import ( "time" "github.com/tailscale/wireguard-go/tun" + "tailscale.com/tstime" "tailscale.com/types/logger" ) // createTAP is non-nil on Linux. var createTAP func(tapName, bridgeName string) (tun.Device, error) +var clock = tstime.StdClock{} + // New returns a tun.Device for the requested device name, along with // the OS-dependent name that was allocated to the device. func New(logf logger.Logf, tunName string) (tun.Device, string, error) { diff --git a/net/tstun/wrap.go b/net/tstun/wrap.go index 20f54744d..3ddf3864c 100644 --- a/net/tstun/wrap.go +++ b/net/tstun/wrap.go @@ -28,6 +28,7 @@ import ( "tailscale.com/net/tsaddr" "tailscale.com/net/tstun/table" "tailscale.com/syncs" + "tailscale.com/tstime" "tailscale.com/tstime/mono" "tailscale.com/types/ipproto" "tailscale.com/types/key" @@ -94,9 +95,7 @@ type Wrapper struct { destMACAtomic syncs.AtomicValue[[6]byte] discoKey syncs.AtomicValue[key.DiscoPublic] - // timeNow, if non-nil, will be used to obtain the current time. - timeNow func() time.Time - + clock tstime.Clock // natV4Config stores the current NAT configuration. natV4Config atomic.Pointer[natV4Config] @@ -262,13 +261,13 @@ func wrap(logf logger.Logf, tdev tun.Device, isTAP bool) *Wrapper { return w } -// now returns the current time, either by calling t.timeNow if set or time.Now +// now returns the current time, either by calling t.clock.Now if set or clock.Now // if not. func (t *Wrapper) now() time.Time { - if t.timeNow != nil { - return t.timeNow() + if t.clock != nil { + return t.clock.Now() } - return time.Now() + return clock.Now() } // SetDestIPActivityFuncs sets a map of funcs to run per packet diff --git a/net/tstun/wrap_test.go b/net/tstun/wrap_test.go index f9e35beec..97284b9a1 100644 --- a/net/tstun/wrap_test.go +++ b/net/tstun/wrap_test.go @@ -818,9 +818,7 @@ func TestCaptureHook(t *testing.T) { now := time.Unix(1682085856, 0) _, w := newFakeTUN(t.Logf, true) - w.timeNow = func() time.Time { - return now - } + w.clock = tstest.NewClock(tstest.ClockOpts{Start: now}) w.InstallCaptureHook(hook) defer w.Close() |
