forked from openprivacy/connectivity
456 lines
12 KiB
Go
456 lines
12 KiB
Go
package tor
|
|
|
|
import (
|
|
// "context"
|
|
"errors"
|
|
"fmt"
|
|
"github.com/eyedeekay/go-i2pcontrol"
|
|
"github.com/eyedeekay/sam3"
|
|
"github.com/eyedeekay/sam3/i2pkeys"
|
|
//"github.com/eyedeekay/sam3/helper"
|
|
"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 garlicListenService struct {
|
|
os *sam3.StreamListener
|
|
tp *i2pProvider
|
|
}
|
|
|
|
type i2pProvider struct {
|
|
controlPort int
|
|
//t *sam3.SAMConn
|
|
dialer *sam3.StreamSession
|
|
appDirectory string
|
|
bundeledTorPath string
|
|
lock sync.Mutex
|
|
breakChan chan bool
|
|
childListeners map[string]*garlicListenService
|
|
statusCallback func(int, string)
|
|
lastRestartTime time.Time
|
|
//authenticator tor.Authenticator
|
|
}
|
|
|
|
func (ols *garlicListenService) AddressFull() string {
|
|
return ols.os.Addr().String()
|
|
}
|
|
|
|
func (ols *garlicListenService) AddressIdentity() string {
|
|
return ols.os.Addr().String()[:56]
|
|
}
|
|
|
|
func (ols *garlicListenService) Accept() (net.Conn, error) {
|
|
return ols.os.Accept()
|
|
}
|
|
|
|
func (ols *garlicListenService) 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 *i2pProvider) GetBootstrapStatus() (int, string) {
|
|
tp.lock.Lock()
|
|
defer tp.lock.Unlock()
|
|
|
|
reseeding, err := i2pcontrol.Reseeding()
|
|
if err != nil {
|
|
return -2, err.Error()
|
|
}
|
|
if reseeding {
|
|
return 25, "I2P Router is Reseeding"
|
|
}
|
|
|
|
stat, err := i2pcontrol.Status()
|
|
if err != nil {
|
|
return -2, err.Error()
|
|
}
|
|
if strings.Contains(stat, "Rejecting") {
|
|
return 50, "I2P Router is rejecting tunnels"
|
|
}
|
|
|
|
netstat, err := i2pcontrol.Status()
|
|
if err != nil {
|
|
return -2, err.Error()
|
|
}
|
|
if strings.Contains(netstat, "ERROR") {
|
|
return -1, netstat
|
|
}
|
|
return 100, ""
|
|
}
|
|
|
|
func (tp *i2pProvider) GetVersion() string {
|
|
return "3.1"
|
|
}
|
|
|
|
// WaitTillBootstrapped Blocks until underlying network is bootstrapped
|
|
func (tp *i2pProvider) WaitTillBootstrapped() {
|
|
for true {
|
|
progress, _ := tp.GetBootstrapStatus()
|
|
if progress == 100 {
|
|
break
|
|
}
|
|
time.Sleep(100 * time.Millisecond)
|
|
}
|
|
}
|
|
|
|
func (tp *i2pProvider) Listen(identity connectivity.PrivateKey, port int) (connectivity.ListenService, error) {
|
|
var garlic = ""
|
|
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 i2pkeys.I2PKeys:
|
|
garlic = pubk.Addr().String()
|
|
}
|
|
}
|
|
|
|
// Hack around tor detached garlics not having a more obvious resume mechanism
|
|
// So we use deterministic ports
|
|
seedbytes := sha3.New224().Sum([]byte(garlic))
|
|
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: garlic address collision") {
|
|
// os = &tor.garlicService{Tor: tp.t, LocalListener: localListener, ID: garlic, 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 := &garlicListenService{os: garlic, tp: tp}
|
|
tp.childListeners[ols.AddressIdentity()] = ols
|
|
return ols, nil
|
|
}
|
|
|
|
func (tp *i2pProvider) Restart() {
|
|
tp.callStatusCallback(0, "rebooting")
|
|
tp.restart()
|
|
}
|
|
|
|
func (tp *i2pProvider) 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+".garlic:9878")
|
|
return conn, resolvedHostname, err
|
|
}
|
|
|
|
func (tp *i2pProvider) 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 *i2pProvider) SetStatusCallback(callback func(int, string)) {
|
|
tp.lock.Lock()
|
|
defer tp.lock.Unlock()
|
|
tp.statusCallback = callback
|
|
}
|
|
|
|
func (tp *i2pProvider) 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{})
|
|
}
|
|
|
|
func (tp *i2pProvider) 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) (*i2pProvider, 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 := &i2pProvider{authenticator: authenticator, controlPort: controlPort, appDirectory: appDirectory, bundeledTorPath: bundledTorPath, childListeners: make(map[string]*garlicListenService), 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 *i2pProvider) 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 *i2pProvider) unregisterListener(id string) {
|
|
tp.lock.Lock()
|
|
defer tp.lock.Unlock()
|
|
delete(tp.childListeners, id)
|
|
}
|
|
|
|
func (tp *i2pProvider) 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 *i2pProvider) 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
|
|
}
|