From c14bc028ac887658bd4c68b8e01775636e1d747a Mon Sep 17 00:00:00 2001 From: David Anderson Date: Tue, 31 Mar 2020 12:24:33 -0700 Subject: [PATCH] cmd/microproxy: tiny TLS proxy that borrows autocert x509 certs. --- cmd/microproxy/microproxy.go | 130 +++++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 cmd/microproxy/microproxy.go diff --git a/cmd/microproxy/microproxy.go b/cmd/microproxy/microproxy.go new file mode 100644 index 000000000..2afe4a194 --- /dev/null +++ b/cmd/microproxy/microproxy.go @@ -0,0 +1,130 @@ +// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// microproxy proxies incoming HTTPS connections to another +// destination. Instead of managing its own TLS certificates, it +// borrows issued certificates and keys from an autocert directory. +package main + +import ( + "crypto/tls" + "flag" + "fmt" + "io/ioutil" + "log" + "net/http" + "net/http/httputil" + "net/url" + "path/filepath" + "sync" + "time" + + "tailscale.com/logpolicy" + "tailscale.com/tsweb" +) + +var ( + addr = flag.String("addr", ":4430", "server address") + certdir = flag.String("certdir", "", "directory to borrow LetsEncrypt certificates from") + hostname = flag.String("hostname", "", "hostname to serve") + logCollection = flag.String("logcollection", "", "If non-empty, logtail collection to log to") + target = flag.String("target", "", "URL to proxy to (usually http://localhost:...") +) + +func main() { + flag.Parse() + + if *logCollection != "" { + logpolicy.New(*logCollection) + } + + u, err := url.Parse(*target) + if err != nil { + log.Fatalf("Couldn't parse URL %q: %v", *target, err) + } + proxy := httputil.NewSingleHostReverseProxy(u) + proxy.FlushInterval = time.Second + mux := tsweb.NewMux(http.HandlerFunc(debugHandler)) + mux.Handle("/", tsweb.Protected(proxy)) + + ch := &certHolder{ + hostname: *hostname, + path: filepath.Join(*certdir, *hostname), + } + + httpsrv := &http.Server{ + Addr: *addr, + Handler: mux, + TLSConfig: &tls.Config{ + GetCertificate: ch.GetCertificate, + }, + } + + if err := httpsrv.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed { + log.Fatal(err) + } +} + +// certHolder loads and caches a TLS certificate from disk, reloading +// it every hour. +type certHolder struct { + hostname string // only hostname allowed in SNI + path string // path of certificate+key combined PEM file + + mu sync.Mutex + cert *tls.Certificate // cached parsed cert+key + loaded time.Time +} + +func (c *certHolder) GetCertificate(ch *tls.ClientHelloInfo) (*tls.Certificate, error) { + if ch.ServerName != c.hostname { + return nil, fmt.Errorf("wrong client SNI %q", ch.ServerName) + } + c.mu.Lock() + defer c.mu.Unlock() + if time.Since(c.loaded) > time.Hour { + if err := c.loadLocked(); err != nil { + log.Printf("Reloading cert %q: %v", c.path, err) + // continue anyway, we might be able to serve off the stale cert. + } + } + return c.cert, nil +} + +// load reloads the TLS certificate and key from disk. Caller must +// hold mu. +func (c *certHolder) loadLocked() error { + bs, err := ioutil.ReadFile(c.path) + if err != nil { + return fmt.Errorf("reading %q: %v", c.path, err) + } + cert, err := tls.X509KeyPair(bs, bs) + if err != nil { + return fmt.Errorf("parsing %q: %v", c.path, err) + } + + c.cert = &cert + c.loaded = time.Now() + return nil +} + +// debugHandler serves a page with links to tsweb-managed debug URLs +// at /debug/. +func debugHandler(w http.ResponseWriter, r *http.Request) { + f := func(format string, args ...interface{}) { fmt.Fprintf(w, format, args...) } + f(` +

microproxy debug

+