Compare commits

..

No commits in common. "master" and "master" have entirely different histories.

19 changed files with 222 additions and 892 deletions

View File

@ -1,76 +1,65 @@
---
kind: pipeline
type: docker
name: linux-test
workspace:
base: /go
path: src/git.openprivacy.ca/openprivacy/connectivity
steps:
- name: fetch
image: golang:1.19.1
volumes:
- name: deps
path: /go
pipeline:
fetch:
when:
repo: openprivacy/connectivity
branch: master
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
- name: quality
image: golang:1.19.1
volumes:
- name: deps
path: /go
- export GO111MODULE=on
- go mod vendor
- go get -u golang.org/x/lint/golint
quality:
when:
repo: openprivacy/connectivity
branch: master
event: [ push, pull_request ]
image: golang
commands:
- staticcheck ./...
- name: units-tests
image: golang:1.19.1
volumes:
- name: deps
path: /go
- go list ./... | xargs go vet
- go list ./... | xargs golint -set_exit_status
units-tests:
when:
repo: openprivacy/connectivity
branch: master
event: [ push, pull_request ]
image: golang
commands:
- export PATH=`pwd`:$PATH
- export PATH=$PATH:/go/src/git.openprivacy.ca/openprivacy/connectivity
- ./tmp/tor -f ./testing/torrc
- sleep 15
- sh testing/tests.sh
- pkill -9 tor
- name: integration-tests
image: golang:1.19.1
volumes:
- name: deps
path: /go
integration-tests:
when:
repo: openprivacy/connectivity
branch: master
event: [ push, pull_request ]
image: golang
commands:
- export PATH=`pwd`:$PATH
- go test -race -v ./testing/launch_tor_integration_test.go
- name: notify-email
notify-email:
image: drillster/drone-email
pull: if-not-exists
host: build.openprivacy.ca
port: 25
skip_verify: true
from: drone@openprivacy.ca
when:
repo: openprivacy/connectivity
branch: master
status: [ failure ]
- name: notify-gogs
notify-gogs:
image: openpriv/drone-gogs
pull: if-not-exists
when:
repo: openprivacy/connectivity
branch: master
event: pull_request
status: [ success, changed, failure ]
environment:
GOGS_ACCOUNT_TOKEN:
from_secret: gogs_account_token
settings:
gogs_url: https://git.openprivacy.ca
volumes:
# gopath where bin and pkg lives to persist across steps
- name: deps
temp: {}
trigger:
repo: openprivacy/connectivity
branch: master
event:
- push
- pull_request
- tag
secrets: [gogs_account_token]
gogs_url: https://git.openprivacy.ca

3
.gitignore vendored
View File

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

View File

