Merge branch 'letsencrypt'

This commit is contained in:
David Fifield 2017-03-30 19:13:01 -07:00
commit 61b604fc46
3 changed files with 153 additions and 41 deletions

62
server/README.md Normal file
View file

@ -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
```

View file

@ -1,11 +1,6 @@
// Snowflake-specific websocket server plugin. This is the same as the websocket // Snowflake-specific websocket server plugin. It reports the transport name as
// server used by flash proxy, except that it reports the transport name as // "snowflake" and does not forward the (unknown) client address to the
// "snowflake" and does not forward the remote address to the ExtORPort. // ExtORPort.
//
// Usage in torrc:
// ExtORPort auto
// ServerTransportListenAddr snowflake 0.0.0.0:9902
// ServerTransportPlugin snowflake exec server
package main package main
import ( import (
@ -19,12 +14,15 @@ import (
"net/http" "net/http"
"os" "os"
"os/signal" "os/signal"
"path/filepath"
"strings"
"sync" "sync"
"syscall" "syscall"
"time" "time"
"git.torproject.org/pluggable-transports/goptlib.git" "git.torproject.org/pluggable-transports/goptlib.git"
"git.torproject.org/pluggable-transports/websocket.git/websocket" "git.torproject.org/pluggable-transports/websocket.git/websocket"
"golang.org/x/crypto/acme/autocert"
) )
const ptMethodName = "snowflake" const ptMethodName = "snowflake"
@ -39,11 +37,15 @@ var ptInfo pt.ServerInfo
var handlerChan = make(chan int) var handlerChan = make(chan int)
func usage() { func usage() {
fmt.Printf("Usage: %s [OPTIONS]\n\n", os.Args[0]) fmt.Fprintf(os.Stderr, `Usage: %s [OPTIONS]
fmt.Printf("WebSocket server pluggable transport for Tor.\n")
fmt.Printf("Works only as a managed proxy.\n") WebSocket server pluggable transport for Snowflake. Works only as a managed
fmt.Printf("\n") proxy. Uses TLS with ACME (Let's Encrypt) by default. Set the certificate
fmt.Printf(" -h, -help show this help.\n") 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() flag.PrintDefaults()
} }
@ -150,7 +152,7 @@ func webSocketHandler(ws *websocket.WebSocket) {
proxy(or, &conn) 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. // This is cribbed from the source of net/http.Server.ListenAndServeTLS.
// We have to separate the Listen and Serve parts because we need to // We have to separate the Listen and Serve parts because we need to
// report the listening address before entering Serve (which is an // 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 // https://groups.google.com/d/msg/Golang-nuts/3F1VRCCENp8/3hcayZiwYM8J
config := &tls.Config{} config := &tls.Config{}
config.NextProtos = []string{"http/1.1"} config.NextProtos = []string{"http/1.1"}
config.GetCertificate = m.GetCertificate
var err error
config.Certificates = make([]tls.Certificate, 1)
config.Certificates[0], err = tls.LoadX509KeyPair(certFilename, keyFilename)
if err != nil {
return nil, err
}
conn, err := net.ListenTCP(network, addr) conn, err := net.ListenTCP(network, addr)
if err != nil { if err != nil {
@ -190,8 +186,8 @@ func startListener(network string, addr *net.TCPAddr) (net.Listener, error) {
return startServer(ln) return startServer(ln)
} }
func startListenerTLS(network string, addr *net.TCPAddr, certFilename, keyFilename string) (net.Listener, error) { func startListenerTLS(network string, addr *net.TCPAddr, m *autocert.Manager) (net.Listener, error) {
ln, err := listenTLS(network, addr, certFilename, keyFilename) ln, err := listenTLS(network, addr, m)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -216,15 +212,24 @@ func startServer(ln net.Listener) (net.Listener, error) {
return ln, nil 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() { func main() {
var acmeEmail string
var acmeHostnamesCommas string
var disableTLS bool var disableTLS bool
var certFilename, keyFilename string
var logFilename string var logFilename string
flag.Usage = usage 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.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.StringVar(&logFilename, "log", "", "log file to write to")
flag.Parse() flag.Parse()
@ -237,15 +242,10 @@ func main() {
log.SetOutput(f) log.SetOutput(f)
} }
if disableTLS { if !disableTLS && acmeHostnamesCommas == "" {
if certFilename != "" || keyFilename != "" { log.Fatal("the --acme-hostnames option is required")
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")
}
} }
acmeHostnames := strings.Split(acmeHostnamesCommas, ",")
log.Printf("starting") log.Printf("starting")
var err error var err error
@ -254,6 +254,41 @@ func main() {
log.Fatalf("error in setup: %s", err) log.Fatalf("error in setup: %s", err)
} }
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,
}
}
// 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) listeners := make([]net.Listener, 0)
for _, bindaddr := range ptInfo.Bindaddrs { for _, bindaddr := range ptInfo.Bindaddrs {
if bindaddr.MethodName != ptMethodName { if bindaddr.MethodName != ptMethodName {
@ -261,6 +296,20 @@ func main() {
continue 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 var ln net.Listener
args := pt.Args{} args := pt.Args{}
if disableTLS { if disableTLS {
@ -268,7 +317,10 @@ func main() {
ln, err = startListener("tcp", bindaddr.Addr) ln, err = startListener("tcp", bindaddr.Addr)
} else { } else {
args.Add("tls", "yes") args.Add("tls", "yes")
ln, err = startListenerTLS("tcp", bindaddr.Addr, certFilename, keyFilename) for _, hostname := range acmeHostnames {
args.Add("hostname", hostname)
}
ln, err = startListenerTLS("tcp", bindaddr.Addr, certManager)
} }
if err != nil { if err != nil {
log.Printf("error opening listener: %s", err) log.Printf("error opening listener: %s", err)

View file

@ -1,9 +1,7 @@
BridgeRelay 1 SocksPort 0
ORPort 9001 ORPort 9001
ExtORPort auto ExtORPort auto
SocksPort 0 BridgeRelay 1
ExitPolicy reject *:*
DataDirectory datadir
ServerTransportListenAddr snowflake 0.0.0.0:9902 ServerTransportListenAddr snowflake 0.0.0.0:443
ServerTransportPlugin snowflake exec ./server --disable-tls --log snowflake.log ServerTransportPlugin snowflake exec ./server --acme-hostnames snowflake.example --acme-email admin@snowflake.example --log /var/log/tor/snowflake-server.log