diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..80acf24 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/examples/httpaltsvc/tor-data diff --git a/examples/README.md b/examples/README.md index 1fc53a5..4f01daf 100644 --- a/examples/README.md +++ b/examples/README.md @@ -6,4 +6,5 @@ The following examples are in this directory: * [simpleserver](simpleserver) - Hosting simple "hello world" Tor onion service * [embeddedversion](embeddedversion) - Example showing how to dump the version of Tor embedded in the binary * [embeddedfileserver](embeddedfileserver) - Example showing a file server using Tor embedded in the binary -* [grpc](grpc) - Example showing how to use gRPC over Tor \ No newline at end of file +* [grpc](grpc) - Example showing how to use gRPC over Tor +* [httpaltsvc](httpaltsvc) - Example showing how to use .onion address as `Alt-Svc` of regular website (in development) \ No newline at end of file diff --git a/examples/httpaltsvc/README.md b/examples/httpaltsvc/README.md new file mode 100644 index 0000000..429c48b --- /dev/null +++ b/examples/httpaltsvc/README.md @@ -0,0 +1,32 @@ +## HTTP Alt-Svc Example + +The Tor browser now supports `Alt-Svc` headers to be onion services as +[HTTP alternate services](https://tools.ietf.org/html/rfc7838) to traditional sites. +[This Cloudflare post](https://blog.cloudflare.com/cloudflare-onion-service/) explains how they use it. This example +shows how to do it yourself. Since + +Specifically, this example listens on all IPs on 80 for insecure HTTP requests. It also listens on an onion service for +insecure HTTP requests. Both services return `Alt-Svc` addresses to an onion service that is run securely. + +**NOTE: Only do the steps in this example if you are comfortable with and aware of the consequences.** + +### Setup + +We're going to self-sign a certificate for use in the Tor browser. First, download `mkcert` and run: + + mkcert -install + +This will install a fake CA on your local machine that certs can be generated from. Remember the path it says the local +CA is at or run `mkcert -CAROOT` to get it back. We will use this "CA path" later. + +Now the CA must be added to the Tor browser. By default the Tor browser doesn't use the cert database so it cannot store +the overrides. To change this, go to `about:config` click through warning and change `security.nocertdb` to `false`. Now +that CAs can be added, go to `Options` (i.e. `about:preferences`) > `Privacy & Security` > `Certificates` section at the +bottom > `View Certificates...` > `Authorities` tab > `Import...` > choose `rootCA.pem` from the "CA path" from +earlier > check "Trust this CA to identify websites" and click `OK`. Restart the Tor browser. + +### Running + +Point yor DNS to this machine's IP (or use start `ngrok http 80` via [ngrok.com](https://ngrok.com/)) + +TODO: the rest... \ No newline at end of file diff --git a/examples/httpaltsvc/main.go b/examples/httpaltsvc/main.go new file mode 100644 index 0000000..8c61988 --- /dev/null +++ b/examples/httpaltsvc/main.go @@ -0,0 +1,187 @@ +package main + +import ( + "context" + "flag" + "fmt" + "io/ioutil" + "log" + "net/http" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/cretz/bine/tor" + "github.com/cretz/bine/torutil" +) + +var verbose bool + +func main() { + flag.BoolVar(&verbose, "verbose", false, "Whether to have verbose logging") + flag.Parse() + if flag.NArg() != 1 { + log.Fatal("Expecting single domain arg") + } else if err := run(flag.Arg(0)); err != nil { + log.Fatal(err) + } +} + +func run(domain string) error { + ctx, cancelFn := context.WithCancel(context.Background()) + defer cancelFn() + // Make sure mkcert is available + if _, err := exec.LookPath("mkcert"); err != nil { + return fmt.Errorf("Unable to find mkcert on PATH: %v", err) + } + // Listen until enter pressed + srv, err := start(ctx, domain, ":80") + if err != nil { + return err + } + defer srv.Close() + fmt.Printf("Listening on all IPs on port 80, so http://%v will use second onion as alt-svc\n", domain) + fmt.Printf("Listening on onion http://%v.onion that will use second onion as alt-svc\n", srv.onion1.ID) + fmt.Printf("Created secure second onion at https://%v.onion\n", srv.onion2.ID) + fmt.Println("Press enter to exit") + // Wait for key asynchronously + go func() { + fmt.Scanln() + cancelFn() + }() + select { + case err := <-srv.Err(): + return err + case <-ctx.Done(): + return nil + } +} + +type server struct { + exitAddrs map[string]bool + t *tor.Tor + onion1 *tor.OnionService + onion2 *tor.OnionService + httpSrv *http.Server + httpSrvErrCh chan error +} + +func start(ctx context.Context, domain string, httpAddr string) (srv *server, err error) { + srv = &server{} + // // Get all exit addrs + if srv.exitAddrs, err = getExitAddresses(); err != nil { + return nil, err + } + // Start tor + startConf := &tor.StartConf{DataDir: "tor-data"} + if verbose { + startConf.DebugWriter = os.Stdout + } else { + startConf.ExtraArgs = []string{"--quiet"} + } + if srv.t, err = tor.Start(ctx, startConf); err != nil { + return nil, err + } + // Henceforth, any err needs to call close + // Start Onion 1 + if srv.onion1, err = srv.t.Listen(ctx, &tor.ListenConf{RemotePorts: []int{80}, Version3: true}); err != nil { + srv.Close() + return nil, err + } + // Start Onion 2 + if srv.onion2, err = srv.t.Listen(ctx, &tor.ListenConf{RemotePorts: []int{443}, Version3: true}); err != nil { + srv.Close() + return nil, err + } + // Call mkcert for both onions + cmd := exec.CommandContext(ctx, "mkcert", domain, srv.onion1.ID+".onion", srv.onion2.ID+".onion") + cmd.Dir = "tor-data" + output, err := cmd.CombinedOutput() + if verbose { + fmt.Printf("Output from mkcert:\n%v\n", string(output)) + } + if err != nil { + srv.Close() + return nil, fmt.Errorf("Failed running mkcert: %v", err) + } + cert := filepath.Join("tor-data", domain+"+2.pem") + key := filepath.Join("tor-data", domain+"+2-key.pem") + // Listen on the onions + srv.httpSrvErrCh = make(chan error, 3) + go func(errCh chan error) { + errCh <- http.Serve(srv.onion1, srv.NewHandler(srv.onion1.ID+".onion", srv.onion2.ID+".onion:443")) + }(srv.httpSrvErrCh) + go func(errCh chan error) { + errCh <- http.ServeTLS(srv.onion2, srv.NewHandler(srv.onion2.ID+".onion", ""), cert, key) + }(srv.httpSrvErrCh) + // Start HTTP server + srv.httpSrv = &http.Server{Addr: httpAddr, Handler: srv.NewHandler(httpAddr, srv.onion2.ID+".onion:443")} + go func(httpSrv *http.Server, errCh chan error) { errCh <- httpSrv.ListenAndServe() }(srv.httpSrv, srv.httpSrvErrCh) + return +} + +func (s *server) Err() <-chan error { return s.httpSrvErrCh } + +func (s *server) NewHandler(siteAddr string, altSvc string) http.Handler { + hitCount := 0 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + hitCount++ + fmt.Printf("Accessed %v (%v)\n", siteAddr, hitCount) + // Set an alt-svc header + if altSvc != "" { + w.Header().Add("Alt-Svc", "h2=\""+altSvc+"\"; ma=600") + } + // Respond + remoteAddr, _, _ := torutil.PartitionString(r.RemoteAddr, ':') + exit := "" + if !s.exitAddrs[remoteAddr] { + exit = " NOT" + } + fmt.Fprintf(w, "Server-side site addr: %v\n", siteAddr) + fmt.Fprintf(w, "You accessed %v on %v from %v which is%v an exit node\n", + r.URL.Path, r.Host, r.RemoteAddr, exit) + fmt.Fprintf(w, "--------------- Headers ---------------\n") + for h, vals := range r.Header { + for _, val := range vals { + fmt.Fprintf(w, "%v: %v\n", h, val) + } + } + }) +} + +func (s *server) Close() { + if s.httpSrv != nil { + s.httpSrv.Close() + } + if s.onion1 != nil { + s.onion1.Close() + } + if s.onion2 != nil { + s.onion2.Close() + } + if s.t != nil { + s.t.Close() + } +} + +func getExitAddresses() (map[string]bool, error) { + resp, err := http.Get("https://check.torproject.org/exit-addresses") + if err != nil { + return nil, err + } + body, err := ioutil.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + return nil, err + } + ret := map[string]bool{} + for _, line := range strings.Split(string(body), "\n") { + pieces := strings.Split(strings.TrimSpace(line), " ") + if len(pieces) >= 2 && pieces[0] == "ExitAddress" { + fmt.Printf("Found exit: '%v'\n", pieces[1]) + ret[pieces[1]] = true + } + } + return ret, nil +}