diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5884cae --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +tor/ diff --git a/LICENSE b/LICENSE index d449d3e..acae383 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) +Copyright (c) <2020> Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/acn.go b/acn.go new file mode 100644 index 0000000..de17257 --- /dev/null +++ b/acn.go @@ -0,0 +1,46 @@ +package connectivity + +import ( + "net" +) + +// PrivateKey represents a private key using an unspecified algorithm. +type PrivateKey interface{} + +// ListenService is an address that was opened with Listen() and can Accept() new connections +type ListenService interface { + // AddressIdentity is the core "identity" part of an address, ex: rsjeuxzlexy4fvo75vrdtj37nrvlmvbw57n5mhypcjpzv3xkka3l4yyd + AddressIdentity() string + + // AddressFull is the full network address, ex: rsjeuxzlexy4fvo75vrdtj37nrvlmvbw57n5mhypcjpzv3xkka3l4yyd.onion:9878 + AddressFull() string + + Accept() (net.Conn, error) + Close() +} + +// ACN is Anonymous Communication Network implementation wrapper that supports Open for new connections and Listen to accept connections +type ACN interface { + // GetBootstrapStatus returns an int 0-100 on the percent the bootstrapping of the underlying network is at and an optional string message + GetBootstrapStatus() (int, string) + // WaitTillBootstrapped Blocks until underlying network is bootstrapped + WaitTillBootstrapped() + // Sets the calback function to be called when ACN status changes + SetStatusCallback(callback func(int, string)) + + // Restarts the underlying connection + Restart() + + // Open takes a hostname and returns a net.conn to the derived endpoint + // Open allows a client to resolve various hostnames to connections + // The supported types are onions address are: + // * ricochet:jlq67qzo6s4yp3sp + // * jlq67qzo6s4yp3sp + // * 127.0.0.1:55555|jlq67qzo6s4yp3sp - Localhost Connection + Open(hostname string) (net.Conn, string, error) + + // Listen takes a private key and a port and returns a ListenService for it + Listen(identity PrivateKey, port int) (ListenService, error) + + Close() +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..515fd7d --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module git.openprivacy.ca/connectivity + +go 1.13 + +require ( + git.openprivacy.ca/openprivacy/libricochet-go v1.0.10 + github.com/cretz/bine v0.1.1-0.20200124154328-f9f678b84cca + golang.org/x/crypto v0.0.0-20200204104054-c9f3fb736b72 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..8931c82 --- /dev/null +++ b/go.sum @@ -0,0 +1,26 @@ +git.openprivacy.ca/openprivacy/libricochet-go v1.0.10 h1:yxEqFJH4EdacPwGuOXx+QieYqIPDyzWP50H27EI7fxI= +git.openprivacy.ca/openprivacy/libricochet-go v1.0.10/go.mod h1:jJdxIwYDCcM4w4HAydeHuksPRTirUnyERAloPL0qtic= +github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 h1:w1UutsfOrms1J05zt7ISrnJIXKzwaspym5BTKGx93EI= +github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412/go.mod h1:WPjqKcmVOxf0XSf3YxCJs6N6AOSrOx3obionmG7T0y0= +github.com/cretz/bine v0.1.0 h1:1/fvhLE+fk0bPzjdO5Ci+0ComYxEMuB1JhM4X5skT3g= +github.com/cretz/bine v0.1.0/go.mod h1:6PF6fWAvYtwjRGkAuDEJeWNOv3a2hUouSP/yRYXmvHw= +github.com/cretz/bine v0.1.1-0.20200124154328-f9f678b84cca h1:Q2r7AxHdJwWfLtBZwvW621M3sPqxPc6ITv2j1FGsYpw= +github.com/cretz/bine v0.1.1-0.20200124154328-f9f678b84cca/go.mod h1:6PF6fWAvYtwjRGkAuDEJeWNOv3a2hUouSP/yRYXmvHw= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +golang.org/x/crypto v0.0.0-20190128193316-c7b33c32a30b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200204104054-c9f3fb736b72 h1:+ELyKg6m8UBf0nPFSqD0mi7zUfwPyXo23HNjMnXPz7w= +golang.org/x/crypto v0.0.0-20200204104054-c9f3fb736b72/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/localProvider.go b/localProvider.go new file mode 100644 index 0000000..fc71662 --- /dev/null +++ b/localProvider.go @@ -0,0 +1,77 @@ +package connectivity + +import ( + "fmt" + "net" + "strings" +) + +type localListenService struct { + l net.Listener +} + +type localProvider struct { +} + +func (ls *localListenService) AddressFull() string { + return ls.l.Addr().String() +} + +func (ls *localListenService) AddressIdentity() string { + return ls.l.Addr().String() +} + +func (ls *localListenService) Accept() (net.Conn, error) { + return ls.l.Accept() +} + +func (ls *localListenService) Close() { + ls.l.Close() +} + +// GetBootstrapStatus returns an int 0-100 on the percent the bootstrapping of the underlying network is at and an optional string message +func (lp *localProvider) GetBootstrapStatus() (int, string) { + return 100, "Done" +} + +func (lp *localProvider) SetStatusCallback(callback func(int, string)) { + // nop +} + +// WaitTillBootstrapped Blocks until underlying network is bootstrapped +func (lp *localProvider) WaitTillBootstrapped() { +} + +func (lp *localProvider) Listen(identity PrivateKey, port int) (ListenService, error) { + l, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%v", port)) + return &localListenService{l}, err +} + +func (lp *localProvider) Open(hostname string) (net.Conn, string, error) { + // Localhost (127.0.0.1:55555|jlq67qzo6s4yp3sp) for testing + addrParts := strings.Split(hostname, "|") + tcpAddr, err := net.ResolveTCPAddr("tcp", addrParts[0]) + if err != nil { + return nil, "", CannotResolveLocalTCPAddressError + } + conn, err := net.DialTCP("tcp", nil, tcpAddr) + if err != nil { + return nil, "", CannotDialLocalTCPAddressError + } + // return just the onion address, not the local override for the hostname + return conn, addrParts[1], nil + +} + +func (lp *localProvider) Restart() { + //noop +} + +func (lp *localProvider) Close() { + +} + +// LocalProvider returns a for testing use only local clearnet implementation of a ACN interface +func LocalProvider() ACN { + return &localProvider{} +} diff --git a/quality.sh b/quality.sh new file mode 100644 index 0000000..0599b46 --- /dev/null +++ b/quality.sh @@ -0,0 +1,22 @@ +#!/bin/sh + +echo "Checking code quality (you want to see no output here)" + +echo "Formatting:" +gofmt -s -w -l . + +echo "Vetting:" +go list ./... | xargs go vet + +echo "" +echo "Linting:" + +go list ./... | xargs golint + +# ineffassign (https://github.com/gordonklaus/ineffassign) +echo "Checking for ineffectual assignment of errors (unchecked errors...)" +ineffassign . + +# misspell (https://github.com/client9/misspell) +echo "Checking for misspelled words..." +go list ./... | xargs misspell diff --git a/sysProcAttr_rest.go b/sysProcAttr_rest.go new file mode 100644 index 0000000..e5aa6d6 --- /dev/null +++ b/sysProcAttr_rest.go @@ -0,0 +1,9 @@ +// +build !windows + +package connectivity + +import ( + "syscall" +) + +var sysProcAttr = &syscall.SysProcAttr{} diff --git a/sysProcAttr_win.go b/sysProcAttr_win.go new file mode 100644 index 0000000..501ff9d --- /dev/null +++ b/sysProcAttr_win.go @@ -0,0 +1,9 @@ +// +build windows + +package connectivity + +import ( + "syscall" +) + +var sysProcAttr = &syscall.SysProcAttr{HideWindow: true} diff --git a/torProvider.go b/torProvider.go new file mode 100644 index 0000000..4a1e524 --- /dev/null +++ b/torProvider.go @@ -0,0 +1,382 @@ +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 +} diff --git a/torProvider_test.go b/torProvider_test.go new file mode 100644 index 0000000..e3fce67 --- /dev/null +++ b/torProvider_test.go @@ -0,0 +1,30 @@ +package connectivity + +import ( + "fmt" + "testing" +) + +func getStatusCallback(progChan chan int) func(int, string) { + return func(prog int, status string) { + fmt.Printf("%v %v\n", prog, status) + progChan <- prog + } +} + +func TestTorProvider(t *testing.T) { + progChan := make(chan int) + acn, err := StartTor(".", "") + if err != nil { + t.Error(err) + return + } + acn.SetStatusCallback(getStatusCallback(progChan)) + + progress := 0 + for progress < 100 { + progress = <-progChan + } + + acn.Close() +}