aboutsummaryrefslogtreecommitdiff
path: root/probetest
diff options
context:
space:
mode:
authorCecylia Bocovich <cohosh@torproject.org>2020-10-13 17:18:50 -0400
committerCecylia Bocovich <cohosh@torproject.org>2020-10-29 11:03:51 -0400
commitf368c871095dae3aa990e7d46a1d6612af9909b9 (patch)
treef47a4048ad9638051e15ca9528cb228ad8e78607 /probetest
parent7a0428e3b11ba437f27d09b1a9ad0fa820e54d24 (diff)
downloadsnowflake-f368c871095dae3aa990e7d46a1d6612af9909b9.tar.gz
snowflake-f368c871095dae3aa990e7d46a1d6612af9909b9.zip
Add a remote service to test NAT compatability
Add a remote probetest service that will allow proxies to test their compatability with symmetric NATs.
Diffstat (limited to 'probetest')
-rw-r--r--probetest/probetest.go226
1 files changed, 226 insertions, 0 deletions
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)
+ }
+}