net/{currenttime,tlsdial}: add minimum possible time for TLS

This adds a new package, net/currenttime, which is a thin wrapper around
time.Now. If the value returned by time.Now is before a hard-coded value
baked into the binary, that hard-coded value will be returned instead.
In the case where the system has a buggy, malfunctioning, or nonexistent
RTC, this can improve the likelihood that Tailscale will be able to
establish a connection to the control plane (via TLS) and fetch the
server certificate.

As a future TODO: we should cache this value on-disk between process
starts (possibly in the state file?) so that we succeed even if the
Tailscale server certificate has already expired from the perspective of
the minimum time.

Additionally, add a GitHub workflow that bumps the current time to a new
value every 14 days, so that the value stays reasonably up-to-date in
our repository without introducing impurities into the build process.

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: If63cf28c4f188993894d3de589fd65ad447def6f
This commit is contained in:
Andrew Dunham 2025-03-10 16:38:44 -04:00
parent 69b27d2fcf
commit 5869f14e74
10 changed files with 127 additions and 0 deletions

View File

@ -0,0 +1,44 @@
name: update-current-time
on:
# allow manual execution
workflow_dispatch:
# run every 14 days
schedule:
- cron: "0 0 */14 * *"
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
update-flake:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Get access token
uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a # v2.1.0
id: generate-token
with:
app_id: ${{ secrets.LICENSING_APP_ID }}
installation_retrieval_mode: "id"
installation_retrieval_payload: ${{ secrets.LICENSING_APP_INSTALLATION_ID }}
private_key: ${{ secrets.LICENSING_APP_PRIVATE_KEY }}
- name: Send pull request
uses: peter-evans/create-pull-request@dd2324fc52d5d43c699a5636bcf19fceaa70c284 #v7.0.7
with:
token: ${{ steps.generate-token.outputs.token }}
author: Time Updater <noreply+time-updater@tailscale.com>
committer: Time Updater <noreply+time-updater@tailscale.com>
branch: time-updates
commit-message: "net/currentime: update minimum time"
title: "net/currentime: update minimum time"
body: Triggered by ${{ github.repository }}@${{ github.sha }}
signoff: true
delete-branch: true
reviewers: andrew-d

View File

@ -103,6 +103,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
tailscale.com/kube/kubetypes from tailscale.com/envknob
tailscale.com/metrics from tailscale.com/cmd/derper+
tailscale.com/net/bakedroots from tailscale.com/net/tlsdial
tailscale.com/net/currenttime from tailscale.com/net/tlsdial
tailscale.com/net/dnscache from tailscale.com/derp/derphttp
tailscale.com/net/ktimeout from tailscale.com/cmd/derper
tailscale.com/net/netaddr from tailscale.com/ipn+

View File

@ -847,6 +847,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
tailscale.com/net/bakedroots from tailscale.com/net/tlsdial+
tailscale.com/net/captivedetection from tailscale.com/ipn/ipnlocal+
tailscale.com/net/connstats from tailscale.com/net/tstun+
tailscale.com/net/currenttime from tailscale.com/net/tlsdial
tailscale.com/net/dns from tailscale.com/ipn/ipnlocal+
tailscale.com/net/dns/publicdns from tailscale.com/net/dns+
tailscale.com/net/dns/recursive from tailscale.com/net/dnsfallback

View File

@ -102,6 +102,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/metrics from tailscale.com/derp+
tailscale.com/net/bakedroots from tailscale.com/net/tlsdial
tailscale.com/net/captivedetection from tailscale.com/net/netcheck
tailscale.com/net/currenttime from tailscale.com/net/tlsdial
tailscale.com/net/dns/recursive from tailscale.com/net/dnsfallback
tailscale.com/net/dnscache from tailscale.com/control/controlhttp+
tailscale.com/net/dnsfallback from tailscale.com/control/controlhttp+

View File

@ -297,6 +297,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/net/bakedroots from tailscale.com/net/tlsdial+
tailscale.com/net/captivedetection from tailscale.com/ipn/ipnlocal+
tailscale.com/net/connstats from tailscale.com/net/tstun+
tailscale.com/net/currenttime from tailscale.com/net/tlsdial
tailscale.com/net/dns from tailscale.com/cmd/tailscaled+
tailscale.com/net/dns/publicdns from tailscale.com/net/dns+
tailscale.com/net/dns/recursive from tailscale.com/net/dnsfallback

View File

@ -0,0 +1,42 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package currenttime provides a fallback "current time" that can be used as
// the minimum possible time for things like TLS certificate verification.
//
// This ensures that if a Tailscale client's clock is wrong, it can still
// verify TLS certificates, assuming that the server certificate hasn't already
// expired from the point of view of the minimum time.
//
// In the future, we may want to consider caching the last known current time
// on-disk to improve the accuracy of this fallback.
package currenttime
import (
_ "embed"
"strconv"
"time"
)
//go:embed mintime.txt
var minTimeUnixMs string
var minCurrentTime time.Time
func init() {
ms, err := strconv.ParseInt(minTimeUnixMs, 10, 64)
if err != nil {
panic(err)
}
minCurrentTime = time.UnixMilli(int64(ms))
}
// Now returns the current time as per [time.Now], except that if it is before
// the baked-in "minimum current time", that value will be returned instead.
func Now() time.Time {
now := time.Now()
if now.Before(minCurrentTime) {
return minCurrentTime
}
return now
}

View File

@ -0,0 +1,14 @@
package currenttime
import (
"testing"
"time"
)
func TestMinTime(t *testing.T) {
// The baked-in time should always be before the current time.
now := time.Now()
if !minCurrentTime.Before(now) {
t.Fatalf("minCurrentTime is not before the current time: %v >= %v", minCurrentTime, now)
}
}

View File

@ -0,0 +1 @@
1741638824797

View File

@ -0,0 +1,20 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build ignore
package main
import (
"fmt"
"log"
"os"
"time"
)
func main() {
contents := fmt.Sprintf(`%d`, time.Now().UnixMilli())
if err := os.WriteFile("mintime.txt", []byte(contents), 0644); err != nil {
log.Fatal(err)
}
}

View File

@ -29,6 +29,7 @@ import (
"tailscale.com/health"
"tailscale.com/hostinfo"
"tailscale.com/net/bakedroots"
"tailscale.com/net/currenttime"
"tailscale.com/net/tlsdial/blockblame"
)
@ -144,6 +145,7 @@ func Config(host string, ht *health.Tracker, base *tls.Config) *tls.Config {
opts := x509.VerifyOptions{
DNSName: cs.ServerName,
Intermediates: x509.NewCertPool(),
CurrentTime: currenttime.Now(), // helps if the system clock is wrong
}
for _, cert := range cs.PeerCertificates[1:] {
opts.Intermediates.AddCert(cert)