commit f368c871095dae3aa990e7d46a1d6612af9909b9 Author: Cecylia Bocovich cohosh@torproject.org Date: Tue Oct 13 17:18:50 2020 -0400
Add a remote service to test NAT compatability
Add a remote probetest service that will allow proxies to test their compatability with symmetric NATs. --- .gitignore | 1 + probetest/probetest.go | 226 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 227 insertions(+)
diff --git a/.gitignore b/.gitignore index 9f36c7c..002f95e 100644 --- a/.gitignore +++ b/.gitignore @@ -8,5 +8,6 @@ broker/broker client/client server/server proxy/proxy +probetest/probetest snowflake.log ignore/ diff --git a/probetest/probetest.go b/probetest/probetest.go new file mode 100644 index 0000000..af08e32 --- /dev/null +++ b/probetest/probetest.go @@ -0,0 +1,226 @@ +/* +Probe test server to check the reachability of Snowflake proxies from +clients with symmetric NATs. + +The probe server receives an offer from a proxy, returns an answer, and then +attempts to establish a datachannel connection to that proxy. The proxy will +self-determine whether the connection opened successfully. +*/ +package main + +import ( + "crypto/tls" + "flag" + "fmt" + "io" + "io/ioutil" + "log" + "net/http" + "os" + "strings" + "time" + + "git.torproject.org/pluggable-transports/snowflake.git/common/messages" + "git.torproject.org/pluggable-transports/snowflake.git/common/safelog" + "git.torproject.org/pluggable-transports/snowflake.git/common/util" + + "github.com/pion/webrtc/v2" + "golang.org/x/crypto/acme/autocert" +) + +const ( + readLimit = 100000 //Maximum number of bytes to be read from an HTTP request + dataChannelTimeout = 20 * time.Second //time after which we assume proxy data channel will not open + stunUrl = "stun:stun.l.google.com:19302" //default STUN URL +) + +// Create a PeerConnection from an SDP offer. Blocks until the gathering of ICE +// candidates is complete and the answer is available in LocalDescription. +func makePeerConnectionFromOffer(sdp *webrtc.SessionDescription, + dataChan chan struct{}) (*webrtc.PeerConnection, error) { + + config := webrtc.Configuration{ + ICEServers: []webrtc.ICEServer{ + { + URLs: []string{stunUrl}, + }, + }, + } + pc, err := webrtc.NewPeerConnection(config) + if err != nil { + return nil, fmt.Errorf("accept: NewPeerConnection: %s", err) + } + pc.OnDataChannel(func(dc *webrtc.DataChannel) { + close(dataChan) + }) + + err = pc.SetRemoteDescription(*sdp) + if err != nil { + if inerr := pc.Close(); inerr != nil { + log.Printf("unable to call pc.Close after pc.SetRemoteDescription with error: %v", inerr) + } + return nil, fmt.Errorf("accept: SetRemoteDescription: %s", err) + } + + answer, err := pc.CreateAnswer(nil) + if err != nil { + if inerr := pc.Close(); inerr != nil { + log.Printf("ICE gathering has generated an error when calling pc.Close: %v", inerr) + } + return nil, err + } + + err = pc.SetLocalDescription(answer) + if err != nil { + if err = pc.Close(); err != nil { + log.Printf("pc.Close after setting local description returned : %v", err) + } + return nil, err + } + + return pc, nil +} + +func probeHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + resp, err := ioutil.ReadAll(http.MaxBytesReader(w, r.Body, readLimit)) + if nil != err { + log.Println("Invalid data.") + w.WriteHeader(http.StatusBadRequest) + return + } + + offer, _, err := messages.DecodePollResponse(resp) + if err != nil { + log.Printf("Error reading offer: %s", err.Error()) + w.WriteHeader(http.StatusBadRequest) + return + } + if offer == "" { + log.Printf("Error processing session description: %s", err.Error()) + w.WriteHeader(http.StatusBadRequest) + return + } + sdp, err := util.DeserializeSessionDescription(offer) + if err != nil { + log.Printf("Error processing session description: %s", err.Error()) + w.WriteHeader(http.StatusBadRequest) + return + } + + dataChan := make(chan struct{}) + pc, err := makePeerConnectionFromOffer(sdp, dataChan) + if err != nil { + log.Printf("Error making WebRTC connection: %s", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + sdp = &webrtc.SessionDescription{ + Type: pc.LocalDescription().Type, + SDP: util.StripLocalAddresses(pc.LocalDescription().SDP), + } + answer, err := util.SerializeSessionDescription(sdp) + if err != nil { + log.Printf("Error making WebRTC connection: %s", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + body, err := messages.EncodeAnswerRequest(answer, "") + if err != nil { + log.Printf("Error making WebRTC connection: %s", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.Write(body) + // Set a timeout on peerconnection. If the connection state has not + // advanced to PeerConnectionStateConnected in this time, + // destroy the peer connection and return the token. + select { + case <-dataChan: + if err := pc.Close(); err != nil { + log.Printf("Error calling pc.Close: %v", err) + } + case <-time.After(dataChannelTimeout): + if err := pc.Close(); err != nil { + log.Printf("Error calling pc.Close: %v", err) + } + } + return + +} + +func main() { + var acmeEmail string + var acmeHostnamesCommas string + var acmeCertCacheDir string + var addr string + var disableTLS bool + var certFilename, keyFilename string + var unsafeLogging bool + + 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.StringVar(&acmeCertCacheDir, "acme-cert-cache", "acme-cert-cache", "directory in which certificates should be cached") + flag.StringVar(&certFilename, "cert", "", "TLS certificate file") + flag.StringVar(&keyFilename, "key", "", "TLS private key file") + flag.StringVar(&addr, "addr", ":8443", "address to listen on") + flag.BoolVar(&disableTLS, "disable-tls", false, "don't use HTTPS") + flag.BoolVar(&unsafeLogging, "unsafe-logging", false, "prevent logs from being scrubbed") + flag.Parse() + + var logOutput io.Writer = os.Stderr + if unsafeLogging { + log.SetOutput(logOutput) + } else { + // Scrub log output just in case an address ends up there + log.SetOutput(&safelog.LogScrubber{Output: logOutput}) + } + + log.SetFlags(log.LstdFlags | log.LUTC) + + http.HandleFunc("/probe", probeHandler) + + server := http.Server{ + Addr: addr, + } + + var err error + if acmeHostnamesCommas != "" { + acmeHostnames := strings.Split(acmeHostnamesCommas, ",") + log.Printf("ACME hostnames: %q", acmeHostnames) + + var cache autocert.Cache + if err = os.MkdirAll(acmeCertCacheDir, 0700); err != nil { + log.Printf("Warning: Couldn't create cache directory %q (reason: %s) so we're *not* using our certificate cache.", acmeCertCacheDir, err) + } else { + cache = autocert.DirCache(acmeCertCacheDir) + } + + certManager := autocert.Manager{ + Cache: cache, + Prompt: autocert.AcceptTOS, + HostPolicy: autocert.HostWhitelist(acmeHostnames...), + Email: acmeEmail, + } + // start certificate manager handler + go func() { + log.Printf("Starting HTTP-01 listener") + log.Fatal(http.ListenAndServe(":80", certManager.HTTPHandler(nil))) + }() + + server.TLSConfig = &tls.Config{GetCertificate: certManager.GetCertificate} + err = server.ListenAndServeTLS("", "") + } else if certFilename != "" && keyFilename != "" { + err = server.ListenAndServeTLS(certFilename, keyFilename) + } else if disableTLS { + err = server.ListenAndServe() + } else { + log.Fatal("the --cert and --key, --acme-hostnames, or --disable-tls option is required") + } + + if err != nil { + log.Println(err) + } +}