ALL

Solo para leer y verificar. No puedes copiar, usar, modificar ni redistribuir este código. Aviso completo en la página principal.

tunnel/certreload.go

SHA-256: cfec06e0b21e854b6b1f65e95f0976635653f73ac0ccb1284cf706467310a78f
192 líneas · 6421 bytes · Ver en GitHub

1package main
2
3import (
4 "context"
5 "crypto/tls"
6 "crypto/x509"
7 "errors"
8 "fmt"
9 "log/slog"
10 "os"
11 "sync"
12 "time"
13)
14
15// The node's own public-CA certificate (the one devices verify on the node's own name, ALPN all-tunnel/2) is renewed by the node's own ACME client
16// (certbot, HTTP-01 through the webroot on :80) and replaced on disk. certReloader puts the new one in service without a restart and without a dropped
17// handshake: it reads the files at start and again whenever they change, at a handshake or on its own timer, and a renewal that is not good (half
18// written, a key that does not match, another name, already expired) never replaces the certificate that works.
19
20// fileSig is what the node looks at to know a file changed: its modification time and size (certbot's live files are symlinks into an archive, and Stat
21// follows them, so a renewal that moves the link is a different file).
22type fileSig struct{ mod, size int64 }
23
24func sigOf(path string) (fileSig, error) {
25 st, err := os.Stat(path)
26 if err != nil {
27 return fileSig{}, err
28 }
29 return fileSig{st.ModTime().UnixNano(), st.Size()}, nil
30}
31
32const (
33 certLookGap = time.Second // a handshake looks at the files at most this often (certReloader.lookGap)
34 certRetryGap = 30 * time.Second // a renewal that did not load is tried again after this even if its files did not change again
35 certWatchEvery = time.Minute // the node looks at the files by itself this often
36 certWarnDays = 5 // days before the end at which the log says it loudly
37)
38
39type certReloader struct {
40 certPath, keyPath string
41 host string // the node's own name the certificate must be valid for; "" skips the name check (tests)
42 clk clock
43 log *slog.Logger
44 lookGap time.Duration
45
46 mu sync.Mutex
47 pair *tls.Certificate
48 notAfter time.Time
49 certSig fileSig // what the certificate in service was read from
50 keySig fileSig
51 lookedAt time.Time
52 failedAt time.Time
53 failedCrt fileSig // what the last refused renewal was read from
54 failedKey fileSig
55 lastErr string
56 loads int
57}
58
59func newCertReloader(certPath, keyPath, host string, clk clock, log *slog.Logger) (*certReloader, error) {
60 if clk.now == nil {
61 clk = newClock(nil, nil)
62 }
63 if log == nil {
64 log = slog.New(slog.DiscardHandler)
65 }
66 r := &certReloader{certPath: certPath, keyPath: keyPath, host: host, clk: clk, log: log, lookGap: certLookGap}
67 if err := r.load(); err != nil {
68 return nil, fmt.Errorf("node certificate: %w", err)
69 }
70 return r, nil
71}
72
73// load reads the pair, checks it and puts it in service. The caller holds no lock.
74func (r *certReloader) load() error {
75 cs, cerr := sigOf(r.certPath)
76 ks, kerr := sigOf(r.keyPath)
77 if err := errors.Join(cerr, kerr); err != nil {
78 return err
79 }
80 pair, err := tls.LoadX509KeyPair(r.certPath, r.keyPath)
81 if err != nil {
82 return err
83 }
84 leaf, err := x509.ParseCertificate(pair.Certificate[0])
85 if err != nil {
86 return err
87 }
88 now := r.clk.Now()
89 if r.host != "" {
90 if err := leaf.VerifyHostname(r.host); err != nil {
91 return fmt.Errorf("the certificate is not for %s", r.host)
92 }
93 }
94 if !now.Before(leaf.NotAfter) {
95 return fmt.Errorf("the certificate expired at %s", leaf.NotAfter.UTC().Format(time.RFC3339))
96 }
97 if now.Add(time.Hour).Before(leaf.NotBefore) {
98 return fmt.Errorf("the certificate is not valid before %s", leaf.NotBefore.UTC().Format(time.RFC3339))
99 }
100 pair.Leaf = leaf
101 r.mu.Lock()
102 r.pair, r.notAfter, r.certSig, r.keySig = &pair, leaf.NotAfter, cs, ks
103 r.lastErr, r.failedAt, r.failedCrt, r.failedKey = "", time.Time{}, fileSig{}, fileSig{}
104 r.loads++
105 r.mu.Unlock()
106 return nil
107}
108
109// refresh looks at the files (at most every certLookGap unless force) and loads them when they are not the ones in service. A refused renewal is
110// remembered by its files, so it is parsed again only when they change or certRetryGap passed.
111func (r *certReloader) refresh(force bool) {
112 now := r.clk.Now()
113 r.mu.Lock()
114 if !force && now.Sub(r.lookedAt) < r.lookGap {
115 r.mu.Unlock()
116 return
117 }
118 r.lookedAt = now
119 cur, curKey, failedCrt, failedKey, failedAt := r.certSig, r.keySig, r.failedCrt, r.failedKey, r.failedAt
120 r.mu.Unlock()
121
122 cs, cerr := sigOf(r.certPath)
123 ks, kerr := sigOf(r.keyPath)
124 if cerr != nil || kerr != nil {
125 return // a renewal in the middle of replacing its files: the certificate in service stays
126 }
127 if cs == cur && ks == curKey {
128 return
129 }
130 if !force && cs == failedCrt && ks == failedKey && !failedAt.IsZero() && now.Sub(failedAt) < certRetryGap {
131 return
132 }
133 if err := r.load(); err != nil {
134 r.mu.Lock()
135 r.failedCrt, r.failedKey, r.failedAt, r.lastErr = cs, ks, now, err.Error()
136 r.mu.Unlock()
137 r.log.Warn("node_cert_reload_refused", "reason", err.Error())
138 return
139 }
140 r.log.Info("node_cert_reloaded", "not_after", r.NotAfter().UTC().Format(time.RFC3339))
141}
142
143// Reload loads the files now, whatever they look like (SIGHUP): it returns why a renewal was refused.
144func (r *certReloader) Reload() error {
145 if err := r.load(); err != nil {
146 r.mu.Lock()
147 r.lastErr = err.Error()
148 r.mu.Unlock()
149 return err
150 }
151 return nil
152}
153
154// get is the Config.CertSource: the certificate in service, after a look at the files.
155func (r *certReloader) get(*tls.ClientHelloInfo) (*tls.Certificate, error) {
156 r.refresh(false)
157 r.mu.Lock()
158 defer r.mu.Unlock()
159 return r.pair, nil
160}
161
162// NotAfter is the end of the certificate in service (the monitor alerts under 14 days and under 5).
163func (r *certReloader) NotAfter() time.Time {
164 r.mu.Lock()
165 defer r.mu.Unlock()
166 return r.notAfter
167}
168
169// LastError is why the last renewal was refused ("" when the certificate in service is the newest on disk).
170func (r *certReloader) LastError() string {
171 r.mu.Lock()
172 defer r.mu.Unlock()
173 return r.lastErr
174}
175
176// Run looks at the files on its own timer, so a renewal is in service even when no device is connecting, and says in the log how long the certificate
177// has left (the provider's expiry e-mails no longer exist, the node's own log and the site's monitor are what warn).
178func (r *certReloader) Run(ctx context.Context) {
179 for {
180 r.refresh(true)
181 left := r.NotAfter().Sub(r.clk.Now())
182 switch {
183 case left <= 0:
184 r.log.Error("node_cert_expired")
185 case left < certWarnDays*24*time.Hour:
186 r.log.Warn("node_cert_expires_soon", "days_left", int(left/(24*time.Hour)))
187 }
188 if !r.clk.sleep(ctx, certWatchEvery) {
189 return
190 }
191 }
192}