@ -7,12 +7,6 @@ A library providing an ACN (Anonymous Communication Network
* Tor v3 Onion Services
## Environment Variables
- `TOR_LD_LIBRARY_PATH` - override the library path given to the Tor process as different from the one given to the parent process.
- `CWTCH_RESTRICT_PORTS` - forces connectivity to bind to a subset of ports `15000-15378`
- `CWTCH_BIND_EXTERNAL_WHONIX` - forces connectivity to bind to external interfaces (only supported/recommended on certain Whonix-based setups. Please open an issue if you think this should be expanded.)
## Requirements for ACN Support
* Reference an EndPoint via a string / hostname
@ -56,4 +50,4 @@ service:
acn.Restart()
and
acn.Close()
acn.Close()

14
acn.go
View File

@ -21,6 +21,9 @@ 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
@ -35,17 +38,10 @@ type ACN interface {
// On ACN error state it returns -2
GetBootstrapStatus() (int, string)
// WaitTillBootstrapped Blocks until underlying network is bootstrapped
WaitTillBootstrapped() error
WaitTillBootstrapped()
// Sets the callback function to be called when ACN status changes
SetStatusCallback(callback func(int, string))
GetStatusCallback() func(int, string)
// Sets the callback function to be called when ACN reboots to emit the version
SetVersionCallback(callback func(string))
GetVersionCallback() func(string)
// Restarts the underlying connection
Restart()
@ -62,7 +58,5 @@ type ACN interface {
// GetVersion returns a string of what the ACN returns when asked for a version
GetVersion() string
GetInfo(onion string) (map[string]string, error)
Close()
}

View File

@ -1,72 +0,0 @@
package connectivity
import (
"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 {
acnError error
statusCallbackCache func(int, string)
versionCallbackCache func(string)
}
func NewErrorACN(err error) ErrorACN {
return ErrorACN{
acnError: err,
statusCallbackCache: func(int, string) {},
versionCallbackCache: func(string) {},
}
}
func (e *ErrorACN) GetStatusCallback() func(int, string) {
return e.statusCallbackCache
}
func (e *ErrorACN) GetVersionCallback() func(string) {
return e.versionCallbackCache
}
func (e *ErrorACN) GetInfo(addr string) (map[string]string, error) {
return nil, e.acnError
}
func (e *ErrorACN) GetBootstrapStatus() (int, string) {
return -1, e.acnError.Error()
}
func (e *ErrorACN) WaitTillBootstrapped() error {
return e.acnError
}
func (e *ErrorACN) SetStatusCallback(callback func(int, string)) {
e.statusCallbackCache = callback
}
func (e *ErrorACN) SetVersionCallback(callback func(string)) {
e.versionCallbackCache = callback
}
func (e *ErrorACN) Restart() {
}
func (e *ErrorACN) Open(hostname string) (net.Conn, string, error) {
return nil, "", e.acnError
}
func (e *ErrorACN) Listen(identity PrivateKey, port int) (ListenService, error) {
return nil, e.acnError
}
func (e *ErrorACN) GetPID() (int, error) {
return -1, e.acnError
}
func (e *ErrorACN) GetVersion() string {
return e.acnError.Error()
}
func (e *ErrorACN) Close() {
// nothing to do...
}

14
go.mod
View File

@ -1,15 +1,9 @@
module git.openprivacy.ca/openprivacy/connectivity
go 1.17
go 1.13
require (
filippo.io/edwards25519 v1.0.0
git.openprivacy.ca/openprivacy/bine v0.0.5
git.openprivacy.ca/openprivacy/log v1.0.3
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d
)
require (
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect
git.openprivacy.ca/openprivacy/bine v0.0.4
git.openprivacy.ca/openprivacy/log v1.0.1
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee
)

30
go.sum
View File

@ -1,9 +1,12 @@
filippo.io/edwards25519 v1.0.0 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek=
filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=
git.openprivacy.ca/openprivacy/bine v0.0.5 h1:DJs5gqw3SkvLSgRDvroqJxZ7F+YsbxbBRg5t0rU5gYE=
git.openprivacy.ca/openprivacy/bine v0.0.5/go.mod h1:fwdeq6RO08WDkV0k7HfArsjRvurVULoUQmT//iaABZM=
git.openprivacy.ca/openprivacy/log v1.0.3 h1:E/PMm4LY+Q9s3aDpfySfEDq/vYQontlvNj/scrPaga0=
git.openprivacy.ca/openprivacy/log v1.0.3/go.mod h1:gGYK8xHtndRLDymFtmjkG26GaMQNgyhioNS82m812Iw=
git.openprivacy.ca/openprivacy/bine v0.0.2 h1:2uJyxOYfcYvpQAuRt5XWc81ZXrHuubdFskNOQjksEgc=
git.openprivacy.ca/openprivacy/bine v0.0.2/go.mod h1:13ZqhKyqakDsN/ZkQkIGNULsmLyqtXc46XBcnuXm/mU=
git.openprivacy.ca/openprivacy/bine v0.0.3 h1:PSHUmNqaW7BZUX8n2eTDeNbjsuRe+t5Ae0Og+P+jDM0=
git.openprivacy.ca/openprivacy/bine v0.0.3/go.mod h1:13ZqhKyqakDsN/ZkQkIGNULsmLyqtXc46XBcnuXm/mU=
git.openprivacy.ca/openprivacy/bine v0.0.4/go.mod h1:13ZqhKyqakDsN/ZkQkIGNULsmLyqtXc46XBcnuXm/mU=
git.openprivacy.ca/openprivacy/log v1.0.0 h1:Rvqm1weUdR4AOnJ79b1upHCc9vC/QF1rhSD2Um7sr1Y=
git.openprivacy.ca/openprivacy/log v1.0.0/go.mod h1:gGYK8xHtndRLDymFtmjkG26GaMQNgyhioNS82m812Iw=
git.openprivacy.ca/openprivacy/log v1.0.1 h1:NWV5oBTatvlSzUE6wtB+UQCulgyMOtm4BXGd34evMys=
git.openprivacy.ca/openprivacy/log v1.0.1/go.mod h1:gGYK8xHtndRLDymFtmjkG26GaMQNgyhioNS82m812Iw=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@ -13,25 +16,20 @@ github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee h1:4yd7jl+vXjalO5ztz6Vc1VADv+S/80LGJmyl1ROJ2AI=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d h1:3qF+Z8Hkrw9sOhrFHti9TlB1Hkac1x+DNRkv0XQiFjo=
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb h1:mUVeFHoDKis5nxCAzoAi7E8Ghb86EXh/RK6wtvJIqRY=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
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/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -18,14 +18,6 @@ func NewLocalACN() ACN {
return &localProvider{}
}
func (lp *localProvider) GetStatusCallback() func(int, string) {
return func(int, string) {}
}
func (lp *localProvider) GetVersionCallback() func(string) {
return func(string) {}
}
func (ls *localListenService) AddressFull() string {
return ls.l.Addr().String()
}
@ -42,10 +34,6 @@ func (ls *localListenService) Close() {
ls.l.Close()
}
func (lp *localProvider) GetInfo(addr string) (map[string]string, error) {
return nil, nil
}
// 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"
@ -55,10 +43,6 @@ func (lp *localProvider) SetStatusCallback(callback func(int, string)) {
// nop
}
func (lp *localProvider) SetVersionCallback(callback func(string)) {
// nop
}
func (lp *localProvider) GetPID() (int, error) {
return 0, nil
}
@ -68,8 +52,7 @@ func (lp *localProvider) GetVersion() string {
}
// WaitTillBootstrapped Blocks until underlying network is bootstrapped
func (lp *localProvider) WaitTillBootstrapped() error {
return nil
func (lp *localProvider) WaitTillBootstrapped() {
}
func (lp *localProvider) Listen(identity PrivateKey, port int) (ListenService, error) {

View File

@ -1,87 +0,0 @@
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.GetStatusCallback())
acn.SetVersionCallback(p.acn.GetVersionCallback())
p.acn = acn
}
func (p *ProxyACN) GetInfo(addr string) (map[string]string, error) {
return p.acn.GetInfo(addr)
}
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) SetVersionCallback(callback func(string)) {
p.acn.SetVersionCallback(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) GetStatusCallback() func(int, string) {
return p.acn.GetStatusCallback()
}
func (p *ProxyACN) GetVersionCallback() func(string) {
return p.acn.GetVersionCallback()
}

View File

@ -5,15 +5,12 @@ import (
"git.openprivacy.ca/openprivacy/log"
"math/rand"
"os"
path "path/filepath"
"runtime"
"runtime/pprof"
"path"
"testing"
"time"
)
func TestLaunchTor(t *testing.T) {
goRoutineStart := runtime.NumGoroutine()
log.SetLevel(log.LevelDebug)
rand.Seed(int64(time.Now().Nanosecond()))
@ -28,23 +25,14 @@ func TestLaunchTor(t *testing.T) {
t.Fatalf("failed to create torrc file: %v", err)
}
dataDir := ""
if dataDir, err = os.MkdirTemp(path.Join("..", "testing"), "data-dir-"); err != nil {
t.Fatalf("could not create data dir")
}
// Get the current working director, clean the paths to remove relative references
wd, _ := os.Getwd()
t.Logf("Launching bundled tor at %v", path.Clean(wd+"/../tmp/tor"))
acn, err := tor.NewTorACNWithAuth(path.Clean(wd+"/../tmp/data"), path.Clean(wd+"/../tmp/tor"), dataDir, controlPort, tor.HashedPasswordAuthenticator{Password: password})
acn, err := tor.NewTorACNWithAuth(path.Clean(wd+"/../tmp/data"), path.Clean(wd+"/../tmp/tor"), controlPort, tor.HashedPasswordAuthenticator{Password: password})
if err != nil {
t.Fatalf("tor failed to start: %v", err)
} else {
err := acn.WaitTillBootstrapped()
if err != nil {
t.Fatalf("error bootstrapping tor %v", err)
}
acn.WaitTillBootstrapped()
if pid, err := acn.GetPID(); err == nil {
t.Logf("tor pid: %v", pid)
} else {
@ -53,15 +41,4 @@ func TestLaunchTor(t *testing.T) {
t.Log("we have bootstrapped!")
acn.Close()
}
time.Sleep(time.Second * 5)
goRoutineEnd := runtime.NumGoroutine()
if goRoutineEnd != goRoutineStart {
pprof.Lookup("goroutine").WriteTo(os.Stdout, 1)
t.Fatalf("goroutine leak in ACN: %v %v", goRoutineStart, goRoutineEnd)
}
}

View File

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

4
testing/tor/torrc Normal file
View File

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

View File

@ -1,4 +1,3 @@
//go:build !windows
// +build !windows
package tor

View File

@ -1,4 +1,3 @@
//go:build windows
// +build windows
package tor

View File

@ -1,7 +1,6 @@
package tor
import (
"bytes"
"context"
"errors"
"fmt"
@ -13,11 +12,12 @@ import (
"git.openprivacy.ca/openprivacy/log"
"golang.org/x/crypto/ed25519"
"golang.org/x/crypto/sha3"
"io/ioutil"
"net"
"net/textproto"
"os"
"os/exec"
path "path/filepath"
"path"
"regexp"
"strconv"
"strings"
@ -31,6 +31,11 @@ const (
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
@ -55,9 +60,8 @@ func (l *logWriter) Write(p []byte) (int, error) {
}
type onionListenService struct {
lock sync.Mutex
os *tor.OnionService
tp *torProvider
os *tor.OnionService
tp *torProvider
}
type torProvider struct {
@ -70,109 +74,30 @@ type torProvider struct {
breakChan chan bool
childListeners map[string]*onionListenService
statusCallback func(int, string)
versionCallback func(string)
lastRestartTime time.Time
authenticator tor.Authenticator
isClosed bool
dataDir string
version string
bootProgress int
}
func (ols *onionListenService) AddressFull() string {
ols.lock.Lock()
defer ols.lock.Unlock()
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.lock.Lock()
defer ols.lock.Unlock()
ols.tp.unregisterListener(ols.AddressIdentity())
ols.os.Close()
}
func (tp *torProvider) GetInfo(onion string) (map[string]string, error) {
tp.lock.Lock()
defer tp.lock.Unlock()
circuits, streams, err := getCircuitInfo(tp.t.Control)
if err == nil {
var circuitID string
for _, stream := range streams {
if stream.Key == "stream-status" {
lines := strings.Split(stream.Val, "\n")
for _, line := range lines {
parts := strings.Split(line, " ")
// StreamID SP StreamStatus SP CircuitID SP Target CRLF
if len(parts) == 4 {
if strings.HasPrefix(parts[3], onion) {
circuitID = parts[2]
break
}
}
}
}
}
if circuitID == "" {
return nil, errors.New("could not find circuit")
}
var hops []string
for _, circuit := range circuits {
if circuit.Key == "circuit-status" {
lines := strings.Split(circuit.Val, "\n")
for _, line := range lines {
parts := strings.Split(line, " ")
// CIRCID SP STATUS SP PATH...
if len(parts) >= 3 && circuitID == parts[0] {
//log.Debugf("Found Circuit for Onion %v %v", onion, parts)
if parts[1] == "BUILT" {
circuitPath := strings.Split(parts[2], ",")
for _, hop := range circuitPath {
fingerprint := hop[1:41]
keyvals, err := tp.t.Control.GetInfo(fmt.Sprintf("ns/id/%s", fingerprint))
if err == nil && len(keyvals) == 1 {
lines = strings.Split(keyvals[0].Val, "\n")
for _, line := range lines {
if strings.HasPrefix(line, "r") {
parts := strings.Split(line, " ")
if len(parts) > 6 {
keyvals, err := tp.t.Control.GetInfo(fmt.Sprintf("ip-to-country/%s", parts[6]))
if err == nil && len(keyvals) >= 1 {
hops = append(hops, fmt.Sprintf("%s:%s", strings.ToUpper(keyvals[0].Val), parts[6]))
} else {
hops = append(hops, fmt.Sprintf("%s:%s", "XX", parts[6]))
}
}
}
}
}
}
return map[string]string{"circuit": strings.Join(hops, ",")}, nil
}
}
}
}
}
}
return nil, err
}
var progRe = regexp.MustCompile("PROGRESS=([0-9]*)")
var sumRe = regexp.MustCompile("SUMMARY=\"(.*)\"$")
// 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
// returns -1 on network disconnected
// returns -2 on error
func (tp *torProvider) GetBootstrapStatus() (int, string) {
tp.lock.Lock()
defer tp.lock.Unlock()
@ -198,6 +123,8 @@ func (tp *torProvider) GetBootstrapStatus() (int, string) {
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])
@ -207,45 +134,47 @@ func (tp *torProvider) GetBootstrapStatus() (int, string) {
status = statusMatches[1]
}
}
tp.bootProgress = progress
return progress, status
}
func (tp *torProvider) GetVersion() string {
tp.lock.Lock()
defer tp.lock.Unlock()
return tp.version
}
func (tp *torProvider) closed() bool {
tp.lock.Lock()
defer tp.lock.Unlock()
return tp.isClosed
if tp.t == nil {
return "No Tor"
}
pinfo, err := tp.t.Control.ProtocolInfo()
if err == nil {
return pinfo.TorVersion
}
return "No Tor"
}
// WaitTillBootstrapped Blocks until underlying network is bootstrapped
func (tp *torProvider) WaitTillBootstrapped() error {
for !tp.closed() {
func (tp *torProvider) WaitTillBootstrapped() {
for true {
progress, _ := tp.GetBootstrapStatus()
if progress == 100 {
return nil
break
}
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()
if tp.t == nil {
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) {
case ed25519.PrivateKey:
privkey = pk
@ -253,11 +182,7 @@ 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
@ -268,101 +193,29 @@ func (tp *torProvider) Listen(identity connectivity.PrivateKey, port int) (conne
localport += 1024
}
var localListener net.Listener
var err error
if cwtchRestrictPorts := os.Getenv("CWTCH_RESTRICT_PORTS"); strings.ToLower(cwtchRestrictPorts) == "true" {
// for whonix like systems we tightly restrict possible listen...
// pick a random port between 15000 and 15378
// cwtch = 63 *77 *74* 63* 68 = 1537844616
log.Infof("using restricted ports, CWTCH_RESTRICT_PORTS=true");
localport = 15000 + (localport % 378)
}
if bindExternal := os.Getenv("CWTCH_BIND_EXTERNAL_WHONIX"); strings.ToLower(bindExternal) == "true" {
if _, ferr := os.Stat("/usr/share/anon-ws-base-files/workstation"); !os.IsNotExist(ferr) {
log.Infof("WARNING: binding to external interfaces. This is potentially unsafe outside of a containerized environment.");
localListener, err = net.Listen("tcp", "0.0.0.0:"+strconv.Itoa(localport))
} else {
log.Errorf("CWTCH_BIND_EXTERNAL_WHONIX flag set, but /usr/share/anon-ws-base-files/workstation does not exist. Defaulting to binding to local ports");
localListener, err = net.Listen("tcp", "127.0.0.1:"+strconv.Itoa(localport))
}
} else {
localListener, err = net.Listen("tcp", "127.0.0.1:"+strconv.Itoa(localport))
}
if err != nil {
return nil, err
}
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(context.TODO(), conf)
// Reattach to the old local listener...
// Note: this code probably shouldn't be hit in Cwtch anymore because we purge torrc on restart.
os, err := tp.t.Listen(nil, conf)
if err != nil && strings.Contains(err.Error(), "550 Unspecified Tor error: Onion address collision") {
log.Errorf("550 Unspecified Tor error: Onion address collision - Recovering, but this probably indicates some weird tor configuration issue...")
os = &tor.OnionService{Tor: tp.t, LocalListener: localListener, ID: onion, Version3: true, Key: bineed255192.FromCryptoPrivateKey(privkey), ClientAuths: make(map[string]string), RemotePorts: []int{port}}
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
// Any other errors require an immediate return as os is likely nil...
if err != nil {
return nil, err
}
// We need to set os.ID here, otherwise os.Close() may not shut down the onion service properly...
os.ID = onion
os.CloseLocalListenerOnClose = true
ols := &onionListenService{os: os, tp: tp}
tp.childListeners[ols.AddressFull()] = ols
tp.childListeners[ols.AddressIdentity()] = ols
return ols, nil
}
func (tp *torProvider) Restart() {
log.Debugf("launching restart...")
tp.lock.Lock()
log.Debugf("checking last restart time")
if time.Since(tp.lastRestartTime) < restartCooldown {
tp.lock.Unlock()
return
}
tp.lock.Unlock()
go tp.restart()
}
func (tp *torProvider) restart() {
log.Debugf("Waiting for Tor to Close...")
tp.callStatusCallback(0, "rebooting")
tp.Close()
tp.lock.Lock()
defer tp.lock.Unlock()
// preserve status callback after shutdown
statusCallback := tp.statusCallback
versionCallback := tp.versionCallback
tp.t = nil
log.Debugf("Restarting Tor Process")
newTp, err := startTor(tp.appDirectory, tp.bundeledTorPath, tp.dataDir, tp.controlPort, tp.authenticator)
if err == nil {
// we need to reassign tor, dialer and callback which will have changed by swapping out
// the underlying connection.
tp.t = newTp.t
tp.dialer = newTp.dialer
tp.statusCallback = statusCallback
tp.versionCallback = versionCallback
if tp.versionCallback != nil {
tp.versionCallback(tp.version)
}
tp.lastRestartTime = time.Now()
tp.isClosed = false
go tp.monitorRestart()
} else {
log.Errorf("Error restarting Tor process: %v", err)
}
tp.restart()
}
func (tp *torProvider) Open(hostname string) (net.Conn, string, error) {
@ -370,11 +223,7 @@ func (tp *torProvider) Open(hostname string) (net.Conn, string, error) {
if tp.t == nil {
tp.lock.Unlock()
return nil, hostname, errors.New("tor is offline")
}
if tp.bootProgress != 100 {
tp.lock.Unlock()
return nil, hostname, fmt.Errorf("tor not online, bootstrap progress only %v", tp.bootProgress)
return nil, hostname, errors.New("Tor is offline")
}
tp.lock.Unlock()
@ -383,38 +232,22 @@ func (tp *torProvider) Open(hostname string) (net.Conn, string, error) {
addrParts := strings.Split(hostname, ":")
resolvedHostname = addrParts[1]
}
conn, err := tp.dialer.Dial("tcp", resolvedHostname+".onion:9878")
return conn, resolvedHostname, err
}
func (tp *torProvider) Close() {
closing := false
for _, child := range tp.childListeners {
child.Close()
}
tp.lock.Lock()
defer tp.lock.Unlock()
if !tp.isClosed {
// Break out of any background checks and close
// the underlying tor connection
tp.isClosed = true
tp.breakChan <- true
// wiggle lock to make sure if monitorRestart is waiting for the lock, it gets it, and finished that branch and gets the channel request to exit
tp.lock.Unlock()
tp.lock.Lock()
closing = true
}
// Unregister Child Listeners
for addr, child := range tp.childListeners {
child.Close()
delete(tp.childListeners, addr)
}
log.Debugf("shutting down acn threads..(is already closed: %v)", tp.isClosed)
if closing {
if tp.t != nil {
tp.t.Close()
tp.t = nil
}
tp.breakChan <- true
if tp.t != nil {
tp.t.Close()
tp.t = nil
}
}
@ -424,42 +257,31 @@ func (tp *torProvider) SetStatusCallback(callback func(int, string)) {
tp.statusCallback = callback
}
func (tp *torProvider) SetVersionCallback(callback func(string)) {
tp.lock.Lock()
defer tp.lock.Unlock()
tp.versionCallback = callback
}
func (tp *torProvider) GetStatusCallback() func(int, string) {
tp.lock.Lock()
defer tp.lock.Unlock()
return tp.statusCallback
}
func (tp *torProvider) GetVersionCallback() func(string) {
tp.lock.Lock()
defer tp.lock.Unlock()
return tp.versionCallback
}
func (tp *torProvider) callStatusCallback(prog int, status string) {
tp.lock.Lock()
defer tp.lock.Unlock()
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, dataDir string, controlPort int, authenticator tor.Authenticator) (connectivity.ACN, error) {
tp, err := startTor(appDirectory, bundledTorPath, dataDir, controlPort, authenticator)
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.isClosed = false
go tp.monitorRestart()
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{})
}
// 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) {
@ -470,21 +292,13 @@ func newHideCmd(exePath string) process.Creator {
cmd.Stdout = loggerDebug
cmd.Stderr = loggerError
cmd.SysProcAttr = sysProcAttr
// override tor ld_library_path if requested
torLdLibPath, exists := os.LookupEnv("TOR_LD_LIBRARY_PATH")
if exists {
ldLibPath := fmt.Sprintf("LD_LIBRARY_PATH=%v", torLdLibPath)
cmd.Env = append([]string{ldLibPath}, os.Environ()...)
}
return cmd, nil
})
}
func (tp *torProvider) checkVersion() (string, error) {
func (tp *torProvider) checkVersion() error {
// attempt connect to system tor
log.Debugf("dialing tor control port")
log.Debugf("dialing system tor control port")
controlport, err := dialControlPort(tp.controlPort)
if err == nil {
defer controlport.Close()
@ -495,32 +309,35 @@ func (tp *torProvider) checkVersion() (string, error) {
if err == nil {
if minTorVersionReqs(pinfo.TorVersion) {
log.Debugln("OK version " + pinfo.TorVersion)
return pinfo.TorVersion, nil
return nil
}
return pinfo.TorVersion, fmt.Errorf("tor version not supported: %v", pinfo.TorVersion)
return fmt.Errorf("Tor version not supported: %v", pinfo.TorVersion)
}
}
}
return "", err
return err
}
func startTor(appDirectory string, bundledTorPath string, dataDir string, controlPort int, authenticator tor.Authenticator) (*torProvider, error) {
func startTor(appDirectory string, bundledTorPath string, controlPort int, authenticator tor.Authenticator) (*torProvider, 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 := &torProvider{authenticator: authenticator, controlPort: controlPort, appDirectory: appDirectory, bundeledTorPath: bundledTorPath, childListeners: make(map[string]*onionListenService), breakChan: make(chan bool, 1), statusCallback: nil, versionCallback: nil, lastRestartTime: time.Now().Add(-restartCooldown), bootProgress: -1}
tp := &torProvider{authenticator: authenticator, controlPort: controlPort, appDirectory: appDirectory, bundeledTorPath: bundledTorPath, childListeners: make(map[string]*onionListenService), breakChan: make(chan bool), statusCallback: nil, lastRestartTime: time.Now().Add(-restartCooldown)}
log.Debugf("checking if there is a running system tor")
if version, err := tp.checkVersion(); err == nil {
tp.version = version
log.Debugf("launching system tor")
if err := tp.checkVersion(); err == nil {
controlport, err := dialControlPort(tp.controlPort)
if err == nil {
log.Debugf("creating tor handler from system tor")
log.Debugf("creating tor handler fom system tor")
tp.t = createFromExisting(controlport, dataDir)
tp.dialer, err = tp.t.Dialer(context.TODO(), &tor.DialConf{Authenticator: tp.authenticator})
return tp, err
}
return tp, nil
}
// check if the torrc file is present where expected
@ -530,39 +347,30 @@ func startTor(appDirectory string, bundledTorPath string, dataDir string, contro
return nil, err
}
// if not, try bundled tor, then running system tor
log.Debugln("checking if we can run bundled tor or system installed tor")
if version, pass := checkCmdlineTorVersion(bundledTorPath); pass {
log.Debugln("bundled tor appears viable, attempting to use '" + bundledTorPath + "'")
t, err := tor.Start(context.TODO(), &tor.StartConf{ControlPort: tp.controlPort, NoAutoSocksPort: true, 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
tp.version = version
} else if version, pass := checkCmdlineTorVersion("tor"); pass {
t, err := tor.Start(context.TODO(), &tor.StartConf{ControlPort: tp.controlPort, NoAutoSocksPort: true, DisableCookieAuth: true, UseEmbeddedControlConn: false, DisableEagerAuth: true, EnableNetwork: true, DataDir: dataDir, TorrcFile: path.Join(torDir, "torrc"), DebugWriter: nil, ProcessCreator: newHideCmd("tor")})
// 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
tp.version = version
} 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)")
} 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
}
version, err := tp.checkVersion()
err = tp.checkVersion()
if err == nil {
tp.t.DeleteDataDirOnClose = false // caller is responsible for dealing with cached information...
tp.dialer, err = tp.t.Dialer(context.TODO(), &tor.DialConf{Authenticator: tp.authenticator})
tp.version = version
tp.t.Control.TakeOwnership()
return tp, err
tp.t.DeleteDataDirOnClose = true
return tp, nil
}
return nil, fmt.Errorf("could not connect to running tor: %v", err)
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 *torProvider) GetPID() (int, error) {
@ -573,6 +381,12 @@ 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
@ -587,8 +401,8 @@ func (tp *torProvider) monitorRestart() {
prog, status := tp.GetBootstrapStatus()
if prog == torDown && tp.t != nil {
log.Warnf("monitorRestart calling tp.restart() with prog:%v\n", prog)
tp.restart()
return
}
if prog != lastBootstrapProgress {
@ -603,13 +417,30 @@ func (tp *torProvider) monitorRestart() {
}
}
func getCircuitInfo(controlport *control.Conn) ([]*control.KeyVal, []*control.KeyVal, error) {
circuits, cerr := controlport.GetInfo("circuit-status")
streams, serr := controlport.GetInfo("stream-status")
if cerr == nil && serr == nil {
return circuits, streams, nil
func (tp *torProvider) 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
}
}
return nil, nil, errors.New("could not fetch circuits or streams")
}
func createFromExisting(controlport *control.Conn, datadir string) *tor.Tor {
@ -618,7 +449,7 @@ func createFromExisting(controlport *control.Conn, datadir string) *tor.Tor {
Control: controlport,
ProcessCancelFunc: nil,
DataDir: datadir,
DeleteDataDirOnClose: false,
DeleteDataDirOnClose: true,
DebugWriter: nil,
StopProcessOnClose: false,
GeoIPCreatedFile: "",
@ -629,57 +460,25 @@ func createFromExisting(controlport *control.Conn, datadir string) *tor.Tor {
return t
}
func checkCmdlineTorVersion(torCmd string) (string, bool) {
if torCmd == "" {
return "", false
}
// ideally we would use CommandContext with Timeout here
// but it doesn't work with programs that may launch other processes via scripts e.g. exec
// and the workout is more complex than just implementing the logic ourselves...
func checkCmdlineTorVersion(torCmd string) bool {
cmd := exec.Command(torCmd, "--version")
var outb bytes.Buffer
cmd.Stdout = &outb
waiting := make(chan error, 1)
// try running the tor process
go func() {
log.Debugf("running tor process: %v", torCmd)
cmd.Run()
waiting <- nil
}()
// timeout function
go func() {
<-time.After(time.Second * 5)
waiting <- errors.New("timeout")
}()
err := <-waiting
if err != nil {
log.Debugf("tor process timed out")
return "", false
}
re := regexp.MustCompile(`[0-1]+\.[0-9]+\.[0-9]+\.[0-9]+`)
sysTorVersion := re.Find(outb.Bytes())
log.Infof("tor version: %v", string(sysTorVersion))
return string(sysTorVersion), minTorVersionReqs(string(sysTorVersion))
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
if len(torversions) >= 3 {
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)))
}
return false
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) {

View File

@ -3,12 +3,8 @@ package tor
import (
"fmt"
"git.openprivacy.ca/openprivacy/log"
"os"
path "path/filepath"
"runtime"
"runtime/pprof"
"path"
"testing"
"time"
)
func getStatusCallback(progChan chan int) func(int, string) {
@ -18,39 +14,17 @@ func getStatusCallback(progChan chan int) func(int, string) {
}
}
func getVersionCallback(verChan chan string) func(string) {
return func(version string) {
fmt.Printf("version: %v\n", version)
verChan <- version
}
}
func TestTorProvider(t *testing.T) {
goRoutineStart := runtime.NumGoroutine()
progChan := make(chan int, 10)
verChan := make(chan string, 10)
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)
dataDir := ""
var err error
if dataDir, err = os.MkdirTemp(path.Join("..", "testing"), "data-dir-"); err != nil {
t.Fatalf("could not create data dir")
}
acn, err := NewTorACNWithAuth(path.Join("../testing/"), torpath, dataDir, 9051, HashedPasswordAuthenticator{"examplehashedpassword"})
acn, err := NewTorACNWithAuth(path.Join("../testing/"), torpath, 9051, HashedPasswordAuthenticator{"examplehashedpassword"})
if err != nil {
t.Error(err)
return
}
acn.SetStatusCallback(getStatusCallback(progChan))
acn.SetVersionCallback(getVersionCallback(verChan))
progress := 0
for progress < 100 {
@ -58,58 +32,5 @@ func TestTorProvider(t *testing.T) {
t.Logf("progress: %v", progress)
}
acn.Restart()
progress = 0
for progress < 100 {
progress = <-progChan
t.Logf("progress: %v", progress)
}
log.Debugf("Pulling tor version from version callback chan...\n")
version := <-verChan
if version == "" {
t.Errorf("failed to get tor version, got empty string\n")
} else {
log.Debugf("Tor version: %v\n", version)
}
// Test opening the OP Server
_, _, err = acn.Open("isbr2t6bflul2zyi6hjtnuezb2xvfr42svzjg2q3gyqfgg3wmnrbkkqd")
if err == nil {
info, err := acn.GetInfo("isbr2t6bflul2zyi6hjtnuezb2xvfr42svzjg2q3gyqfgg3wmnrbkkqd")
if err != nil {
t.Fatalf("could not find info for OP server %v", err)
}
cinfo, exists := info["circuit"]
if !exists || len(cinfo) == 0 {
t.Fatalf("could not find circuit info for OP server %v", err)
}
_, err = acn.GetInfo("not_a_real_onion")
if err == nil {
t.Fatalf("GetInfo for non existent onion should have errored")
}
} else {
t.Fatalf("could not connect to OP server %v", err)
}
// Should skip without blocking...
acn.Restart()
acn.Restart()
acn.Restart()
acn.Close()
time.Sleep(time.Second * 10)
goRoutineEnd := runtime.NumGoroutine()
if goRoutineEnd != goRoutineStart {
pprof.Lookup("goroutine").WriteTo(os.Stdout, 1)
t.Fatalf("goroutine leak in ACN: %v %v", goRoutineStart, goRoutineEnd)
}
}

View File

@ -3,7 +3,6 @@ package tor
import (
"encoding/base32"
"errors"
"filippo.io/edwards25519"
"git.openprivacy.ca/openprivacy/bine/control"
"git.openprivacy.ca/openprivacy/bine/tor"
"golang.org/x/crypto/ed25519"
@ -46,33 +45,6 @@ func IsValidHostname(address string) bool {
data, err := base32.StdEncoding.DecodeString(strings.ToUpper(address))
if err == nil {
pubkey := data[0:ed25519.PublicKeySize]
// Tor won't allow us to connect to a hostname containing a torsion component
// However because we permit authentication over inbound connections we would like to
// be extra safe and reject all *invalid* hostnames that contain a torsion component...
// to do this we need to multiply the point by the order of the group and check that the
// result is the ed25519 identity element.
// l = order of the group (minus 1)
lBytes := []byte{236, 211, 245, 92, 26, 99, 18, 88, 214, 156, 247, 162, 222, 249, 222, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16}
l, _ := edwards25519.NewScalar().SetCanonicalBytes(lBytes)
// construct a curve point from the public key
// if this fails then the hostname is invalid
p, err := new(edwards25519.Point).SetBytes(pubkey)
if err != nil {
return false
}
// Calculate l*P (actually (l-1)P + P because of the limitations of the scalar library...
result := new(edwards25519.Point).ScalarMult(l, p)
result = new(edwards25519.Point).Add(result, p)
// The result should be the identity point..assuming the hostname contains no torsion components...
if result.Equal(edwards25519.NewIdentityPoint()) == 0 {
return false
}
// Finally check that we arrive at the same hostname as the one we were given...
if GetTorV3Hostname(ed25519.PublicKey(pubkey)) == address {
return true
}

View File

@ -1,9 +1,6 @@
package tor
import (
"encoding/hex"
"filippo.io/edwards25519"
"git.openprivacy.ca/openprivacy/bine/torutil"
"os"
"testing"
)
@ -16,79 +13,6 @@ func TestGenerateHashedPassword(t *testing.T) {
}
}
func TestIsValidHostname(t *testing.T) {
openprivonion := "openpravyvc6spbd4flzn4g2iqu4sxzsizbtb5aqec25t76dnoo5w7yd"
t.Logf("testing: %v", openprivonion)
if IsValidHostname(openprivonion) == false {
t.Fatalf("open privacy onion should validate as a valid hostname")
}
sarahonion := "icyt7rvdsdci42h6si2ibtwucdmjrlcb2ezkecuagtquiiflbkxf2cqd"
t.Logf("testing: %v", sarahonion)
if IsValidHostname(sarahonion) == false {
t.Fatalf("sarah onion should validate as a valid hostname")
}
// First we will construct a torsion point from our Valid Onion
pubKey, _ := torutil.PublicKeyFromV3OnionServiceID(openprivonion)
pubKeyPoint, _ := new(edwards25519.Point).SetBytes(pubKey)
torsionPubKeyBytes, _ := hex.DecodeString("26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc05")
torsionHostname, _ := torutil.PublicKeyFromV3OnionServiceID(GetTorV3Hostname(torsionPubKeyBytes))
torsionPoint, _ := new(edwards25519.Point).SetBytes(torsionHostname)
malformedKey := new(edwards25519.Point).Add(pubKeyPoint, torsionPoint)
t.Logf("testing: %v", GetTorV3Hostname(malformedKey.Bytes()))
if IsValidHostname(GetTorV3Hostname(malformedKey.Bytes())) == true {
t.Fatalf("torsion onion should not validate as a valid hostname")
}
// Testing a few torsion points taken from https://lists.torproject.org/pipermail/tor-dev/2017-April/012226.html
torsionPubKey, _ := hex.DecodeString("0000000000000000000000000000000000000000000000000000000000000000")
t.Logf("testing: %v", GetTorV3Hostname(torsionPubKey))
if IsValidHostname(GetTorV3Hostname(torsionPubKey)) == true {
t.Fatalf("torsion onion should not validate as a valid hostname")
}
torsionPubKey, _ = hex.DecodeString("26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc05")
t.Logf("testing: %v", GetTorV3Hostname(torsionPubKey))
if IsValidHostname(GetTorV3Hostname(torsionPubKey)) == true {
t.Fatalf("torsion onion should not validate as a valid hostname")
}
torsionPubKey, _ = hex.DecodeString("c9fff3af0471c28e33e98c2043e44f779d0427b1e37c521a6bddc011ed1869af")
t.Logf("testing: %v", GetTorV3Hostname(torsionPubKey))
if IsValidHostname(GetTorV3Hostname(torsionPubKey)) == true {
t.Fatalf("torsion onion should not validate as a valid hostname")
}
torsionPubKey, _ = hex.DecodeString("f43e3a046db8749164c6e69b193f1e942c7452e7d888736f40b98093d814d5e7")
t.Logf("testing: %v", GetTorV3Hostname(torsionPubKey))
if IsValidHostname(GetTorV3Hostname(torsionPubKey)) == true {
t.Fatalf("torsion onion should not should validate as a valid hostname")
}
torsionPubKey, _ = hex.DecodeString("300ef2e64e588e1df55b48e4da0416ffb64cc85d5b00af6463d5cc6c2b1c185e")
t.Logf("testing: %v", GetTorV3Hostname(torsionPubKey))
if IsValidHostname(GetTorV3Hostname(torsionPubKey)) == true {
t.Fatalf("torsion onion should not validate as a valid hostname")
}
// this should pass
// (also from https://lists.torproject.org/pipermail/tor-dev/2017-April/012230.html)
validPubKey, _ := hex.DecodeString("4ba2e44760dff4c559ef3c38768c1c14a8a54740c782c8d70803e9d6e3ad8794")
t.Logf("testing: %v", GetTorV3Hostname(validPubKey))
if IsValidHostname(GetTorV3Hostname(validPubKey)) == false {
t.Fatalf("valid onion should validate as a valid hostname")
}
// Finally test a completely invalid key...
badPubKey, _ := hex.DecodeString("e19c65de75c68cf3b7643ea732ba9eb1a3d20d6d57ba223c2ece1df66feb5af0")
t.Logf("testing: %v", GetTorV3Hostname(badPubKey))
if IsValidHostname(GetTorV3Hostname(badPubKey)) == true {
t.Fatalf("invalid ed25519 point should not validate as a valid hostname")
}
}
func TestGenerateTorrc(t *testing.T) {
path := "./torrc.test"
password := "examplehashedpassword"
@ -98,11 +22,3 @@ 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

@ -6,18 +6,10 @@ import (
"encoding/hex"
"fmt"
"io"
"os"
"io/ioutil"
"strings"
)
type TorLogLevel string
const TorLogLevelDebug TorLogLevel = "debug"
const TorLogLevelNotice TorLogLevel = "notice"
const TorLogLevelInfo TorLogLevel = "info"
const TorLogLevelWarn TorLogLevel = "warn"
const TorLogLevelErr TorLogLevel = "err"
// TorrcBuilder is a a helper for building torrc files
type TorrcBuilder struct {
lines []string
@ -42,32 +34,6 @@ func (tb *TorrcBuilder) WithControlPort(port int) *TorrcBuilder {
return tb
}
// WithLog sets the Log to file directive to the specified file with the specified log level
func (tb *TorrcBuilder) WithLog(logfile string, level TorLogLevel) *TorrcBuilder {
tb.lines = append(tb.lines, fmt.Sprintf("Log %v file %v", level, logfile))
return tb
}
// WithSocksTimeout adjusts how long before a timeout error is generated trying to connect to the SOCKS port
func (tb *TorrcBuilder) WithSocksTimeout(timeOutSecs int) *TorrcBuilder {
tb.lines = append(tb.lines, fmt.Sprintf("SocksTimeout %v", timeOutSecs))
return tb
}
// WithCustom appends to 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 = append(tb.lines, lines...)
return tb
}
// UseCustom clobbers the torrc builder and allows the client to set any option they want, while benefiting
// from other configuration options.
func (tb *TorrcBuilder) UseCustom(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 {
@ -79,12 +45,6 @@ func (tb *TorrcBuilder) WithOnionTrafficOnly() *TorrcBuilder {
return tb
}
// WithOwningPid adds a __OwningControllerProcess line to the config that will attempt to have tor monitor parent PID health and die when parent dies
func (tb *TorrcBuilder) WithOwningPid(pid int) *TorrcBuilder {
tb.lines = append(tb.lines, fmt.Sprintf("__OwningControllerProcess %v", pid))
return tb
}
// WithHashedPassword sets a password for the control port.
func (tb *TorrcBuilder) WithHashedPassword(password string) *TorrcBuilder {
var salt [8]byte
@ -98,12 +58,7 @@ func (tb *TorrcBuilder) WithHashedPassword(password string) *TorrcBuilder {
// Build finalizes the torrc contents and write a file
func (tb *TorrcBuilder) Build(path string) error {
return os.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")
return ioutil.WriteFile(path, []byte(strings.Join(tb.lines, "\n")), 0600)
}
// GenerateHashedPassword calculates a hash in the same way tha tor --hash-password does