commit f7f792f28ebc3520e06b124083a768cb32e4c88d Author: Yawning Angel yawning@schwanenlied.me Date: Sun Apr 10 06:26:36 2016 +0000
Add support for dynamic reloading of certificates. (Go 1.6+)
Entirely untested, probably correct. The callback used to feed the TLS Listener the certificate was introduced in Go 1.6, so that version or newer is now required. --- meek-server/certificate.go | 114 +++++++++++++++++++++++++++++++++++++++++++++ meek-server/meek-server.go | 14 +++--- 2 files changed, 122 insertions(+), 6 deletions(-)
diff --git a/meek-server/certificate.go b/meek-server/certificate.go new file mode 100644 index 0000000..2c57d85 --- /dev/null +++ b/meek-server/certificate.go @@ -0,0 +1,114 @@ +// certificate.go - Certificate management for meek-server. + +// +build go1.6 + +package main + +import ( + "crypto/tls" + "log" + "os" + "sync" + "time" +) + +const certLoadErrorRateLimit = 1 * time.Minute + +type certContext struct { + sync.Mutex + + certFile string + keyFile string + + certFileInfo os.FileInfo + keyFileInfo os.FileInfo + cachedCert *tls.Certificate + + lastWarnAt time.Time +} + +func newCertContext(certFilename, keyFilename string) (*certContext, error) { + ctx := new(certContext) + ctx.certFile = certFilename + ctx.keyFile = keyFilename + if _, err := ctx.reloadCertificate(); err != nil { + return nil, err + } + return ctx, nil +} + +func (ctx *certContext) reloadCertificate() (*tls.Certificate, error) { + doReload := true + + // XXX/Yawning: I assume compared to everything else related to TLS + // handshakes, stat() is cheap. If not, ratelimit here. gettimeofday() + // is vDSO-ed so it would be significantly faster than the syscalls. + + var err error + var cfInfo, kfInfo os.FileInfo + if cfInfo, err = os.Stat(ctx.certFile); err == nil { + kfInfo, err = os.Stat(ctx.keyFile) + } + + ctx.Lock() + defer ctx.Unlock() + + // Grab the cached certificate, compare the modification times if able. + cert := ctx.cachedCert + if err != nil { + // If stat fails, we likely aren't going to be able to reload, so + // return early. + return cert, err + } else if ctx.cachedCert != nil { + // Only compare the file times if there's actually a cached cert, + // and reload the cert if either the key or the certificate have + // been modified. + doReload = !ctx.certFileInfo.ModTime().Equal(cfInfo.ModTime()) || !ctx.keyFileInfo.ModTime().Equal(kfInfo.ModTime()) + } + + // Attempt to load the updated certificate, if required. + if doReload { + newCert, err := tls.LoadX509KeyPair(ctx.certFile, ctx.keyFile) + if err != nil { + // If the load fails, return the old certificate, so that it can + // be used till the load succeeds. + return cert, err + } + + // If the user regenerates the cert/key between the stat() and + // LoadX509KeyPair calls, this will race, but will self-correct + // after the next reloadCertificate() call because doReload will + // be true. + + ctx.cachedCert = &newCert + ctx.certFileInfo = cfInfo + ctx.keyFileInfo = kfInfo + + cert = ctx.cachedCert + } + return cert, nil +} + +func (ctx *certContext) getCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { + cert, err := ctx.reloadCertificate() + if err != nil { + // Failure to reload the certificate is a non-fatal error as this + // may be a filesystem related race condition. There is nothing + // preventing the next callback from hopefully succeeding, so rate + // limit an error log. + now := time.Now() + if now.After(ctx.lastWarnAt.Add(certLoadErrorRateLimit)) { + ctx.lastWarnAt = now + log.Printf("failed to reload certificate: %v", err) + } + } + + // This should NEVER happen because we will continue to use the old + // certificate on load failure, and we will never be calling the + // listener GetCertificate() callback if the initial load fails. + if cert == nil { + panic("no cached certificate available") + } + + return cert, nil +} diff --git a/meek-server/meek-server.go b/meek-server/meek-server.go index ab7730a..eb7adc9 100644 --- a/meek-server/meek-server.go +++ b/meek-server/meek-server.go @@ -264,6 +264,11 @@ func (state *State) ExpireSessions() { }
func listenTLS(network string, addr *net.TCPAddr, certFilename, keyFilename string) (net.Listener, error) { + ctx, err := newCertContext(certFilename, keyFilename) + if err != nil { + return nil, err + } + // This is cribbed from the source of net/http.Server.ListenAndServeTLS. // We have to separate the Listen and Serve parts because we need to // report the listening address before entering Serve (which is an @@ -272,12 +277,9 @@ func listenTLS(network string, addr *net.TCPAddr, certFilename, keyFilename stri config := &tls.Config{} config.NextProtos = []string{"http/1.1"}
- var err error - config.Certificates = make([]tls.Certificate, 1) - config.Certificates[0], err = tls.LoadX509KeyPair(certFilename, keyFilename) - if err != nil { - return nil, err - } + // Install a GetCertificate callback that ensures that the certificate is + // up to date. + config.GetCertificate = ctx.getCertificate
conn, err := net.ListenTCP(network, addr) if err != nil {
tor-commits@lists.torproject.org