692 lines
20 KiB
Go
692 lines
20 KiB
Go
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
|
|
}
|