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

1
.gitignore vendored
View File

@ -4,3 +4,4 @@ tor/tor/
vendor/ vendor/
*.cover.out *.cover.out
tmp/ 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 // ListenService is an address that was opened with Listen() and can Accept() new connections
type ListenService interface { 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 is the full network address, ex: rsjeuxzlexy4fvo75vrdtj37nrvlmvbw57n5mhypcjpzv3xkka3l4yyd.onion:9878
AddressFull() string AddressFull() string
@ -38,7 +35,7 @@ type ACN interface {
// On ACN error state it returns -2 // On ACN error state it returns -2
GetBootstrapStatus() (int, string) GetBootstrapStatus() (int, string)
// WaitTillBootstrapped Blocks until underlying network is bootstrapped // WaitTillBootstrapped Blocks until underlying network is bootstrapped
WaitTillBootstrapped() WaitTillBootstrapped() error
// Sets the callback function to be called when ACN status changes // Sets the callback function to be called when ACN status changes
SetStatusCallback(callback func(int, string)) 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 returns a string of what the ACN returns when asked for a version
GetVersion() string GetVersion() string
Callback() func(int, string)
Close() 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{} return &localProvider{}
} }
func (lp *localProvider) Callback() func(int, string) {
return func(int, string) {}
}
func (ls *localListenService) AddressFull() string { func (ls *localListenService) AddressFull() string {
return ls.l.Addr().String() return ls.l.Addr().String()
} }
@ -52,7 +56,8 @@ func (lp *localProvider) GetVersion() string {
} }
// WaitTillBootstrapped Blocks until underlying network is bootstrapped // 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) { 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 { if err != nil {
t.Fatalf("tor failed to start: %v", err) t.Fatalf("tor failed to start: %v", err)
} else { } else {
acn.WaitTillBootstrapped() err := acn.WaitTillBootstrapped()
if err != nil {
t.Fatalf("error bootstrapping tor %v", err)
}
if pid, err := acn.GetPID(); err == nil { if pid, err := acn.GetPID(); err == nil {
t.Logf("tor pid: %v", pid) t.Logf("tor pid: %v", pid)
} else { } else {

View File

@ -1,9 +1,7 @@
#!/bin/sh #!/bin/sh
echo "Checking code quality (you want to see no output here)" echo "Checking code quality (you want to see no output here)"
echo ""
echo "Formatting:"
gofmt -s -w -l .
echo "Vetting:" echo "Vetting:"
go list ./... | xargs go vet go list ./... | xargs go vet
@ -11,12 +9,16 @@ go list ./... | xargs go vet
echo "" echo ""
echo "Linting:" echo "Linting:"
go list ./... | xargs golint staticcheck ./...
echo "Time to format"
gofmt -l -s -w .
# ineffassign (https://github.com/gordonklaus/ineffassign) # ineffassign (https://github.com/gordonklaus/ineffassign)
echo "Checking for ineffectual assignment of errors (unchecked errors...)" echo "Checking for ineffectual assignment of errors (unchecked errors...)"
ineffassign . ineffassign .
# misspell (https://github.com/client9/misspell) # misspell (https://github.com/client9/misspell/cmd/misspell)
echo "Checking for misspelled words..." 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 { type onionListenService struct {
lock sync.Mutex lock sync.Mutex
os *tor.OnionService os *tor.OnionService
tp *torProvider tp *torProvider
} }
type torProvider struct { type torProvider struct {
@ -72,6 +72,7 @@ type torProvider struct {
statusCallback func(int, string) statusCallback func(int, string)
lastRestartTime time.Time lastRestartTime time.Time
authenticator tor.Authenticator authenticator tor.Authenticator
isClosed bool
} }
func (ols *onionListenService) AddressFull() string { func (ols *onionListenService) AddressFull() string {
@ -80,21 +81,13 @@ func (ols *onionListenService) AddressFull() string {
return ols.os.Addr().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) { func (ols *onionListenService) Accept() (net.Conn, error) {
return ols.os.Accept() return ols.os.Accept()
} }
func (ols *onionListenService) Close() { func (ols *onionListenService) Close() {
address := ols.AddressIdentity()
ols.lock.Lock() ols.lock.Lock()
defer ols.lock.Unlock() defer ols.lock.Unlock()
ols.tp.unregisterListener(address)
ols.os.Close() ols.os.Close()
} }
@ -156,21 +149,25 @@ func (tp *torProvider) GetVersion() string {
return "No Tor" 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 // WaitTillBootstrapped Blocks until underlying network is bootstrapped
func (tp *torProvider) WaitTillBootstrapped() { func (tp *torProvider) WaitTillBootstrapped() error {
for { for !tp.closed() {
progress, _ := tp.GetBootstrapStatus() progress, _ := tp.GetBootstrapStatus()
if progress == 100 { if progress == 100 {
break return nil
} }
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
} }
return errors.New("close called before bootstrap")
} }
func (tp *torProvider) Listen(identity connectivity.PrivateKey, port int) (connectivity.ListenService, error) { func (tp *torProvider) Listen(identity connectivity.PrivateKey, port int) (connectivity.ListenService, error) {
var onion = ""
var privkey ed25519.PrivateKey
tp.lock.Lock() tp.lock.Lock()
defer tp.lock.Unlock() 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") return nil, errors.New("tor provider closed")
} }
var onion string
var privkey ed25519.PrivateKey
switch pk := identity.(type) { switch pk := identity.(type) {
case ed25519.PrivateKey: case ed25519.PrivateKey:
privkey = pk privkey = pk
@ -185,7 +185,11 @@ func (tp *torProvider) Listen(identity connectivity.PrivateKey, port int) (conne
switch pubk := gpubk.(type) { switch pubk := gpubk.(type) {
case ed25519.PublicKey: case ed25519.PublicKey:
onion = GetTorV3Hostname(pubk) 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 // 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 os.CloseLocalListenerOnClose = true
ols := &onionListenService{os: os, tp: tp} ols := &onionListenService{os: os, tp: tp}
tp.childListeners[ols.AddressIdentity()] = ols tp.childListeners[ols.AddressFull()] = ols
return ols, nil return ols, nil
} }
@ -284,16 +288,24 @@ func (tp *torProvider) Open(hostname string) (net.Conn, string, error) {
} }
func (tp *torProvider) Close() { func (tp *torProvider) Close() {
for _, child := range tp.childListeners {
child.Close()
}
tp.lock.Lock() tp.lock.Lock()
defer tp.lock.Unlock() defer tp.lock.Unlock()
tp.breakChan <- true
if tp.t != nil { // Unregister Child Listeners
tp.t.Close() for addr, child := range tp.childListeners {
tp.t = nil 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 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) { func (tp *torProvider) callStatusCallback(prog int, status string) {
tp.lock.Lock() tp.lock.Lock()
if tp.statusCallback != nil { if tp.statusCallback != nil {
@ -410,7 +428,7 @@ func startTor(appDirectory string, bundledTorPath string, controlPort int, authe
tp.t = t tp.t = t
} else { } else {
log.Debugln("Could not find a viable tor running or to run") 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() err = tp.checkVersion()
@ -430,12 +448,6 @@ func (tp *torProvider) GetPID() (int, error) {
return 0, err return 0, err
} }
func (tp *torProvider) unregisterListener(id string) {
tp.lock.Lock()
defer tp.lock.Unlock()
delete(tp.childListeners, id)
}
func (tp *torProvider) monitorRestart() { func (tp *torProvider) monitorRestart() {
lastBootstrapProgress := networkUnknown lastBootstrapProgress := networkUnknown
interval := minStatusIntervalMs interval := minStatusIntervalMs

View File

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

View File

@ -98,3 +98,11 @@ func TestGenerateTorrc(t *testing.T) {
} }
os.Remove(path) 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 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. // WithOnionTrafficOnly ensures that the tor process only routes tor onion traffic.
func (tb *TorrcBuilder) WithOnionTrafficOnly() *TorrcBuilder { func (tb *TorrcBuilder) WithOnionTrafficOnly() *TorrcBuilder {
for i, line := range tb.lines { 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) 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 // 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 // this function takes a salt as input which is not great from an api-misuse perspective, but
// we make it private. // we make it private.