mirror of
https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake.git
synced 2025-10-14 05:11:19 -04:00
Merge branch 'letsencrypt'
This commit is contained in:
commit
61b604fc46
3 changed files with 153 additions and 41 deletions
62
server/README.md
Normal file
62
server/README.md
Normal 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
|
||||||
|
```
|
122
server/server.go
122
server/server.go
|
@ -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)
|
||||||
|
|
10
server/torrc
10
server/torrc
|
@ -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
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue