From 61310600c3dc419da5661dba9b11da57acd5188f Mon Sep 17 00:00:00 2001 From: David Fifield Date: Wed, 18 Jan 2017 19:19:26 -0800 Subject: [PATCH 1/8] Automatically fetch certificates from Let's Encrypt. This removes the --tls-cert and --tls-keys options and replaces them with --acme-hostname and (optional) --acme-email. It uses https://godoc.org/golang.org/x/crypto/acme/autocert, which is kind of a successor to https://godoc.org/rsc.io/letsencrypt. The autocert package only works when the listener runs on port 443. For that reason, if TOR_PT_SERVER_BINDADDR asks for a port other than 443, the program will open an *additional* listening port on 443. If there is an error opening the listener, it is reported through an SMETHOD-ERROR for the requested address. The inspiration for this code came from George Tankersley's patch for meek-server: https://bugs.torproject.org/18655#comment:8 https://github.com/gtank/meek/tree/letsencrypt --- server/server.go | 70 +++++++++++++++++++++++++++++++++--------------- server/torrc | 4 +-- 2 files changed, 50 insertions(+), 24 deletions(-) diff --git a/server/server.go b/server/server.go index 8025ad3..bedc70c 100644 --- a/server/server.go +++ b/server/server.go @@ -19,12 +19,14 @@ import ( "net/http" "os" "os/signal" + "strings" "sync" "syscall" "time" "git.torproject.org/pluggable-transports/goptlib.git" "git.torproject.org/pluggable-transports/websocket.git/websocket" + "golang.org/x/crypto/acme/autocert" ) const ptMethodName = "snowflake" @@ -150,7 +152,7 @@ func webSocketHandler(ws *websocket.WebSocket) { proxy(or, &conn) } -func listenTLS(network string, addr *net.TCPAddr, certFilename, keyFilename string) (net.Listener, error) { +func listenTLS(network string, addr *net.TCPAddr, m *autocert.Manager) (net.Listener, error) { // 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 @@ -158,13 +160,7 @@ func listenTLS(network string, addr *net.TCPAddr, certFilename, keyFilename stri // https://groups.google.com/d/msg/Golang-nuts/3F1VRCCENp8/3hcayZiwYM8J 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 - } + config.GetCertificate = m.GetCertificate conn, err := net.ListenTCP(network, addr) if err != nil { @@ -190,8 +186,8 @@ func startListener(network string, addr *net.TCPAddr) (net.Listener, error) { return startServer(ln) } -func startListenerTLS(network string, addr *net.TCPAddr, certFilename, keyFilename string) (net.Listener, error) { - ln, err := listenTLS(network, addr, certFilename, keyFilename) +func startListenerTLS(network string, addr *net.TCPAddr, m *autocert.Manager) (net.Listener, error) { + ln, err := listenTLS(network, addr, m) if err != nil { return nil, err } @@ -217,14 +213,13 @@ func startServer(ln net.Listener) (net.Listener, error) { } func main() { + var acmeHostnamesCommas string var disableTLS bool - var certFilename, keyFilename string var logFilename string flag.Usage = usage + flag.StringVar(&acmeHostnamesCommas, "acme-hostnames", "", "comma-separated hostnames for 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(&logFilename, "log", "", "log file to write to") flag.Parse() @@ -237,15 +232,10 @@ func main() { log.SetOutput(f) } - if disableTLS { - if certFilename != "" || keyFilename != "" { - log.Fatalf("the --cert and --key options are not allowed with --disable-tls") - } - } else { - if certFilename == "" || keyFilename == "" { - log.Fatalf("the --cert and --key options are required") - } + if !disableTLS && acmeHostnamesCommas == "" { + log.Fatal("the --acme-hostnames option is required") } + acmeHostnames := strings.Split(acmeHostnamesCommas, ",") log.Printf("starting") var err error @@ -254,6 +244,28 @@ func main() { log.Fatalf("error in setup: %s", err) } + if !disableTLS { + log.Printf("ACME hostnames: %q", acmeHostnames) + } + certManager := autocert.Manager{ + Prompt: autocert.AcceptTOS, + HostPolicy: autocert.HostWhitelist(acmeHostnames...), + } + + // The ACME responder only works when it is running on port 443. In case + // there is not already going to be a TLS listener on port 443, we need + // to open an additional one. The port is actually opened in the loop + // below, so that any errors can be reported in the SMETHOD-ERROR of + // another bindaddr. + // https://letsencrypt.github.io/acme-spec/#domain-validation-with-server-name-indication-dvsni + need443Listener := !disableTLS + for _, bindaddr := range ptInfo.Bindaddrs { + if !disableTLS && bindaddr.Addr.Port == 443 { + need443Listener = false + break + } + } + listeners := make([]net.Listener, 0) for _, bindaddr := range ptInfo.Bindaddrs { if bindaddr.MethodName != ptMethodName { @@ -261,6 +273,20 @@ func main() { continue } + if need443Listener { + addr := *bindaddr.Addr + addr.Port = 443 + log.Printf("opening additional ACME listener on %s", addr.String()) + ln443, err := startListenerTLS("tcp", &addr, &certManager) + if err != nil { + log.Printf("error opening ACME listener: %s", err) + pt.SmethodError(bindaddr.MethodName, "ACME listener: "+err.Error()) + continue + } + listeners = append(listeners, ln443) + need443Listener = false + } + var ln net.Listener args := pt.Args{} if disableTLS { @@ -268,7 +294,7 @@ func main() { ln, err = startListener("tcp", bindaddr.Addr) } else { args.Add("tls", "yes") - ln, err = startListenerTLS("tcp", bindaddr.Addr, certFilename, keyFilename) + ln, err = startListenerTLS("tcp", bindaddr.Addr, &certManager) } if err != nil { log.Printf("error opening listener: %s", err) diff --git a/server/torrc b/server/torrc index 74f6af0..ed71a39 100644 --- a/server/torrc +++ b/server/torrc @@ -5,5 +5,5 @@ SocksPort 0 ExitPolicy reject *:* DataDirectory datadir -ServerTransportListenAddr snowflake 0.0.0.0:9902 -ServerTransportPlugin snowflake exec ./server --disable-tls --log snowflake.log +ServerTransportListenAddr snowflake 0.0.0.0:443 +ServerTransportPlugin snowflake exec ./server --acme-hostnames snowflake.example --acme-email admin@snowflake.example --log snowflake.log From b86bbd748de10157e0022cc0ef7746c7adcaea97 Mon Sep 17 00:00:00 2001 From: David Fifield Date: Fri, 20 Jan 2017 15:42:42 -0800 Subject: [PATCH 2/8] Add --acme-email option. --- server/server.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/server.go b/server/server.go index bedc70c..0ff736b 100644 --- a/server/server.go +++ b/server/server.go @@ -213,11 +213,13 @@ func startServer(ln net.Listener) (net.Listener, error) { } func main() { + var acmeEmail string var acmeHostnamesCommas string var disableTLS bool var logFilename string flag.Usage = usage + flag.StringVar(&acmeEmail, "acme-email", "", "optional contact email for Let's Encrypt notifications") flag.StringVar(&acmeHostnamesCommas, "acme-hostnames", "", "comma-separated hostnames for TLS certificate") flag.BoolVar(&disableTLS, "disable-tls", false, "don't use HTTPS") flag.StringVar(&logFilename, "log", "", "log file to write to") @@ -250,6 +252,7 @@ func main() { certManager := autocert.Manager{ Prompt: autocert.AcceptTOS, HostPolicy: autocert.HostWhitelist(acmeHostnames...), + Email: acmeEmail, } // The ACME responder only works when it is running on port 443. In case From 80acfbd8d8add1950a3eb529615281003e624568 Mon Sep 17 00:00:00 2001 From: David Fifield Date: Fri, 20 Jan 2017 14:55:19 -0800 Subject: [PATCH 3/8] Explain more in usage. --- server/server.go | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/server/server.go b/server/server.go index 0ff736b..9ebe255 100644 --- a/server/server.go +++ b/server/server.go @@ -41,11 +41,15 @@ var ptInfo pt.ServerInfo var handlerChan = make(chan int) func usage() { - fmt.Printf("Usage: %s [OPTIONS]\n\n", os.Args[0]) - fmt.Printf("WebSocket server pluggable transport for Tor.\n") - fmt.Printf("Works only as a managed proxy.\n") - fmt.Printf("\n") - fmt.Printf(" -h, -help show this help.\n") + fmt.Fprintf(os.Stderr, `Usage: %s [OPTIONS] + +WebSocket server pluggable transport for Snowflake. Works only as a managed +proxy. Uses TLS with ACME (Let's Encrypt) by default. Set the certificate +hostnames with the --acme-hostnames option. Use ServerTransportListenAddr in +torrc to choose the listening port. When using TLS, if the port is not 443, this +program will open an additional listening port on 443 to work with ACME. + +`, os.Args[0]) flag.PrintDefaults() } From 1b1fb37afe435ffb38d7ec53b9a79896f6b6e5c2 Mon Sep 17 00:00:00 2001 From: David Fifield Date: Fri, 20 Jan 2017 16:51:14 -0800 Subject: [PATCH 4/8] Add "hostname" args to the bridge descriptor as well. --- server/server.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/server.go b/server/server.go index 9ebe255..aaf2e05 100644 --- a/server/server.go +++ b/server/server.go @@ -301,6 +301,9 @@ func main() { ln, err = startListener("tcp", bindaddr.Addr) } else { args.Add("tls", "yes") + for _, hostname := range acmeHostnames { + args.Add("hostname", hostname) + } ln, err = startListenerTLS("tcp", bindaddr.Addr, &certManager) } if err != nil { From 138d2b5391199193055cc3bdd1c386ce1504b618 Mon Sep 17 00:00:00 2001 From: David Fifield Date: Fri, 20 Jan 2017 19:16:10 -0800 Subject: [PATCH 5/8] Use websocket relay at wss://snowflake.bamsoftware.com:443. --- proxy/snowflake.coffee | 4 ++-- proxy/websocket.coffee | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/proxy/snowflake.coffee b/proxy/snowflake.coffee index 1fff686..38d7d08 100644 --- a/proxy/snowflake.coffee +++ b/proxy/snowflake.coffee @@ -9,8 +9,8 @@ this must always act as the answerer. ### DEFAULT_BROKER = 'snowflake-reg.appspot.com' DEFAULT_RELAY = - host: '192.81.135.242' - port: 9902 + host: 'snowflake.bamsoftware.com' + port: 443 COPY_PASTE_ENABLED = false COOKIE_NAME = "snowflake-allow" diff --git a/proxy/websocket.coffee b/proxy/websocket.coffee index 94cf274..75c7a2f 100644 --- a/proxy/websocket.coffee +++ b/proxy/websocket.coffee @@ -46,7 +46,7 @@ buildUrl = (scheme, host, port, path, params) -> parts.join '' makeWebsocket = (addr) -> - url = buildUrl 'ws', addr.host, addr.port, '/' + url = buildUrl 'wss', addr.host, addr.port, '/' ws = new WebSocket url ### 'User agents can use this as a hint for how to handle incoming binary data: if From b0826304a4a18e4e30136ba7532b82372ef63c56 Mon Sep 17 00:00:00 2001 From: David Fifield Date: Sat, 21 Jan 2017 13:52:24 -0800 Subject: [PATCH 6/8] Make certManager a pointer and only set it when !disableTLS. --- server/server.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/server/server.go b/server/server.go index aaf2e05..62f166d 100644 --- a/server/server.go +++ b/server/server.go @@ -250,13 +250,14 @@ func main() { log.Fatalf("error in setup: %s", err) } + var certManager *autocert.Manager if !disableTLS { log.Printf("ACME hostnames: %q", acmeHostnames) - } - certManager := autocert.Manager{ - Prompt: autocert.AcceptTOS, - HostPolicy: autocert.HostWhitelist(acmeHostnames...), - Email: acmeEmail, + certManager = &autocert.Manager{ + Prompt: autocert.AcceptTOS, + HostPolicy: autocert.HostWhitelist(acmeHostnames...), + Email: acmeEmail, + } } // The ACME responder only works when it is running on port 443. In case @@ -284,7 +285,7 @@ func main() { addr := *bindaddr.Addr addr.Port = 443 log.Printf("opening additional ACME listener on %s", addr.String()) - ln443, err := startListenerTLS("tcp", &addr, &certManager) + ln443, err := startListenerTLS("tcp", &addr, certManager) if err != nil { log.Printf("error opening ACME listener: %s", err) pt.SmethodError(bindaddr.MethodName, "ACME listener: "+err.Error()) @@ -304,7 +305,7 @@ func main() { for _, hostname := range acmeHostnames { args.Add("hostname", hostname) } - ln, err = startListenerTLS("tcp", bindaddr.Addr, &certManager) + ln, err = startListenerTLS("tcp", bindaddr.Addr, certManager) } if err != nil { log.Printf("error opening listener: %s", err) From 1f8be86a01bcd322ee89c1d1b749406d4b03273c Mon Sep 17 00:00:00 2001 From: David Fifield Date: Sat, 21 Jan 2017 14:10:10 -0800 Subject: [PATCH 7/8] Add a DirCache for certificates under TOR_PT_STATE_LOCATION. This way, we don't lose state of certificates every time the process is restarted. There's a possibility, otherwise, that if you have to restart the server rapidly, you might run into Let's Encrypt rate limits and be unable to create a cert for a while. https://godoc.org/rsc.io/letsencrypt#hdr-Persistent_Storage --- server/server.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/server/server.go b/server/server.go index 62f166d..aec9b51 100644 --- a/server/server.go +++ b/server/server.go @@ -19,6 +19,7 @@ import ( "net/http" "os" "os/signal" + "path/filepath" "strings" "sync" "syscall" @@ -216,6 +217,14 @@ 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, "snowflake-certificate-cache"), nil +} + func main() { var acmeEmail string var acmeHostnamesCommas string @@ -253,10 +262,21 @@ func main() { var certManager *autocert.Manager if !disableTLS { log.Printf("ACME hostnames: %q", acmeHostnames) + + 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) + } + certManager = &autocert.Manager{ Prompt: autocert.AcceptTOS, HostPolicy: autocert.HostWhitelist(acmeHostnames...), Email: acmeEmail, + Cache: cache, } } From a936fc7e9b0c3bf81b56e8a2c6815bca949c23a1 Mon Sep 17 00:00:00 2001 From: David Fifield Date: Sat, 21 Jan 2017 14:53:09 -0800 Subject: [PATCH 8/8] README and documentation for server. --- server/README.md | 62 ++++++++++++++++++++++++++++++++++++++++++++++++ server/server.go | 11 +++------ server/torrc | 8 +++---- 3 files changed, 68 insertions(+), 13 deletions(-) create mode 100644 server/README.md diff --git a/server/README.md b/server/README.md new file mode 100644 index 0000000..d0fd91d --- /dev/null +++ b/server/README.md @@ -0,0 +1,62 @@ +This is the server transport plugin for Snowflake. +The actual transport protocol it uses is +[WebSocket](https://tools.ietf.org/html/rfc6455). +In Snowflake, the client connects to the proxy using WebRTC, +and the proxy connects to the server (this program) using WebSocket. + + +# Setup + +The server needs to be able to listen on port 443 +in order to generate its TLS certificates. +On Linux, use the `setcap` program to enable +the server to listen on port 443 without running as root: +``` +setcap 'cap_net_bind_service=+ep' /usr/local/bin/snowflake-server +``` + +Here is a short example of configuring your torrc file +to run the Snowflake server under Tor: +``` +SocksPort 0 +ORPort 9001 +ExtORPort auto +BridgeRelay 1 + +ServerTransportListenAddr snowflake 0.0.0.0:443 +ServerTransportPlugin snowflake exec ./server --acme-hostnames snowflake.example --acme-email admin@snowflake.example --log /var/log/tor/snowflake-server.log +``` +The domain names given to the `--acme-hostnames` option +should resolve to the IP address of the server. +You can give more than one, separated by commas. + + +# TLS + +The server uses TLS WebSockets by default: wss:// not ws://. +There is a `--disable-tls` option for testing purposes, +but you should use TLS in production. + +The server automatically fetches certificates +from [Let's Encrypt](https://en.wikipedia.org/wiki/Let's_Encrypt) as needed. +Use the `--acme-hostnames` option to tell the server +what hostnames it may request certificates for. +You can optionally provide a contact email address, +using the `--acme-email` option, +so that Let's Encrypt can inform you of any problems. +The server will cache TLS certificate data in the directory +`pt_state/snowflake-certificate-cache` inside the tor state directory. + +In order to fetch certificates automatically, +the server needs to listen on port 443. +This is a requirement of the ACME protocol used by Let's Encrypt. +If your `ServerTransportListenAddr` is not on port 443, +the server will open an listener on port 443 in addition +to the port you requested. +The program will exit if it can't bind to port 443. +On Linux, you can use the `setcap` program, +part of libcap2, to enable the server to bind to low-numbered ports +without having to run as root: +``` +setcap 'cap_net_bind_service=+ep' /usr/local/bin/snowflake-server +``` diff --git a/server/server.go b/server/server.go index aec9b51..7229e06 100644 --- a/server/server.go +++ b/server/server.go @@ -1,11 +1,6 @@ -// Snowflake-specific websocket server plugin. This is the same as the websocket -// server used by flash proxy, except that it reports the transport name as -// "snowflake" and does not forward the remote address to the ExtORPort. -// -// Usage in torrc: -// ExtORPort auto -// ServerTransportListenAddr snowflake 0.0.0.0:9902 -// ServerTransportPlugin snowflake exec server +// Snowflake-specific websocket server plugin. It reports the transport name as +// "snowflake" and does not forward the (unknown) client address to the +// ExtORPort. package main import ( diff --git a/server/torrc b/server/torrc index ed71a39..5dc2008 100644 --- a/server/torrc +++ b/server/torrc @@ -1,9 +1,7 @@ -BridgeRelay 1 +SocksPort 0 ORPort 9001 ExtORPort auto -SocksPort 0 -ExitPolicy reject *:* -DataDirectory datadir +BridgeRelay 1 ServerTransportListenAddr snowflake 0.0.0.0:443 -ServerTransportPlugin snowflake exec ./server --acme-hostnames snowflake.example --acme-email admin@snowflake.example --log snowflake.log +ServerTransportPlugin snowflake exec ./server --acme-hostnames snowflake.example --acme-email admin@snowflake.example --log /var/log/tor/snowflake-server.log