commit 20bea12d64473a57969c2b1fbb087d64a7259116 Author: David Fifield david@bamsoftware.com Date: Thu Mar 30 18:08:03 2017 -0700
Add the --acme-hostnames option for automatic Let's Encrypt certificates.
The inspiration for this code came from George Tankersley's patch: https://bugs.torproject.org/18655#comment:8 https://github.com/gtank/meek/tree/letsencrypt
The meek-server code is partially cribbed from the Snowflake server code: https://trac.torproject.org/projects/tor/ticket/18654#comment:7 https://gitweb.torproject.org/user/dcf/snowflake.git/diff/?h=letsencrypt&... --- doc/meek-server.1.txt | 33 ++++++++++++++--- meek-server/meek-server.go | 90 ++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 104 insertions(+), 19 deletions(-)
diff --git a/doc/meek-server.1.txt b/doc/meek-server.1.txt index b162af8..bbbcc7d 100644 --- a/doc/meek-server.1.txt +++ b/doc/meek-server.1.txt @@ -11,26 +11,51 @@ meek-server - The meek server transport plugin
SYNOPSIS -------- -**meek-server** **--cert**=__FILENAME__ **--key**=__FILENAME__ [__OPTIONS__] +**meek-server** **--acme-hostnames**=__HOSTNAME__ [__OPTIONS__]
DESCRIPTION ----------- meek-server is a transport plugin for Tor that encodes a stream as a sequence of HTTP requests and responses.
-The server runs in HTTPS mode by default, and the **--cert** and -**--key** options are required. +You will need to configure TLS certificates. There are two ways to set +up certificates: + +* **--acme-hostnames**=__HOSTNAME__ will automatically get certificates + for __HOSTNAME__ using Let's Encrypt. This only works when meek-server + is running on port 443. +* **--cert**=__FILENAME__ and **--key**=__FILENAME__ allow use to use + your own externally acquired certificate.
Configuration for meek-server usually appears in a torrc file. Here is a -sample configuration using HTTPS: +sample configuration using automatic Let's Encrypt certificates: +---- +ExtORPort auto +ServerTransportListenAddr 0.0.0.0:443 +ServerTransportPlugin meek exec ./meek-server --acme-hostnames meek-server.example --log meek-server.log +---- +Here is a sample configuration using externally acquired certificates: ---- ExtORPort auto ServerTransportListenAddr meek 0.0.0.0:8443 ServerTransportPlugin meek exec ./meek-server 8443 --cert cert.pem --key key.pem --log meek-server.log ----
+To listen on port 443 without needed to run as root, on Linux, +you can use the `setcap` program, part of libcap2: +---- +setcap 'cap_net_bind_service=+ep' /usr/local/bin/meek-server +---- + OPTIONS ------- +**--acme-hostnames**=__HOSTNAME__[,__HOSTNAME__]...:: + Comma-separated list of hostnames to honor when getting automatic + certificates from Let's Encrypt. meek-server has to be running on + port 443 in order for the **--acme-hostnames** option to work. The + certificates will be cached in the pt_state/meek-certificate-cache + directory inside tor state directory. + **--cert**=__FILENAME__:: Name of a PEM-encoded TLS certificate file. Required unless **--disable-tls** is used. diff --git a/meek-server/meek-server.go b/meek-server/meek-server.go index be2cd36..b8449a0 100644 --- a/meek-server/meek-server.go +++ b/meek-server/meek-server.go @@ -3,14 +3,19 @@ // data to a local OR port. // // Sample usage in torrc: +// ServerTransportListenAddr meek 0.0.0.0:443 +// ServerTransportPlugin meek exec ./meek-server --acme-hostnames meek-server.example --log meek-server.log +// Using your own TLS certificate: // ServerTransportListenAddr meek 0.0.0.0:8443 // ServerTransportPlugin meek exec ./meek-server --cert cert.pem --key key.pem --log meek-server.log // Plain HTTP usage: // ServerTransportListenAddr meek 0.0.0.0:8080 // ServerTransportPlugin meek exec ./meek-server --disable-tls --log meek-server.log // -// The server runs in HTTPS mode by default, and the --cert and --key options -// are required. Use the --disable-tls option to run with plain HTTP. +// The server runs in HTTPS mode by default, getting certificates from Let's +// Encrypt automatically. The server must be listening on port 443 for the +// automatic certificates to work. If you have your own certificate, use the +// --cert and --key options. Use --disable-tls option to run with plain HTTP. package main
import ( @@ -24,12 +29,15 @@ import ( "os" "os/signal" "path" + "path/filepath" "runtime" + "strings" "sync" "syscall" "time"
"git.torproject.org/pluggable-transports/goptlib.git" + "golang.org/x/crypto/acme/autocert" )
const ( @@ -326,15 +334,25 @@ func startServer(ln net.Listener) (net.Listener, error) { return ln, nil }
+func getCertificateCacheDir() (string, error) { + stateDir, err := pt.MakeStateDir() + if err != nil { + return "", err + } + return filepath.Join(stateDir, "meek-certificate-cache"), nil +} + func main() { + var acmeHostnamesCommas string var disableTLS bool var certFilename, keyFilename string var logFilename string var port int
+ flag.StringVar(&acmeHostnamesCommas, "acme-hostnames", "", "comma-separated hostnames for automatic TLS certificate") flag.BoolVar(&disableTLS, "disable-tls", false, "don't use HTTPS") - flag.StringVar(&certFilename, "cert", "", "TLS certificate file (required without --disable-tls)") - flag.StringVar(&keyFilename, "key", "", "TLS private key file (required without --disable-tls)") + flag.StringVar(&certFilename, "cert", "", "TLS certificate file") + flag.StringVar(&keyFilename, "key", "", "TLS private key file") flag.StringVar(&logFilename, "log", "", "name of log file") flag.IntVar(&port, "port", 0, "port to listen on") flag.Parse() @@ -348,31 +366,69 @@ func main() { log.SetOutput(f) }
+ var err error + ptInfo, err = pt.ServerSetup(nil) + if err != nil { + log.Fatalf("error in ServerSetup: %s", err) + } + // Handle the various ways of setting up TLS. The legal configurations // are: + // --acme-hostnames // --cert and --key together // --disable-tls - // The outputs of this block of code are the disableTLS and - // getCertificate variables. + // The outputs of this block of code are the disableTLS, + // missing443Listener, and getCertificate variables. + var missing443Listener = false var getCertificate func (*tls.ClientHelloInfo) (*tls.Certificate, error) if disableTLS { - if certFilename != "" || keyFilename != "" { - log.Fatalf("The --cert and --key options are not allowed with --disable-tls.\n") + if acmeHostnamesCommas != "" || certFilename != "" || keyFilename != "" { + log.Fatalf("The --acme-hostnames, --cert, and --key options are not allowed with --disable-tls.") } } else if certFilename != "" && keyFilename != "" { + if acmeHostnamesCommas != "" { + log.Fatalf("The --cert and --key options are not allowed with --acme-hostnames.") + } ctx, err := newCertContext(certFilename, keyFilename) if err != nil { log.Fatal(err) } getCertificate = ctx.GetCertificate - } else { - log.Fatalf("The --cert and --key options are required.\n") - } + } else if acmeHostnamesCommas != "" { + acmeHostnames := strings.Split(acmeHostnamesCommas, ",") + log.Printf("ACME hostnames: %q", acmeHostnames) + + missing443Listener = true + // The ACME responder only works when it is running on port 443. + // https://letsencrypt.github.io/acme-spec/#domain-validation-with-server-name-... + for _, bindaddr := range ptInfo.Bindaddrs { + if bindaddr.Addr.Port == 443 { + missing443Listener = false + break + } + } + // Don't quit immediately if we need a 443 listener and don't + // have it; do it later in the SMETHOD loop so it appears in the + // tor log. + + var cache autocert.Cache + cacheDir, err := getCertificateCacheDir() + if err == nil { + log.Printf("caching ACME certificates in directory %q", cacheDir) + cache = autocert.DirCache(cacheDir) + } else { + log.Printf("disabling ACME certificate cache: %s", err) + }
- var err error - ptInfo, err = pt.ServerSetup(nil) - if err != nil { - log.Fatalf("error in ServerSetup: %s", err) + certManager := &autocert.Manager{ + Prompt: autocert.AcceptTOS, + HostPolicy: autocert.HostWhitelist(acmeHostnames...), + // Email: acmeEmail, + Cache: cache, + } + getCertificate = certManager.GetCertificate + } else { + log.Fatalf("You must use either --acme-hostnames, or --cert and --key.") }
log.Printf("starting version %s (%s)", programVersion, runtime.Version()) @@ -383,6 +439,10 @@ func main() { } switch bindaddr.MethodName { case ptMethodName: + if missing443Listener { + pt.SmethodError(bindaddr.MethodName, "The --acme-hostnames option requires one of the bindaddrs to be on port 443.") + break + } var ln net.Listener if disableTLS { ln, err = startListener("tcp", bindaddr.Addr)