forked from openprivacy/libricochet-go
383 lines
10 KiB
Go
383 lines
10 KiB
Go
package connectivity
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"git.openprivacy.ca/openprivacy/libricochet-go/log"
|
|
"git.openprivacy.ca/openprivacy/libricochet-go/utils"
|
|
"github.com/cretz/bine/control"
|
|
"github.com/cretz/bine/process"
|
|
"github.com/cretz/bine/tor"
|
|
bineed255192 "github.com/cretz/bine/torutil/ed25519"
|
|
"golang.org/x/crypto/ed25519"
|
|
"golang.org/x/crypto/sha3"
|
|
"net"
|
|
"net/textproto"
|
|
"os"
|
|
"os/exec"
|
|
"path"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
// CannotResolveLocalTCPAddressError is thrown when a local ricochet connection has the wrong format.
|
|
CannotResolveLocalTCPAddressError = utils.Error("CannotResolveLocalTCPAddressError")
|
|
// CannotDialLocalTCPAddressError is thrown when a connection to a local ricochet address fails.
|
|
CannotDialLocalTCPAddressError = utils.Error("CannotDialLocalTCPAddressError")
|
|
// CannotDialRicochetAddressError is thrown when a connection to a ricochet address fails.
|
|
CannotDialRicochetAddressError = utils.Error("CannotDialRicochetAddressError")
|
|
)
|
|
|
|
const (
|
|
minStatusIntervalMs = 200
|
|
maxStatusIntervalMs = 2000
|
|
)
|
|
|
|
type onionListenService struct {
|
|
os *tor.OnionService
|
|
tp *torProvider
|
|
}
|
|
|
|
type torProvider struct {
|
|
t *tor.Tor
|
|
dialer *tor.Dialer
|
|
appDirectory string
|
|
bundeledTorPath string
|
|
lock sync.Mutex
|
|
breakChan chan bool
|
|
childListeners map[string]*onionListenService
|
|
statusCallback func(int, string)
|
|
}
|
|
|
|
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 -1 on error or 0-100 on the percent the bootstrapping of the underlying network is at and an optional string message
|
|
func (tp *torProvider) GetBootstrapStatus() (int, string) {
|
|
if tp.t == nil {
|
|
return -1, "error: no tor, trying to restart..."
|
|
}
|
|
kvs, err := tp.t.Control.GetInfo("status/bootstrap-phase")
|
|
if err != nil {
|
|
return -1, "error"
|
|
}
|
|
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
|
|
}
|
|
|
|
// 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 PrivateKey, port int) (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 = utils.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() {
|
|
if tp.statusCallback != nil {
|
|
tp.statusCallback(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
|
|
}
|
|
|
|
// StartTor creates/starts a Tor ACN and returns a usable ACN object
|
|
func StartTor(appDirectory string, bundledTorPath string) (ACN, error) {
|
|
tp, err := startTor(appDirectory, bundledTorPath)
|
|
if err == nil {
|
|
tp.dialer, err = tp.t.Dialer(nil, &tor.DialConf{})
|
|
if err == nil {
|
|
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) {
|
|
cmd := exec.CommandContext(ctx, exePath, args...)
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
cmd.SysProcAttr = sysProcAttr
|
|
return cmd, nil
|
|
})
|
|
}
|
|
|
|
func startTor(appDirectory string, bundledTorPath string) (*torProvider, error) {
|
|
dataDir := path.Join(appDirectory, "tor")
|
|
os.MkdirAll(dataDir, 0700)
|
|
tp := &torProvider{appDirectory: appDirectory, bundeledTorPath: bundledTorPath, childListeners: make(map[string]*onionListenService), breakChan: make(chan bool), statusCallback: nil}
|
|
|
|
// attempt connect to system tor
|
|
log.Debugf("dialing system tor control port\n")
|
|
controlport, err := dialControlPort(9051)
|
|
|
|
if err == nil {
|
|
// TODO: configurable auth
|
|
err := controlport.Authenticate("")
|
|
if err == nil {
|
|
log.Debugln("connected to control port")
|
|
pinfo, err := controlport.ProtocolInfo()
|
|
if err == nil && minTorVersionReqs(pinfo.TorVersion) {
|
|
log.Debugln("OK version " + pinfo.TorVersion)
|
|
tp.t = createFromExisting(controlport, dataDir)
|
|
return tp, nil
|
|
}
|
|
controlport.Close()
|
|
}
|
|
}
|
|
|
|
// if not, try running system tor
|
|
if checkCmdlineTorVersion("tor") {
|
|
t, err := tor.Start(nil, &tor.StartConf{EnableNetwork: true, DataDir: dataDir, DebugWriter: nil, ProcessCreator: newHideCmd("tor")})
|
|
if err == nil {
|
|
tp.t = t
|
|
return tp, nil
|
|
}
|
|
log.Debugf("Error connecting to self-run system tor: %v\n", err)
|
|
}
|
|
|
|
// try running bundledTor
|
|
if bundledTorPath != "" && checkCmdlineTorVersion(bundledTorPath) {
|
|
log.Debugln("using bundled tor '" + bundledTorPath + "'")
|
|
t, err := tor.Start(nil, &tor.StartConf{EnableNetwork: true, DataDir: dataDir, ExePath: bundledTorPath, DebugWriter: nil, ProcessCreator: newHideCmd(bundledTorPath)})
|
|
if err != nil {
|
|
log.Debugf("Error running bundled tor: %v\n", err)
|
|
}
|
|
tp.t = t
|
|
return tp, err
|
|
}
|
|
return nil, errors.New("Could not connect to or start Tor that met requirments (min Tor version 0.3.5.x)")
|
|
}
|
|
|
|
func (tp *torProvider) unregisterListener(id string) {
|
|
tp.lock.Lock()
|
|
defer tp.lock.Unlock()
|
|
delete(tp.childListeners, id)
|
|
}
|
|
|
|
func (tp *torProvider) monitorRestart() {
|
|
lastBootstrapProgress := 0
|
|
interval := minStatusIntervalMs
|
|
|
|
for {
|
|
select {
|
|
case <-time.After(time.Millisecond * time.Duration(interval)):
|
|
prog, status := tp.GetBootstrapStatus()
|
|
|
|
if prog == -1 && tp.t != nil {
|
|
if tp.statusCallback != nil {
|
|
tp.statusCallback(prog, status)
|
|
}
|
|
tp.restart()
|
|
interval = minStatusIntervalMs
|
|
} else if prog != lastBootstrapProgress {
|
|
if tp.statusCallback != nil {
|
|
tp.statusCallback(prog, status)
|
|
}
|
|
interval = minStatusIntervalMs
|
|
} else {
|
|
if interval < maxStatusIntervalMs {
|
|
interval *= 2
|
|
}
|
|
}
|
|
lastBootstrapProgress = prog
|
|
case <-tp.breakChan:
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (tp *torProvider) restart() {
|
|
|
|
for _, child := range tp.childListeners {
|
|
child.Close()
|
|
}
|
|
|
|
tp.lock.Lock()
|
|
defer tp.lock.Unlock()
|
|
|
|
tp.t.Close()
|
|
tp.t = nil
|
|
|
|
for {
|
|
newTp, err := startTor(tp.appDirectory, tp.bundeledTorPath)
|
|
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: false,
|
|
DebugWriter: nil,
|
|
StopProcessOnClose: false,
|
|
GeoIPCreatedFile: "",
|
|
GeoIPv6CreatedFile: "",
|
|
}
|
|
t.Control.DebugWriter = t.DebugWriter
|
|
|
|
t.EnableNetwork(nil, true)
|
|
|
|
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
|
|
}
|