package tor import ( "context" "errors" "fmt" "git.openprivacy.ca/openprivacy/bine/control" "git.openprivacy.ca/openprivacy/bine/process" "git.openprivacy.ca/openprivacy/bine/tor" bineed255192 "git.openprivacy.ca/openprivacy/bine/torutil/ed25519" "git.openprivacy.ca/openprivacy/connectivity" "git.openprivacy.ca/openprivacy/log" "golang.org/x/crypto/ed25519" "golang.org/x/crypto/sha3" "io/ioutil" "net" "net/textproto" "os" "os/exec" "path" "regexp" "strconv" "strings" "sync" "time" ) const ( minStatusIntervalMs = 200 maxStatusIntervalMs = 2000 restartCooldown = time.Second * 30 ) const ( // CannotDialRicochetAddressError is thrown when a connection to a ricochet address fails. CannotDialRicochetAddressError = connectivity.Error("CannotDialRicochetAddressError") ) const ( networkUnknown = -3 torDown = -2 networkDown = -1 networkUp = 0 ) // NoTorrcError is a typed error thrown to indicate start could not complete due to lack of a torrc file type NoTorrcError struct { path string } func (e *NoTorrcError) Error() string { return fmt.Sprintf("torrc file does not exist at %v", e.path) } type logWriter struct { level log.Level } func (l *logWriter) Write(p []byte) (int, error) { log.Printf(l.level, "tor: %v", string(p)) return len(p), nil } type onionListenService struct { os *tor.OnionService tp *torProvider } type torProvider struct { controlPort int t *tor.Tor dialer *tor.Dialer appDirectory string bundeledTorPath string lock sync.Mutex breakChan chan bool childListeners map[string]*onionListenService statusCallback func(int, string) lastRestartTime time.Time authenticator tor.Authenticator } func (ols *onionListenService) AddressFull() string { return ols.os.Addr().String() } func (ols *onionListenService) AddressIdentity() string { return ols.os.Addr().String()[:56] } func (ols *onionListenService) Accept() (net.Conn, error) { return ols.os.Accept() } func (ols *onionListenService) Close() { ols.tp.unregisterListener(ols.AddressIdentity()) ols.os.Close() } // GetBootstrapStatus returns an int 0-100 on the percent the bootstrapping of the underlying network is at and an optional string message // returns -1 on network disconnected // returns -2 on error func (tp *torProvider) GetBootstrapStatus() (int, string) { tp.lock.Lock() defer tp.lock.Unlock() if tp.t == nil { return torDown, "error: no tor, trying to restart..." } val, err := tp.t.Control.GetInfo("network-liveness") if err != nil { return torDown, "can't query tor network-liveness" } if val[0].Val == "down" { return networkDown, "tor cannot detect underlying network" } // else network is up kvs, err := tp.t.Control.GetInfo("status/bootstrap-phase") if err != nil { return torDown, "error querrying status/bootstrap-phase" } progress := 0 status := "" if len(kvs) > 0 { progRe := regexp.MustCompile("PROGRESS=([0-9]*)") sumRe := regexp.MustCompile("SUMMARY=\"(.*)\"$") if progMatches := progRe.FindStringSubmatch(kvs[0].Val); len(progMatches) > 1 { progress, _ = strconv.Atoi(progMatches[1]) } if statusMatches := sumRe.FindStringSubmatch(kvs[0].Val); len(statusMatches) > 1 { status = statusMatches[1] } } return progress, status } func (tp *torProvider) GetVersion() string { tp.lock.Lock() defer tp.lock.Unlock() if tp.t == nil { return "No Tor" } pinfo, err := tp.t.Control.ProtocolInfo() if err == nil { return pinfo.TorVersion } return "No Tor" } // WaitTillBootstrapped Blocks until underlying network is bootstrapped func (tp *torProvider) WaitTillBootstrapped() { for true { progress, _ := tp.GetBootstrapStatus() if progress == 100 { break } time.Sleep(100 * time.Millisecond) } } func (tp *torProvider) Listen(identity connectivity.PrivateKey, port int) (connectivity.ListenService, error) { var onion = "" var privkey ed25519.PrivateKey tp.lock.Lock() defer tp.lock.Unlock() if tp.t == nil { return nil, errors.New("Tor Provider closed") } switch pk := identity.(type) { case ed25519.PrivateKey: privkey = pk gpubk := pk.Public() switch pubk := gpubk.(type) { case ed25519.PublicKey: onion = GetTorV3Hostname(pubk) } } // Hack around tor detached onions not having a more obvious resume mechanism // So we use deterministic ports seedbytes := sha3.New224().Sum([]byte(onion)) localport := int(seedbytes[0]) + (int(seedbytes[1]) << 8) if localport < 1024 { // this is not uniformly random, but we don't need it to be localport += 1024 } localListener, err := net.Listen("tcp", "127.0.0.1:"+strconv.Itoa(localport)) conf := &tor.ListenConf{NoWait: true, Version3: true, Key: identity, RemotePorts: []int{port}, Detach: true, DiscardKey: true, LocalListener: localListener} os, err := tp.t.Listen(nil, conf) if err != nil && strings.Contains(err.Error(), "550 Unspecified Tor error: Onion address collision") { os = &tor.OnionService{Tor: tp.t, LocalListener: localListener, ID: onion, Version3: true, Key: bineed255192.FromCryptoPrivateKey(privkey), ClientAuths: make(map[string]string, 0), RemotePorts: []int{port}} err = nil } // Not set in t.Listen if supplied, we want it to handle this however os.CloseLocalListenerOnClose = true if err != nil { return nil, err } ols := &onionListenService{os: os, tp: tp} tp.childListeners[ols.AddressIdentity()] = ols return ols, nil } func (tp *torProvider) Restart() { tp.callStatusCallback(0, "rebooting") tp.restart() } func (tp *torProvider) Open(hostname string) (net.Conn, string, error) { tp.lock.Lock() if tp.t == nil { tp.lock.Unlock() return nil, hostname, errors.New("Tor is offline") } tp.lock.Unlock() resolvedHostname := hostname if strings.HasPrefix(hostname, "ricochet:") { addrParts := strings.Split(hostname, ":") resolvedHostname = addrParts[1] } conn, err := tp.dialer.Dial("tcp", resolvedHostname+".onion:9878") return conn, resolvedHostname, err } func (tp *torProvider) Close() { for _, child := range tp.childListeners { child.Close() } tp.lock.Lock() defer tp.lock.Unlock() tp.breakChan <- true if tp.t != nil { tp.t.Close() tp.t = nil } } func (tp *torProvider) SetStatusCallback(callback func(int, string)) { tp.lock.Lock() defer tp.lock.Unlock() tp.statusCallback = callback } func (tp *torProvider) callStatusCallback(prog int, status string) { tp.lock.Lock() if tp.statusCallback != nil { tp.statusCallback(prog, status) } tp.lock.Unlock() } // NewTorACNWithAuth creates/starts a Tor ACN and returns a usable ACN object func NewTorACNWithAuth(appDirectory string, bundledTorPath string, controlPort int, authenticator tor.Authenticator) (connectivity.ACN, error) { tp, err := startTor(appDirectory, bundledTorPath, controlPort, authenticator) if err == nil { tp.dialer, err = tp.t.Dialer(nil, &tor.DialConf{Authenticator: authenticator}) if err == nil { go tp.monitorRestart() } } return tp, err } // NewTorACN creates/starts a Tor ACN and returns a usable ACN object with a NullAuthenticator - this will fail. func NewTorACN(appDirectory string, bundledTorPath string) (connectivity.ACN, error) { return NewTorACNWithAuth(appDirectory, bundledTorPath, 9051, NullAuthenticator{}) } // newHideCmd creates a Creator function for bine which generates a cmd that one windows will hide the dosbox func newHideCmd(exePath string) process.Creator { return process.CmdCreatorFunc(func(ctx context.Context, args ...string) (*exec.Cmd, error) { loggerDebug := &logWriter{log.LevelDebug} loggerError := &logWriter{log.LevelError} cmd := exec.CommandContext(ctx, exePath, args...) cmd.Stdout = loggerDebug cmd.Stderr = loggerError cmd.SysProcAttr = sysProcAttr return cmd, nil }) } func (tp *torProvider) checkVersion() error { // attempt connect to system tor log.Debugf("dialing system tor control port") controlport, err := dialControlPort(tp.controlPort) if err == nil { defer controlport.Close() err := tp.authenticator.Authenticate(controlport) if err == nil { log.Debugln("connected to control port") pinfo, err := controlport.ProtocolInfo() if err == nil { if minTorVersionReqs(pinfo.TorVersion) { log.Debugln("OK version " + pinfo.TorVersion) return nil } return fmt.Errorf("Tor version not supported: %v", pinfo.TorVersion) } } } return err } func startTor(appDirectory string, bundledTorPath string, controlPort int, authenticator tor.Authenticator) (*torProvider, error) { torDir := path.Join(appDirectory, "tor") os.MkdirAll(torDir, 0700) dataDir := "" var err error if dataDir, err = ioutil.TempDir(torDir, "data-dir-"); err != nil { return nil, fmt.Errorf("Unable to create temp data dir: %v", err) } tp := &torProvider{authenticator: authenticator, controlPort: controlPort, appDirectory: appDirectory, bundeledTorPath: bundledTorPath, childListeners: make(map[string]*onionListenService), breakChan: make(chan bool), statusCallback: nil, lastRestartTime: time.Now().Add(-restartCooldown)} log.Debugf("launching system tor") if err := tp.checkVersion(); err == nil { controlport, err := dialControlPort(tp.controlPort) if err == nil { log.Debugf("creating tor handler fom system tor") tp.t = createFromExisting(controlport, dataDir) } return tp, nil } // check if the torrc file is present where expected if _, err := os.Stat(path.Join(torDir, "torrc")); os.IsNotExist(err) { err = &NoTorrcError{path.Join(torDir, "torrc")} log.Debugln(err.Error()) return nil, err } // if not, try running system tor if checkCmdlineTorVersion("tor") { t, err := tor.Start(nil, &tor.StartConf{ControlPort: tp.controlPort, DisableCookieAuth: true, UseEmbeddedControlConn: false, DisableEagerAuth: true, EnableNetwork: true, DataDir: dataDir, TorrcFile: path.Join(torDir, "torrc"), DebugWriter: nil, ProcessCreator: newHideCmd("tor")}) if err != nil { log.Debugf("Error connecting to self-run system tor: %v\n", err) return nil, err } tp.t = t } else if bundledTorPath != "" && checkCmdlineTorVersion(bundledTorPath) { log.Debugln("attempting using bundled tor '" + bundledTorPath + "'") t, err := tor.Start(nil, &tor.StartConf{ControlPort: tp.controlPort, DisableCookieAuth: true, UseEmbeddedControlConn: false, DisableEagerAuth: true, EnableNetwork: true, DataDir: dataDir, TorrcFile: path.Join(torDir, "torrc"), ExePath: bundledTorPath, DebugWriter: nil, ProcessCreator: newHideCmd(bundledTorPath)}) if err != nil { log.Debugf("Error running bundled tor %v\n", err) return nil, err } tp.t = t } err = tp.checkVersion() if err == nil { tp.t.DeleteDataDirOnClose = true return tp, nil } return nil, fmt.Errorf("could not connect to or start Tor that met requirments (min Tor version 0.3.5.x): %v", err) } func (tp *torProvider) GetPID() (int, error) { val, err := tp.t.Control.GetInfo("process/pid") if err == nil { return strconv.Atoi(val[0].Val) } return 0, err } func (tp *torProvider) unregisterListener(id string) { tp.lock.Lock() defer tp.lock.Unlock() delete(tp.childListeners, id) } func (tp *torProvider) monitorRestart() { lastBootstrapProgress := networkUnknown interval := minStatusIntervalMs for { select { case <-time.After(time.Millisecond * time.Duration(interval)): if interval < maxStatusIntervalMs { interval *= 2 } prog, status := tp.GetBootstrapStatus() if prog == torDown && tp.t != nil { log.Warnf("monitorRestart calling tp.restart() with prog:%v\n", prog) tp.restart() } if prog != lastBootstrapProgress { tp.callStatusCallback(prog, status) interval = minStatusIntervalMs lastBootstrapProgress = prog } case <-tp.breakChan: return } } } func (tp *torProvider) restart() { tp.lock.Lock() defer tp.lock.Unlock() if time.Now().Sub(tp.lastRestartTime) < restartCooldown { return } tp.lastRestartTime = time.Now() for _, child := range tp.childListeners { delete(tp.childListeners, child.AddressIdentity()) child.os.Close() } tp.t.Close() tp.t = nil for { newTp, err := startTor(tp.appDirectory, tp.bundeledTorPath, tp.controlPort, tp.authenticator) if err == nil { tp.t = newTp.t tp.dialer, _ = tp.t.Dialer(nil, &tor.DialConf{}) return } } } func createFromExisting(controlport *control.Conn, datadir string) *tor.Tor { t := &tor.Tor{ Process: nil, Control: controlport, ProcessCancelFunc: nil, DataDir: datadir, DeleteDataDirOnClose: true, DebugWriter: nil, StopProcessOnClose: false, GeoIPCreatedFile: "", GeoIPv6CreatedFile: "", } t.Control.DebugWriter = t.DebugWriter return t } func checkCmdlineTorVersion(torCmd string) bool { cmd := exec.Command(torCmd, "--version") cmd.SysProcAttr = sysProcAttr out, err := cmd.CombinedOutput() re := regexp.MustCompile("[0-1]\\.[0-9]\\.[0-9]\\.[0-9]") sysTorVersion := re.Find(out) log.Infoln("tor version: " + string(sysTorVersion)) return err == nil && minTorVersionReqs(string(sysTorVersion)) } // returns true if supplied version meets our min requirments // min requirement: 0.3.5.x func minTorVersionReqs(torversion string) bool { torversions := strings.Split(torversion, ".") //eg: 0.3.4.8 or 0.3.5.1-alpha log.Debugf("torversions: %v", torversions) tva, _ := strconv.Atoi(torversions[0]) tvb, _ := strconv.Atoi(torversions[1]) tvc, _ := strconv.Atoi(torversions[2]) return tva > 0 || (tva == 0 && (tvb > 3 || (tvb == 3 && tvc >= 5))) } func dialControlPort(port int) (*control.Conn, error) { textConn, err := textproto.Dial("tcp", "127.0.0.1:"+strconv.Itoa(port)) if err != nil { return nil, err } return control.NewConn(textConn), nil }