package tor import ( "errors" "fmt" "github.com/yawning/bulb" "log" "net" "net/http" "net/url" "os" "os/exec" "path" "strings" "time" ) // Manager checks connectivity of the Tor process used to support Cwtch type Manager struct { socksPort int controlPort int process *exec.Cmd } // NewTorManager Instantiates a new connection manager, returns non-nil error if it fails to connect to a tor daemon on the given ports. func NewTorManager(socksPort int, controlPort int, torPath string, torrc string) (*Manager, error) { torManager := new(Manager) torManager.socksPort = socksPort torManager.controlPort = controlPort err := torManager.TestConnection() if err == nil { log.Printf("using existing tor proxy") return torManager, nil } // try to start tor cmd := exec.Command(torPath, "-f", torrc) // on Android, home can be set to '/' which is not writeable if os.Getenv("HOME") == "" { cmd.Env = append(os.Environ(), fmt.Sprintf("HOME=%s", path.Dir(torrc))) } log.Printf("starting local tor proxy") err = cmd.Start() if err != nil { log.Printf("starting tor failed %v", err) return nil, err } torManager.process = cmd // for 30 seconds check every 5 if tor is up and working for i := 0; i < 6; i++ { time.Sleep(time.Second * 5) err = torManager.TestConnection() if err == nil { break } } return torManager, err } type proxyStatus int const ( proxyStatusOK proxyStatus = iota proxyStatusWrongType proxyStatusCannotConnect proxyStatusTimeout ) // Shutdown kills the managed Tor Process func (tm *Manager) Shutdown() { if tm.process != nil { if err := tm.process.Process.Kill(); err != nil { log.Fatal("failed to kill process: ", err) } } } // Detect whether a proxy is connectable and is a Tor proxy func checkTorProxy(proxyAddress string) proxyStatus { // A trick to do this without making an outward connection is, // paradoxically, to try to open it as http. // This is documented in section 4 here: https://github.com/torproject/torspec/blob/master/socks-extensions.txt client := &http.Client{Timeout: 2 * time.Second} response, err := client.Get("http://" + proxyAddress + "/") if err != nil { switch t := err.(type) { case *url.Error: switch t.Err.(type) { case *net.OpError: // Network-level error. Will in turn contain a os.SyscallError return proxyStatusCannotConnect default: // http.error unfortunately not exported, need to match on string // net/http: request canceled if strings.Index(t.Err.Error(), "request canceled") != -1 { return proxyStatusTimeout } } } // Protocol-level errors mean that http failed, so it's not Tor return proxyStatusWrongType } defer response.Body.Close() if response.Status != "501 Tor is not an HTTP Proxy" { return proxyStatusWrongType } return proxyStatusOK } func proxyStatusMessage(status proxyStatus) string { switch status { case proxyStatusWrongType: return "Proxy specified is not a Tor proxy" case proxyStatusCannotConnect: return "Cannot connect to Tor proxy" case proxyStatusTimeout: return "Proxy timeout" default: return "Unknown proxy error" } } // TestConnection returns nil if both the socks and control ports of the Tor connection are active, otherwise it returns an error. func (tm *Manager) TestConnection() error { proxyStatus := checkTorProxy(fmt.Sprintf("127.0.0.1:%d", tm.socksPort)) controlAddress := fmt.Sprintf("127.0.0.1:%d", tm.controlPort) if proxyStatus == proxyStatusOK { c, err := bulb.Dial("tcp4", controlAddress) if c != nil { c.Close() } if err == nil { return nil } return fmt.Errorf("could not connect to Tor Control Port %v %v", tm.controlPort, err) } return errors.New(proxyStatusMessage(proxyStatus)) }