package tor import ( "bytes" "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" "net" "net/textproto" "os" "os/exec" path "path/filepath" "regexp" "strconv" "strings" "sync" "time" ) const ( minStatusIntervalMs = 200 maxStatusIntervalMs = 2000 restartCooldown = time.Second * 30 ) 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 { lock sync.Mutex 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) versionCallback func(string) lastRestartTime time.Time authenticator tor.Authenticator isClosed bool dataDir string version string bootProgress int } func (ols *onionListenService) AddressFull() string { ols.lock.Lock() defer ols.lock.Unlock() return ols.os.Addr().String() } func (ols *onionListenService) Accept() (net.Conn, error) { return ols.os.Accept() } func (ols *onionListenService) Close() { ols.lock.Lock() defer ols.lock.Unlock() ols.os.Close() } func (tp *torProvider) GetInfo(onion string) (map[string]string, error) { tp.lock.Lock() defer tp.lock.Unlock() circuits, streams, err := getCircuitInfo(tp.t.Control) if err == nil { var circuitID string for _, stream := range streams { if stream.Key == "stream-status" { lines := strings.Split(stream.Val, "\n") for _, line := range lines { parts := strings.Split(line, " ") // StreamID SP StreamStatus SP CircuitID SP Target CRLF if len(parts) == 4 { if strings.HasPrefix(parts[3], onion) { circuitID = parts[2] break } } } } } if circuitID == "" { return nil, errors.New("could not find circuit") } var hops []string for _, circuit := range circuits { if circuit.Key == "circuit-status" { lines := strings.Split(circuit.Val, "\n") for _, line := range lines { parts := strings.Split(line, " ") // CIRCID SP STATUS SP PATH... if len(parts) >= 3 && circuitID == parts[0] { //log.Debugf("Found Circuit for Onion %v %v", onion, parts) if parts[1] == "BUILT" { circuitPath := strings.Split(parts[2], ",") for _, hop := range circuitPath { fingerprint := hop[1:41] keyvals, err := tp.t.Control.GetInfo(fmt.Sprintf("ns/id/%s", fingerprint)) if err == nil && len(keyvals) == 1 { lines = strings.Split(keyvals[0].Val, "\n") for _, line := range lines { if strings.HasPrefix(line, "r") { parts := strings.Split(line, " ") if len(parts) > 6 { keyvals, err := tp.t.Control.GetInfo(fmt.Sprintf("ip-to-country/%s", parts[6])) if err == nil && len(keyvals) >= 1 { hops = append(hops, fmt.Sprintf("%s:%s", strings.ToUpper(keyvals[0].Val), parts[6])) } else { hops = append(hops, fmt.Sprintf("%s:%s", "XX", parts[6])) } } } } } } return map[string]string{"circuit": strings.Join(hops, ",")}, nil } } } } } } return nil, err } var progRe = regexp.MustCompile("PROGRESS=([0-9]*)") var sumRe = regexp.MustCompile("SUMMARY=\"(.*)\"$") // 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 { 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] } } tp.bootProgress = progress return progress, status } func (tp *torProvider) GetVersion() string { tp.lock.Lock() defer tp.lock.Unlock() return tp.version } func (tp *torProvider) closed() bool { tp.lock.Lock() defer tp.lock.Unlock() return tp.isClosed } // WaitTillBootstrapped Blocks until underlying network is bootstrapped func (tp *torProvider) WaitTillBootstrapped() error { for !tp.closed() { progress, _ := tp.GetBootstrapStatus() if progress == 100 { return nil } time.Sleep(100 * time.Millisecond) } return errors.New("close called before bootstrap") } func (tp *torProvider) Listen(identity connectivity.PrivateKey, port int) (connectivity.ListenService, error) { tp.lock.Lock() defer tp.lock.Unlock() if tp.t == nil { return nil, errors.New("tor provider closed") } var onion string var privkey ed25519.PrivateKey switch pk := identity.(type) { case ed25519.PrivateKey: privkey = pk gpubk := pk.Public() switch pubk := gpubk.(type) { case ed25519.PublicKey: onion = GetTorV3Hostname(pubk) default: return nil, fmt.Errorf("unknown public key type %v", pubk) } default: return nil, fmt.Errorf("unknown private key type %v", pk) } // 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 } var localListener net.Listener var err error if cwtchRestrictPorts := os.Getenv("CWTCH_RESTRICT_PORTS"); strings.ToLower(cwtchRestrictPorts) == "true" { // for whonix like systems we tightly restrict possible listen... // pick a random port between 15000 and 15378 // cwtch = 63 *77 *74* 63* 68 = 1537844616 log.Infof("using restricted ports, CWTCH_RESTRICT_PORTS=true"); localport = 15000 + (localport % 378) } if bindExternal := os.Getenv("CWTCH_BIND_EXTERNAL_WHONIX"); strings.ToLower(bindExternal) == "true" { if _, ferr := os.Stat("/usr/share/anon-ws-base-files/workstation"); !os.IsNotExist(ferr) { log.Infof("WARNING: binding to external interfaces. This is potentially unsafe outside of a containerized environment."); localListener, err = net.Listen("tcp", "0.0.0.0:"+strconv.Itoa(localport)) } else { log.Errorf("CWTCH_BIND_EXTERNAL_WHONIX flag set, but /usr/share/anon-ws-base-files/workstation does not exist. Defaulting to binding to local ports"); localListener, err = net.Listen("tcp", "127.0.0.1:"+strconv.Itoa(localport)) } } else { localListener, err = net.Listen("tcp", "127.0.0.1:"+strconv.Itoa(localport)) } if err != nil { return nil, err } conf := &tor.ListenConf{NoWait: true, Version3: true, Key: identity, RemotePorts: []int{port}, Detach: true, DiscardKey: true, LocalListener: localListener} os, err := tp.t.Listen(context.TODO(), conf) // Reattach to the old local listener... // Note: this code probably shouldn't be hit in Cwtch anymore because we purge torrc on restart. if err != nil && strings.Contains(err.Error(), "550 Unspecified Tor error: Onion address collision") { log.Errorf("550 Unspecified Tor error: Onion address collision - Recovering, but this probably indicates some weird tor configuration issue...") os = &tor.OnionService{Tor: tp.t, LocalListener: localListener, ID: onion, Version3: true, Key: bineed255192.FromCryptoPrivateKey(privkey), ClientAuths: make(map[string]string), RemotePorts: []int{port}} err = nil } // Any other errors require an immediate return as os is likely nil... if err != nil { return nil, err } // We need to set os.ID here, otherwise os.Close() may not shut down the onion service properly... os.ID = onion os.CloseLocalListenerOnClose = true ols := &onionListenService{os: os, tp: tp} tp.childListeners[ols.AddressFull()] = ols return ols, nil } func (tp *torProvider) Restart() { log.Debugf("launching restart...") tp.lock.Lock() log.Debugf("checking last restart time") if time.Since(tp.lastRestartTime) < restartCooldown { tp.lock.Unlock() return } tp.lock.Unlock() go tp.restart() } func (tp *torProvider) restart() { log.Debugf("Waiting for Tor to Close...") tp.callStatusCallback(0, "rebooting") tp.Close() tp.lock.Lock() defer tp.lock.Unlock() // preserve status callback after shutdown statusCallback := tp.statusCallback versionCallback := tp.versionCallback tp.t = nil log.Debugf("Restarting Tor Process") newTp, err := startTor(tp.appDirectory, tp.bundeledTorPath, tp.dataDir, tp.controlPort, tp.authenticator) if err == nil { // we need to reassign tor, dialer and callback which will have changed by swapping out // the underlying connection. tp.t = newTp.t tp.dialer = newTp.dialer tp.statusCallback = statusCallback tp.versionCallback = versionCallback if tp.versionCallback != nil { tp.versionCallback(tp.version) } tp.lastRestartTime = time.Now() tp.isClosed = false go tp.monitorRestart() } else { log.Errorf("Error restarting Tor process: %v", err) } } 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") } if tp.bootProgress != 100 { tp.lock.Unlock() return nil, hostname, fmt.Errorf("tor not online, bootstrap progress only %v", tp.bootProgress) } 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() { closing := false tp.lock.Lock() defer tp.lock.Unlock() if !tp.isClosed { // Break out of any background checks and close // the underlying tor connection tp.isClosed = true tp.breakChan <- true // wiggle lock to make sure if monitorRestart is waiting for the lock, it gets it, and finished that branch and gets the channel request to exit tp.lock.Unlock() tp.lock.Lock() closing = true } // Unregister Child Listeners for addr, child := range tp.childListeners { child.Close() delete(tp.childListeners, addr) } log.Debugf("shutting down acn threads..(is already closed: %v)", tp.isClosed) if closing { 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) SetVersionCallback(callback func(string)) { tp.lock.Lock() defer tp.lock.Unlock() tp.versionCallback = callback } func (tp *torProvider) GetStatusCallback() func(int, string) { tp.lock.Lock() defer tp.lock.Unlock() return tp.statusCallback } func (tp *torProvider) GetVersionCallback() func(string) { tp.lock.Lock() defer tp.lock.Unlock() return tp.versionCallback } func (tp *torProvider) callStatusCallback(prog int, status string) { tp.lock.Lock() defer tp.lock.Unlock() if tp.statusCallback != nil { tp.statusCallback(prog, status) } } // NewTorACNWithAuth creates/starts a Tor ACN and returns a usable ACN object func NewTorACNWithAuth(appDirectory string, bundledTorPath string, dataDir string, controlPort int, authenticator tor.Authenticator) (connectivity.ACN, error) { tp, err := startTor(appDirectory, bundledTorPath, dataDir, controlPort, authenticator) if err == nil { tp.isClosed = false go tp.monitorRestart() } return tp, err } // 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 // override tor ld_library_path if requested torLdLibPath, exists := os.LookupEnv("TOR_LD_LIBRARY_PATH") if exists { ldLibPath := fmt.Sprintf("LD_LIBRARY_PATH=%v", torLdLibPath) cmd.Env = append([]string{ldLibPath}, os.Environ()...) } return cmd, nil }) } func (tp *torProvider) checkVersion() (string, error) { // attempt connect to system tor log.Debugf("dialing 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 pinfo.TorVersion, nil } return pinfo.TorVersion, fmt.Errorf("tor version not supported: %v", pinfo.TorVersion) } } } return "", err } func startTor(appDirectory string, bundledTorPath string, dataDir string, controlPort int, authenticator tor.Authenticator) (*torProvider, error) { torDir := path.Join(appDirectory, "tor") os.MkdirAll(torDir, 0700) tp := &torProvider{authenticator: authenticator, controlPort: controlPort, appDirectory: appDirectory, bundeledTorPath: bundledTorPath, childListeners: make(map[string]*onionListenService), breakChan: make(chan bool, 1), statusCallback: nil, versionCallback: nil, lastRestartTime: time.Now().Add(-restartCooldown), bootProgress: -1} log.Debugf("checking if there is a running system tor") if version, err := tp.checkVersion(); err == nil { tp.version = version controlport, err := dialControlPort(tp.controlPort) if err == nil { log.Debugf("creating tor handler from system tor") tp.t = createFromExisting(controlport, dataDir) tp.dialer, err = tp.t.Dialer(context.TODO(), &tor.DialConf{Authenticator: tp.authenticator}) return tp, err } } // 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 bundled tor, then running system tor log.Debugln("checking if we can run bundled tor or system installed tor") if version, pass := checkCmdlineTorVersion(bundledTorPath); pass { log.Debugln("bundled tor appears viable, attempting to use '" + bundledTorPath + "'") t, err := tor.Start(context.TODO(), &tor.StartConf{ControlPort: tp.controlPort, NoAutoSocksPort: true, 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 tp.version = version } else if version, pass := checkCmdlineTorVersion("tor"); pass { t, err := tor.Start(context.TODO(), &tor.StartConf{ControlPort: tp.controlPort, NoAutoSocksPort: true, 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 tp.version = version } else { log.Debugln("Could not find a viable tor running or to run") return nil, fmt.Errorf("could not connect to or start Tor that met requirements (min Tor version 0.3.5.x)") } version, err := tp.checkVersion() if err == nil { tp.t.DeleteDataDirOnClose = false // caller is responsible for dealing with cached information... tp.dialer, err = tp.t.Dialer(context.TODO(), &tor.DialConf{Authenticator: tp.authenticator}) tp.version = version tp.t.Control.TakeOwnership() return tp, err } return nil, fmt.Errorf("could not connect to running tor: %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) 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 { tp.restart() return } if prog != lastBootstrapProgress { tp.callStatusCallback(prog, status) interval = minStatusIntervalMs lastBootstrapProgress = prog } case <-tp.breakChan: return } } } func getCircuitInfo(controlport *control.Conn) ([]*control.KeyVal, []*control.KeyVal, error) { circuits, cerr := controlport.GetInfo("circuit-status") streams, serr := controlport.GetInfo("stream-status") if cerr == nil && serr == nil { return circuits, streams, nil } return nil, nil, errors.New("could not fetch circuits or streams") } func createFromExisting(controlport *control.Conn, datadir string) *tor.Tor { t := &tor.Tor{ Process: nil, Control: controlport, ProcessCancelFunc: nil, DataDir: datadir, DeleteDataDirOnClose: false, DebugWriter: nil, StopProcessOnClose: false, GeoIPCreatedFile: "", GeoIPv6CreatedFile: "", } t.Control.DebugWriter = t.DebugWriter return t } func checkCmdlineTorVersion(torCmd string) (string, bool) { if torCmd == "" { return "", false } // ideally we would use CommandContext with Timeout here // but it doesn't work with programs that may launch other processes via scripts e.g. exec // and the workout is more complex than just implementing the logic ourselves... cmd := exec.Command(torCmd, "--version") var outb bytes.Buffer cmd.Stdout = &outb waiting := make(chan error, 1) // try running the tor process go func() { log.Debugf("running tor process: %v", torCmd) cmd.Run() waiting <- nil }() // timeout function go func() { <-time.After(time.Second * 5) waiting <- errors.New("timeout") }() err := <-waiting if err != nil { log.Debugf("tor process timed out") return "", false } re := regexp.MustCompile(`[0-1]+\.[0-9]+\.[0-9]+\.[0-9]+`) sysTorVersion := re.Find(outb.Bytes()) log.Infof("tor version: %v", string(sysTorVersion)) return string(sysTorVersion), 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 if len(torversions) >= 3 { 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))) } return false } 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 }