Merge pull request 'Allow Custom Tor Config in TorRCBuilder + ProxyACN and ErrorACN' (#25) from custom_tor_config into master
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/tag Build is pending Details

Reviewed-on: #25
This commit is contained in:
erinn 2022-01-12 20:16:41 +00:00
commit 85da1dac37
13 changed files with 219 additions and 53 deletions

View File

@ -10,10 +10,10 @@ pipeline:
event: [ push, pull_request ]
image: golang
commands:
- go install honnef.co/go/tools/cmd/staticcheck@latest
- wget https://git.openprivacy.ca/openprivacy/buildfiles/raw/master/tor/tor -P tmp/
- chmod a+x tmp/tor
- go mod download
- go get -u golang.org/x/lint/golint
quality:
when:
repo: openprivacy/connectivity
@ -21,8 +21,7 @@ pipeline:
event: [ push, pull_request ]
image: golang
commands:
- go list ./... | xargs go vet
- go list ./... | xargs golint -set_exit_status
- staticcheck ./...
units-tests:
when:
repo: openprivacy/connectivity

1
.gitignore vendored
View File

@ -4,3 +4,4 @@ tor/tor/
vendor/
*.cover.out
tmp/
testing/tor/*

7
acn.go
View File

@ -21,9 +21,6 @@ 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
@ -38,7 +35,7 @@ type ACN interface {
// On ACN error state it returns -2
GetBootstrapStatus() (int, string)
// WaitTillBootstrapped Blocks until underlying network is bootstrapped
WaitTillBootstrapped()
WaitTillBootstrapped() error
// Sets the callback function to be called when ACN status changes
SetStatusCallback(callback func(int, string))
@ -58,5 +55,7 @@ type ACN interface {
// GetVersion returns a string of what the ACN returns when asked for a version
GetVersion() string
Callback() func(int, string)
Close()
}

51
error_acn.go Normal file
View File

@ -0,0 +1,51 @@
package connectivity
import (
"errors"
"fmt"
"net"
)
// ErrorACN - a status-callback safe errored ACN. Use this when ACN construction goes wrong
// and you need a safe substitute that can later be replaced with a working ACN without impacting calling clients.
type ErrorACN struct {
statusCallbackCache func(int, string)
}
func (e ErrorACN) Callback() func(int, string) {
return e.statusCallbackCache
}
func (e ErrorACN) GetBootstrapStatus() (int, string) {
return -1, "error initializing tor"
}
func (e ErrorACN) WaitTillBootstrapped() error {
return errors.New("error initializing tor")
}
func (e *ErrorACN) SetStatusCallback(callback func(int, string)) {
e.statusCallbackCache = callback
}
func (e ErrorACN) Restart() {
}
func (e ErrorACN) Open(hostname string) (net.Conn, string, error) {
return nil, "", fmt.Errorf("error initializing tor")
}
func (e ErrorACN) Listen(identity PrivateKey, port int) (ListenService, error) {
return nil, fmt.Errorf("error initializing tor")
}
func (e ErrorACN) GetPID() (int, error) {
return -1, fmt.Errorf("error initializing tor")
}
func (e ErrorACN) GetVersion() string {
return "Error Initializing Tor"
}
func (e ErrorACN) Close() {
}

View File

@ -18,6 +18,10 @@ func NewLocalACN() ACN {
return &localProvider{}
}
func (lp *localProvider) Callback() func(int, string) {
return func(int, string) {}
}
func (ls *localListenService) AddressFull() string {
return ls.l.Addr().String()
}
@ -52,7 +56,8 @@ func (lp *localProvider) GetVersion() string {
}
// WaitTillBootstrapped Blocks until underlying network is bootstrapped
func (lp *localProvider) WaitTillBootstrapped() {
func (lp *localProvider) WaitTillBootstrapped() error {
return nil
}
func (lp *localProvider) Listen(identity PrivateKey, port int) (ListenService, error) {

74
proxy_acn.go Normal file
View File

@ -0,0 +1,74 @@
package connectivity
import (
"net"
"sync"
)
// ProxyACN because there is rarely a problem that can't be solved by another layer of indirection.
// ACN is a core resource that many parts of a system may need access too e.g. all clients and servers need an instance
// and a UI may also need status information and a configuration interface.
// We want to allow configuration and replacement of an ACN without impacting the API of all downstream systems - introducing
// ProxyACN - a wrapper around an ACN that allows safe replacement of a running ACN that is transparent to callers.
type ProxyACN struct {
acn ACN
// All operations on the underlying acn are assumed to be thread safe, however changing the actual
// acn in ReplaceACN will lock to force an ordering of Close and Callback
lock sync.Mutex
}
func NewProxyACN(acn ACN) ProxyACN {
return ProxyACN{
acn: acn,
}
}
// ReplaceACN closes down the current ACN and replaces it with a new ACN.
func (p *ProxyACN) ReplaceACN(acn ACN) {
p.lock.Lock()
defer p.lock.Unlock()
p.acn.Close()
acn.SetStatusCallback(p.acn.Callback())
p.acn = acn
}
func (p *ProxyACN) GetBootstrapStatus() (int, string) {
return p.acn.GetBootstrapStatus()
}
func (p *ProxyACN) WaitTillBootstrapped() error {
return p.acn.WaitTillBootstrapped()
}
func (p *ProxyACN) SetStatusCallback(callback func(int, string)) {
p.acn.SetStatusCallback(callback)
}
func (p *ProxyACN) Restart() {
p.acn.Restart()
}
func (p *ProxyACN) Open(hostname string) (net.Conn, string, error) {
return p.acn.Open(hostname)
}
func (p *ProxyACN) Listen(identity PrivateKey, port int) (ListenService, error) {
return p.acn.Listen(identity, port)
}
func (p *ProxyACN) GetPID() (int, error) {
return p.acn.GetPID()
}
func (p *ProxyACN) GetVersion() string {
return p.acn.GetVersion()
}
func (p *ProxyACN) Close() {
p.acn.Close()
}
func (p *ProxyACN) Callback() func(int, string) {
return p.acn.Callback()
}

View File

@ -32,7 +32,11 @@ func TestLaunchTor(t *testing.T) {
if err != nil {
t.Fatalf("tor failed to start: %v", err)
} else {
acn.WaitTillBootstrapped()
err := acn.WaitTillBootstrapped()
if err != nil {
t.Fatalf("error bootstrapping tor %v", err)
}
if pid, err := acn.GetPID(); err == nil {
t.Logf("tor pid: %v", pid)
} else {

View File

@ -1,9 +1,7 @@
#!/bin/sh
echo "Checking code quality (you want to see no output here)"
echo "Formatting:"
gofmt -s -w -l .
echo ""
echo "Vetting:"
go list ./... | xargs go vet
@ -11,12 +9,16 @@ go list ./... | xargs go vet
echo ""
echo "Linting:"
go list ./... | xargs golint
staticcheck ./...
echo "Time to format"
gofmt -l -s -w .
# ineffassign (https://github.com/gordonklaus/ineffassign)
echo "Checking for ineffectual assignment of errors (unchecked errors...)"
ineffassign .
# misspell (https://github.com/client9/misspell)
# misspell (https://github.com/client9/misspell/cmd/misspell)
echo "Checking for misspelled words..."
go list ./... | xargs misspell
misspell . | grep -v "testing/" | grep -v "vendor/" | grep -v "go.sum" | grep -v ".idea"

View File

@ -1,4 +0,0 @@
SOCKSPort 9050
ControlPort 9051
# "examplehashedpassword" - used for testing
HashedControlPassword 16:C15305F97789414B601259E3EC5E76B8E55FC56A9F562B713F3D2BA257

View File

@ -55,9 +55,9 @@ func (l *logWriter) Write(p []byte) (int, error) {
}
type onionListenService struct {
lock sync.Mutex
os *tor.OnionService
tp *torProvider
lock sync.Mutex
os *tor.OnionService
tp *torProvider
}
type torProvider struct {
@ -72,6 +72,7 @@ type torProvider struct {
statusCallback func(int, string)
lastRestartTime time.Time
authenticator tor.Authenticator
isClosed bool
}
func (ols *onionListenService) AddressFull() string {
@ -80,21 +81,13 @@ func (ols *onionListenService) AddressFull() string {
return ols.os.Addr().String()
}
func (ols *onionListenService) AddressIdentity() string {
ols.lock.Lock()
defer ols.lock.Unlock()
return ols.os.Addr().String()[:56]
}
func (ols *onionListenService) Accept() (net.Conn, error) {
return ols.os.Accept()
}
func (ols *onionListenService) Close() {
address := ols.AddressIdentity()
ols.lock.Lock()
defer ols.lock.Unlock()
ols.tp.unregisterListener(address)
ols.os.Close()
}
@ -156,21 +149,25 @@ func (tp *torProvider) GetVersion() string {
return "No Tor"
}
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() {
for {
func (tp *torProvider) WaitTillBootstrapped() error {
for !tp.closed() {
progress, _ := tp.GetBootstrapStatus()
if progress == 100 {
break
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) {
var onion = ""
var privkey ed25519.PrivateKey
tp.lock.Lock()
defer tp.lock.Unlock()
@ -178,6 +175,9 @@ func (tp *torProvider) Listen(identity connectivity.PrivateKey, port int) (conne
return nil, errors.New("tor provider closed")
}
var onion string
var privkey ed25519.PrivateKey
switch pk := identity.(type) {
case ed25519.PrivateKey:
privkey = pk
@ -185,7 +185,11 @@ func (tp *torProvider) Listen(identity connectivity.PrivateKey, port int) (conne
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
@ -221,7 +225,7 @@ func (tp *torProvider) Listen(identity connectivity.PrivateKey, port int) (conne
os.CloseLocalListenerOnClose = true
ols := &onionListenService{os: os, tp: tp}
tp.childListeners[ols.AddressIdentity()] = ols
tp.childListeners[ols.AddressFull()] = ols
return ols, nil
}
@ -284,16 +288,24 @@ func (tp *torProvider) Open(hostname string) (net.Conn, string, error) {
}
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
// Unregister Child Listeners
for addr, child := range tp.childListeners {
child.Close()
delete(tp.childListeners, addr)
}
if !tp.isClosed {
// Break out of any background checks and close
// the underlying tor connection
tp.isClosed = true
tp.breakChan <- true
if tp.t != nil {
tp.t.Close()
tp.t = nil
}
}
}
@ -303,6 +315,12 @@ func (tp *torProvider) SetStatusCallback(callback func(int, string)) {
tp.statusCallback = callback
}
func (tp *torProvider) Callback() func(int, string) {
tp.lock.Lock()
defer tp.lock.Unlock()
return tp.statusCallback
}
func (tp *torProvider) callStatusCallback(prog int, status string) {
tp.lock.Lock()
if tp.statusCallback != nil {
@ -410,7 +428,7 @@ func startTor(appDirectory string, bundledTorPath string, controlPort int, authe
tp.t = t
} 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)")
return nil, fmt.Errorf("could not connect to or start Tor that met requirements (min Tor version 0.3.5.x)")
}
err = tp.checkVersion()
@ -430,12 +448,6 @@ func (tp *torProvider) GetPID() (int, error) {
return 0, err
}
func (tp *torProvider) unregisterListener(id string) {
tp.lock.Lock()
defer tp.lock.Unlock()
delete(tp.childListeners, id)
}
func (tp *torProvider) monitorRestart() {
lastBootstrapProgress := networkUnknown
interval := minStatusIntervalMs

View File

@ -3,7 +3,7 @@ package tor
import (
"fmt"
"git.openprivacy.ca/openprivacy/log"
"path"
path "path/filepath"
"testing"
)
@ -18,6 +18,9 @@ func TestTorProvider(t *testing.T) {
progChan := make(chan int)
log.SetLevel(log.LevelDebug)
torpath := path.Join("..", "tmp/tor")
NewTorrc().WithControlPort(9051).WithHashedPassword("examplehashedpassword").Build(path.Join("..", "testing", "tor", "torrc"))
log.Debugf("setting tor path %v", torpath)
acn, err := NewTorACNWithAuth(path.Join("../testing/"), torpath, 9051, HashedPasswordAuthenticator{"examplehashedpassword"})
if err != nil {

View File

@ -98,3 +98,11 @@ func TestGenerateTorrc(t *testing.T) {
}
os.Remove(path)
}
func TestPreviewTorrc(t *testing.T) {
expected := "SocksPort 9050 OnionTrafficOnly\nControlPort 9061"
torrc := NewTorrc().WithCustom([]string{"SocksPort 9050"}).WithControlPort(9061).WithOnionTrafficOnly().Preview()
if torrc != expected {
t.Fatalf("unexpected torrc generated: [%v] [%v]", expected, torrc)
}
}

View File

@ -34,6 +34,13 @@ func (tb *TorrcBuilder) WithControlPort(port int) *TorrcBuilder {
return tb
}
// WithCustom clobbers the torrc builder and allows the client to set any option they want, while benefiting
// from other configuration options.
func (tb *TorrcBuilder) WithCustom(lines []string) *TorrcBuilder {
tb.lines = lines
return tb
}
// WithOnionTrafficOnly ensures that the tor process only routes tor onion traffic.
func (tb *TorrcBuilder) WithOnionTrafficOnly() *TorrcBuilder {
for i, line := range tb.lines {
@ -61,6 +68,11 @@ func (tb *TorrcBuilder) Build(path string) error {
return ioutil.WriteFile(path, []byte(strings.Join(tb.lines, "\n")), 0600)
}
// Preview provides a string representation of the torrc file without writing it to a file location.
func (tb *TorrcBuilder) Preview() string {
return strings.Join(tb.lines, "\n")
}
// GenerateHashedPassword calculates a hash in the same way tha tor --hash-password does
// this function takes a salt as input which is not great from an api-misuse perspective, but
// we make it private.