Compare commits
117 Commits
Author | SHA1 | Date |
---|---|---|
Sarah Jamie Lewis | 1524e78a4a | |
Sarah Jamie Lewis | cd87779e87 | |
Sarah Jamie Lewis | d8dd82d065 | |
Sarah Jamie Lewis | 932f99fac8 | |
Sarah Jamie Lewis | bbacb5539d | |
Sarah Jamie Lewis | 2c9ec9d894 | |
Sarah Jamie Lewis | c9ea1e4464 | |
Sarah Jamie Lewis | 61ced82cb4 | |
Sarah Jamie Lewis | 91c41e2005 | |
Sarah Jamie Lewis | caca121441 | |
Sarah Jamie Lewis | 9beff8a10a | |
Sarah Jamie Lewis | dedcbdd3cb | |
Sarah Jamie Lewis | c18cd719a1 | |
Sarah Jamie Lewis | 380fd1834a | |
Sarah Jamie Lewis | 1162cf4168 | |
Sarah Jamie Lewis | 4a2eeed072 | |
Dan Ballard | 2f3860eb89 | |
Dan Ballard | bbe4198a41 | |
Sarah Jamie Lewis | 51029af959 | |
Dan Ballard | 1f52dc7138 | |
Sarah Jamie Lewis | 72f689de26 | |
Dan Ballard | 69085fc721 | |
Sarah Jamie Lewis | 98b15bd105 | |
Dan Ballard | 789de52589 | |
Dan Ballard | 1741e63424 | |
Sarah Jamie Lewis | 478df967fc | |
Sarah Jamie Lewis | 8e5dc44400 | |
Dan Ballard | 6122ad437d | |
Dan Ballard | 882849b66a | |
Dan Ballard | 9a134810c3 | |
Dan Ballard | 9992edadba | |
Dan Ballard | 9d4e1e5ca5 | |
Dan Ballard | b280f7a1fe | |
Sarah Jamie Lewis | cc924a476e | |
Sarah Jamie Lewis | 8d0ed17e7f | |
Dan Ballard | a30e16354c | |
Sarah Jamie Lewis | 586cc261a3 | |
Dan Ballard | 3d36e7d4a5 | |
Sarah Jamie Lewis | 8ad13a6b36 | |
Sarah Jamie Lewis | 1a402de50d | |
Sarah Jamie Lewis | 6d5d067d70 | |
erinn | 04dec3238b | |
Sarah Jamie Lewis | dfae5b9261 | |
Sarah Jamie Lewis | 4353143ae4 | |
Sarah Jamie Lewis | 0c745e7691 | |
Sarah Jamie Lewis | f82cf26731 | |
erinn | 85da1dac37 | |
Sarah Jamie Lewis | 023d1a6e5d | |
Sarah Jamie Lewis | 35247bd044 | |
Sarah Jamie Lewis | 384d59e9ef | |
Sarah Jamie Lewis | 5d4cf85d26 | |
Sarah Jamie Lewis | 13045e3d98 | |
Sarah Jamie Lewis | 8a56f02fc5 | |
Sarah Jamie Lewis | 9169018529 | |
Sarah Jamie Lewis | 415ca32a32 | |
Sarah Jamie Lewis | dbc3d675ec | |
Sarah Jamie Lewis | d3398bd074 | |
erinn | a681dd6d57 | |
Sarah Jamie Lewis | b36f6dc33f | |
Sarah Jamie Lewis | 8fe2974aaa | |
Sarah Jamie Lewis | 6d5accb338 | |
Sarah Jamie Lewis | 7a2c5f8fed | |
Dan Ballard | 9ab706cf02 | |
Dan Ballard | a0770a3f1b | |
Sarah Jamie Lewis | c11e326785 | |
Dan Ballard | 1e06db5145 | |
Sarah Jamie Lewis | 4e4b39b707 | |
erinn | 15060335f0 | |
erinn | 2f5de5d1ca | |
Sarah Jamie Lewis | 4c0148619e | |
erinn | 9bcea4aa0a | |
erinn | e8fca87087 | |
Sarah Jamie Lewis | 38eb603dec | |
Dan Ballard | 9947d289c7 | |
Sarah Jamie Lewis | a40c4b9e1d | |
Dan Ballard | fd0cf2f7a2 | |
Sarah Jamie Lewis | d31f77262b | |
Dan Ballard | e72f8c9846 | |
Dan Ballard | eb8576304b | |
Dan Ballard | b8183662dc | |
Dan Ballard | 1382c5bbd3 | |
Dan Ballard | 98343fd4b4 | |
Dan Ballard | f511744171 | |
Sarah Jamie Lewis | fa03a91425 | |
Dan Ballard | 540183a881 | |
Sarah Jamie Lewis | f13387fcae | |
Dan Ballard | 89dff1f5e0 | |
Dan Ballard | d8d823d9d5 | |
Sarah Jamie Lewis | 52048e1686 | |
Dan Ballard | 34171843fa | |
Sarah Jamie Lewis | 0b0f65017a | |
Dan Ballard | 604237ea34 | |
Sarah Jamie Lewis | ba56a6cb48 | |
Sarah Jamie Lewis | d290b705eb | |
Dan Ballard | f01a76f337 | |
Dan Ballard | 6f2a158504 | |
Sarah Jamie Lewis | 3888ece4d3 | |
Sarah Jamie Lewis | a8d31e2adb | |
Dan Ballard | 908fa93368 | |
Sarah Jamie Lewis | afda040693 | |
Sarah Jamie Lewis | 94ce702214 | |
Sarah Jamie Lewis | dcba0c1a1b | |
Sarah Jamie Lewis | bc53460574 | |
Sarah Jamie Lewis | 5c101d7289 | |
Sarah Jamie Lewis | 77602b9973 | |
Sarah Jamie Lewis | ad2ddbb029 | |
Sarah Jamie Lewis | e16fbe1c76 | |
Sarah Jamie Lewis | 513c51376b | |
Sarah Jamie Lewis | b076b8bad6 | |
Dan Ballard | 181478adfb | |
Sarah Jamie Lewis | 42033f9819 | |
Dan Ballard | 4eedeaa4db | |
Sarah Jamie Lewis | f903fd4c8f | |
Dan Ballard | 35293ea087 | |
Dan Ballard | beb2665c49 | |
Dan Ballard | e276c4fbe7 | |
Dan Ballard | 504139e4c6 |
|
@ -0,0 +1,76 @@
|
|||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: linux-test
|
||||
|
||||
steps:
|
||||
- name: fetch
|
||||
image: golang:1.19.1
|
||||
volumes:
|
||||
- name: deps
|
||||
path: /go
|
||||
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
|
||||
commands:
|
||||
- staticcheck ./...
|
||||
- name: units-tests
|
||||
image: golang:1.19.1
|
||||
volumes:
|
||||
- name: deps
|
||||
path: /go
|
||||
commands:
|
||||
- export PATH=`pwd`:$PATH
|
||||
- ./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
|
||||
commands:
|
||||
- export PATH=`pwd`:$PATH
|
||||
- go test -race -v ./testing/launch_tor_integration_test.go
|
||||
- name: notify-email
|
||||
image: drillster/drone-email
|
||||
pull: if-not-exists
|
||||
host: build.openprivacy.ca
|
||||
port: 25
|
||||
skip_verify: true
|
||||
from: drone@openprivacy.ca
|
||||
when:
|
||||
status: [ failure ]
|
||||
- name: notify-gogs
|
||||
image: openpriv/drone-gogs
|
||||
pull: if-not-exists
|
||||
when:
|
||||
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
|
|
@ -1,2 +1,9 @@
|
|||
tor/
|
||||
.idea/
|
||||
coverage.*
|
||||
tor/tor/
|
||||
vendor/
|
||||
*.cover.out
|
||||
tmp/
|
||||
testing/tor/*
|
||||
tor/data-dir*
|
||||
testing/data-dir*
|
58
README.md
58
README.md
|
@ -1,3 +1,59 @@
|
|||
# connectivity
|
||||
|
||||
A library providing an ACN (Anonymous Communication Network) abstraction and a tor (for now) implementation.
|
||||
A library providing an ACN (Anonymous Communication Network
|
||||
) networking abstraction
|
||||
|
||||
## Supported ACNs
|
||||
|
||||
* 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
|
||||
* Maintain an endpoint via a PublicKey (the underlying crypto is the
|
||||
responsibility of the implementation)
|
||||
|
||||
## Using
|
||||
|
||||
Each ACN implementation provides a specific start function that takes in the
|
||||
required parameters to e.g. find a specific binary on the system, attempt to
|
||||
talk to a specific system service or launch an in-memory networking manager:
|
||||
|
||||
acn, err := NewTorACN(".", "", 9051, HashedPasswordAuthenticator{"examplehasedpassword"})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
At this point the ACN is responsible for setting up the networking interface,
|
||||
the result of which can be checked via the Status callback:
|
||||
|
||||
|
||||
acn.SetStatusCallback(getStatusCallback(progChan))
|
||||
|
||||
progress := 0
|
||||
for progress < 100 {
|
||||
progress = <-progChan
|
||||
}
|
||||
|
||||
Once initialized the ACN can be used to open new connections:
|
||||
|
||||
conn,err := acn.Open(hostname);
|
||||
|
||||
Or host a service on the ACN:
|
||||
|
||||
ls,err := acn.Listen(identity, port) ;
|
||||
|
||||
We also provide closing and restart functionality for managing the networking
|
||||
service:
|
||||
|
||||
acn.Restart()
|
||||
and
|
||||
|
||||
acn.Close()
|
||||
|
|
40
acn.go
40
acn.go
|
@ -4,14 +4,23 @@ import (
|
|||
"net"
|
||||
)
|
||||
|
||||
// Error captures various common ricochet errors
|
||||
type Error string
|
||||
|
||||
func (e Error) Error() string { return string(e) }
|
||||
|
||||
const (
|
||||
// CannotResolveLocalTCPAddressError is thrown when a local ricochet connection has the wrong format.
|
||||
CannotResolveLocalTCPAddressError = Error("CannotResolveLocalTCPAddressError")
|
||||
// CannotDialLocalTCPAddressError is thrown when a connection to a local ricochet address fails.
|
||||
CannotDialLocalTCPAddressError = Error("CannotDialLocalTCPAddressError")
|
||||
)
|
||||
|
||||
// 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
|
||||
|
||||
|
@ -22,25 +31,38 @@ type ListenService interface {
|
|||
// 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
|
||||
// On Network down it returns -1
|
||||
// On ACN error state it returns -2
|
||||
GetBootstrapStatus() (int, string)
|
||||
// WaitTillBootstrapped Blocks until underlying network is bootstrapped
|
||||
WaitTillBootstrapped()
|
||||
// Sets the calback function to be called when ACN status changes
|
||||
WaitTillBootstrapped() error
|
||||
// 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()
|
||||
|
||||
// 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)
|
||||
|
||||
// Get PID
|
||||
GetPID() (int, error)
|
||||
|
||||
// GetVersion returns a string of what the ACN returns when asked for a version
|
||||
GetVersion() string
|
||||
|
||||
GetInfo(onion string) (map[string]string, error)
|
||||
|
||||
Close()
|
||||
}
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
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...
|
||||
}
|
15
go.mod
15
go.mod
|
@ -1,10 +1,15 @@
|
|||
module git.openprivacy.ca/openprivacy/connectivity
|
||||
|
||||
go 1.13
|
||||
go 1.17
|
||||
|
||||
require (
|
||||
git.openprivacy.ca/openprivacy/log v0.0.0-20200206193118-3093e3fae448
|
||||
github.com/cretz/bine v0.1.1-0.20200124154328-f9f678b84cca
|
||||
github.com/stretchr/testify v1.3.0 // indirect
|
||||
golang.org/x/crypto v0.0.0-20200204104054-c9f3fb736b72
|
||||
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
|
||||
)
|
||||
|
|
37
go.sum
37
go.sum
|
@ -1,20 +1,37 @@
|
|||
git.openprivacy.ca/openprivacy/log v0.0.0-20200206193118-3093e3fae448 h1:Tzy38uHvRCT/v6fiG/456myCjRk6RASH6XIX9yXhcpQ=
|
||||
git.openprivacy.ca/openprivacy/log v0.0.0-20200206193118-3093e3fae448/go.mod h1:gGYK8xHtndRLDymFtmjkG26GaMQNgyhioNS82m812Iw=
|
||||
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=
|
||||
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=
|
||||
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=
|
||||
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 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
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-20200204104054-c9f3fb736b72 h1:+ELyKg6m8UBf0nPFSqD0mi7zUfwPyXo23HNjMnXPz7w=
|
||||
golang.org/x/crypto v0.0.0-20200204104054-c9f3fb736b72/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
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/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/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/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=
|
||||
|
|
|
@ -13,6 +13,19 @@ type localListenService struct {
|
|||
type localProvider struct {
|
||||
}
|
||||
|
||||
// NewLocalACN returns a for testing use only local clearnet implementation of a ACN interface
|
||||
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()
|
||||
}
|
||||
|
@ -29,6 +42,10 @@ 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"
|
||||
|
@ -38,8 +55,21 @@ 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
|
||||
}
|
||||
|
||||
func (lp *localProvider) GetVersion() string {
|
||||
return "0.1"
|
||||
}
|
||||
|
||||
// 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) {
|
||||
|
@ -70,8 +100,3 @@ func (lp *localProvider) Restart() {
|
|||
func (lp *localProvider) Close() {
|
||||
|
||||
}
|
||||
|
||||
// LocalProvider returns a for testing use only local clearnet implementation of a ACN interface
|
||||
func LocalProvider() ACN {
|
||||
return &localProvider{}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
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()
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
package testing
|
||||
|
||||
import (
|
||||
"git.openprivacy.ca/openprivacy/connectivity/tor"
|
||||
"git.openprivacy.ca/openprivacy/log"
|
||||
"math/rand"
|
||||
"os"
|
||||
path "path/filepath"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestLaunchTor(t *testing.T) {
|
||||
goRoutineStart := runtime.NumGoroutine()
|
||||
log.SetLevel(log.LevelDebug)
|
||||
|
||||
rand.Seed(int64(time.Now().Nanosecond()))
|
||||
controlPort := rand.Intn(1000) + 9052
|
||||
password := "examplehashedpassword"
|
||||
|
||||
// Create the tor data directory if it doesn't already exist..
|
||||
os.MkdirAll("../tmp/data/tor", 0700)
|
||||
err := tor.NewTorrc().WithControlPort(controlPort).WithHashedPassword(password).Build("../tmp/data/tor/torrc")
|
||||
|
||||
if err != nil {
|
||||
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})
|
||||
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)
|
||||
}
|
||||
|
||||
if pid, err := acn.GetPID(); err == nil {
|
||||
t.Logf("tor pid: %v", pid)
|
||||
} else {
|
||||
t.Fatalf("error fetching pid: %v", err)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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"
|
|
@ -0,0 +1,10 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
pwd
|
||||
GORACE="haltonerror=1"
|
||||
go test -race ${1} -coverprofile=tor.cover.out -v ./tor
|
||||
echo "mode: set" > coverage.out && cat *.cover.out | grep -v mode: | sort -r | \
|
||||
awk '{if($1 != last) {print $0;last=$1}}' >> coverage.out
|
||||
rm -rf *.cover.out
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
SOCKSPort 9050
|
||||
ControlPort 9051
|
||||
# "examplehashedpassword" - used for testing
|
||||
HashedControlPassword 16:C15305F97789414B601259E3EC5E76B8E55FC56A9F562B713F3D2BA257
|
||||
# Needed for integ tests
|
||||
RunAsDaemon 1
|
|
@ -1,6 +1,7 @@
|
|||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package connectivity
|
||||
package tor
|
||||
|
||||
import (
|
||||
"syscall"
|
|
@ -1,6 +1,7 @@
|
|||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package connectivity
|
||||
package tor
|
||||
|
||||
import (
|
||||
"syscall"
|
|
@ -0,0 +1,691 @@
|
|||
package tor
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.openprivacy.ca/openprivacy/bine/control"
|
||||
"git.openprivacy.ca/openprivacy/bine/process"
|
||||
"git.openprivacy.ca/openprivacy/bine/tor"
|
||||
bineed255192 "git.openprivacy.ca/openprivacy/bine/torutil/ed25519"
|
||||
"git.openprivacy.ca/openprivacy/connectivity"
|
||||
"git.openprivacy.ca/openprivacy/log"
|
||||
"golang.org/x/crypto/ed25519"
|
||||
"golang.org/x/crypto/sha3"
|
||||
"net"
|
||||
"net/textproto"
|
||||
"os"
|
||||
"os/exec"
|
||||
path "path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
minStatusIntervalMs = 200
|
||||
maxStatusIntervalMs = 2000
|
||||
restartCooldown = time.Second * 30
|
||||
)
|
||||
|
||||
const (
|
||||
networkUnknown = -3
|
||||
torDown = -2
|
||||
networkDown = -1
|
||||
networkUp = 0
|
||||
)
|
||||
|
||||
// NoTorrcError is a typed error thrown to indicate start could not complete due to lack of a torrc file
|
||||
type NoTorrcError struct {
|
||||
path string
|
||||
}
|
||||
|
||||
func (e *NoTorrcError) Error() string { return fmt.Sprintf("torrc file does not exist at %v", e.path) }
|
||||
|
||||
type logWriter struct {
|
||||
level log.Level
|
||||
}
|
||||
|
||||
func (l *logWriter) Write(p []byte) (int, error) {
|
||||
log.Printf(l.level, "tor: %v", string(p))
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
type onionListenService struct {
|
||||
lock sync.Mutex
|
||||
os *tor.OnionService
|
||||
tp *torProvider
|
||||
}
|
||||
|
||||
type torProvider struct {
|
||||
controlPort int
|
||||
t *tor.Tor
|
||||
dialer *tor.Dialer
|
||||
appDirectory string
|
||||
bundeledTorPath string
|
||||
lock sync.Mutex
|
||||
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) Accept() (net.Conn, error) {
|
||||
return ols.os.Accept()
|
||||
}
|
||||
|
||||
func (ols *onionListenService) Close() {
|
||||
ols.lock.Lock()
|
||||
defer ols.lock.Unlock()
|
||||
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
|
||||
func (tp *torProvider) GetBootstrapStatus() (int, string) {
|
||||
tp.lock.Lock()
|
||||
defer tp.lock.Unlock()
|
||||
|
||||
if tp.t == nil {
|
||||
return torDown, "error: no tor, trying to restart..."
|
||||
}
|
||||
|
||||
val, err := tp.t.Control.GetInfo("network-liveness")
|
||||
if err != nil {
|
||||
return torDown, "can't query tor network-liveness"
|
||||
}
|
||||
if val[0].Val == "down" {
|
||||
return networkDown, "tor cannot detect underlying network"
|
||||
}
|
||||
// else network is up
|
||||
|
||||
kvs, err := tp.t.Control.GetInfo("status/bootstrap-phase")
|
||||
if err != nil {
|
||||
return torDown, "error querrying status/bootstrap-phase"
|
||||
}
|
||||
progress := 0
|
||||
status := ""
|
||||
|
||||
if len(kvs) > 0 {
|
||||
|
||||
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]
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// WaitTillBootstrapped Blocks until underlying network is bootstrapped
|
||||
func (tp *torProvider) WaitTillBootstrapped() error {
|
||||
for !tp.closed() {
|
||||
progress, _ := tp.GetBootstrapStatus()
|
||||
if progress == 100 {
|
||||
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) {
|
||||
tp.lock.Lock()
|
||||
defer tp.lock.Unlock()
|
||||
|
||||
if tp.t == nil {
|
||||
return nil, errors.New("tor provider closed")
|
||||
}
|
||||
|
||||
var onion string
|
||||
var privkey ed25519.PrivateKey
|
||||
|
||||
switch pk := identity.(type) {
|
||||
case ed25519.PrivateKey:
|
||||
privkey = pk
|
||||
gpubk := pk.Public()
|
||||
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
|
||||
// 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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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.
|
||||
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}}
|
||||
err = nil
|
||||
}
|
||||
|
||||
// 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
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
if tp.bootProgress != 100 {
|
||||
tp.lock.Unlock()
|
||||
return nil, hostname, fmt.Errorf("tor not online, bootstrap progress only %v", tp.bootProgress)
|
||||
}
|
||||
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() {
|
||||
closing := false
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (tp *torProvider) SetStatusCallback(callback func(int, string)) {
|
||||
tp.lock.Lock()
|
||||
defer tp.lock.Unlock()
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err == nil {
|
||||
tp.isClosed = false
|
||||
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) {
|
||||
loggerDebug := &logWriter{log.LevelDebug}
|
||||
loggerError := &logWriter{log.LevelError}
|
||||
|
||||
cmd := exec.CommandContext(ctx, exePath, args...)
|
||||
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) {
|
||||
// attempt connect to system tor
|
||||
log.Debugf("dialing tor control port")
|
||||
controlport, err := dialControlPort(tp.controlPort)
|
||||
if err == nil {
|
||||
defer controlport.Close()
|
||||
err := tp.authenticator.Authenticate(controlport)
|
||||
if err == nil {
|
||||
log.Debugln("connected to control port")
|
||||
pinfo, err := controlport.ProtocolInfo()
|
||||
if err == nil {
|
||||
if minTorVersionReqs(pinfo.TorVersion) {
|
||||
log.Debugln("OK version " + pinfo.TorVersion)
|
||||
return pinfo.TorVersion, nil
|
||||
}
|
||||
return pinfo.TorVersion, fmt.Errorf("tor version not supported: %v", pinfo.TorVersion)
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
func startTor(appDirectory string, bundledTorPath string, dataDir string, controlPort int, authenticator tor.Authenticator) (*torProvider, error) {
|
||||
torDir := path.Join(appDirectory, "tor")
|
||||
|
||||
os.MkdirAll(torDir, 0700)
|
||||
|
||||
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}
|
||||
|
||||
log.Debugf("checking if there is a running system tor")
|
||||
if version, err := tp.checkVersion(); err == nil {
|
||||
tp.version = version
|
||||
controlport, err := dialControlPort(tp.controlPort)
|
||||
if err == nil {
|
||||
log.Debugf("creating tor handler from system tor")
|
||||
tp.t = createFromExisting(controlport, dataDir)
|
||||
tp.dialer, err = tp.t.Dialer(context.TODO(), &tor.DialConf{Authenticator: tp.authenticator})
|
||||
return tp, err
|
||||
}
|
||||
}
|
||||
|
||||
// check if the torrc file is present where expected
|
||||
if _, err := os.Stat(path.Join(torDir, "torrc")); os.IsNotExist(err) {
|
||||
err = &NoTorrcError{path.Join(torDir, "torrc")}
|
||||
log.Debugln(err.Error())
|
||||
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 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)")
|
||||
}
|
||||
|
||||
version, 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
|
||||
}
|
||||
return nil, fmt.Errorf("could not connect to running tor: %v", err)
|
||||
}
|
||||
|
||||
func (tp *torProvider) GetPID() (int, error) {
|
||||
val, err := tp.t.Control.GetInfo("process/pid")
|
||||
if err == nil {
|
||||
return strconv.Atoi(val[0].Val)
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
|
||||
func (tp *torProvider) monitorRestart() {
|
||||
lastBootstrapProgress := networkUnknown
|
||||
interval := minStatusIntervalMs
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-time.After(time.Millisecond * time.Duration(interval)):
|
||||
if interval < maxStatusIntervalMs {
|
||||
interval *= 2
|
||||
}
|
||||
|
||||
prog, status := tp.GetBootstrapStatus()
|
||||
|
||||
if prog == torDown && tp.t != nil {
|
||||
tp.restart()
|
||||
return
|
||||
}
|
||||
|
||||
if prog != lastBootstrapProgress {
|
||||
tp.callStatusCallback(prog, status)
|
||||
interval = minStatusIntervalMs
|
||||
lastBootstrapProgress = prog
|
||||
}
|
||||
|
||||
case <-tp.breakChan:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
return nil, nil, errors.New("could not fetch circuits or streams")
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
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...
|
||||
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))
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
|
@ -0,0 +1,115 @@
|
|||
package tor
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.openprivacy.ca/openprivacy/log"
|
||||
"os"
|
||||
path "path/filepath"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func getStatusCallback(progChan chan int) func(int, string) {
|
||||
return func(prog int, status string) {
|
||||
fmt.Printf("%v %v\n", prog, status)
|
||||
progChan <- prog
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
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"})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
acn.SetStatusCallback(getStatusCallback(progChan))
|
||||
acn.SetVersionCallback(getVersionCallback(verChan))
|
||||
|
||||
progress := 0
|
||||
for progress < 100 {
|
||||
progress = <-progChan
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
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"
|
||||
"golang.org/x/crypto/sha3"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// V3HostnameLength is the length of a Tor V3 Onion Address (without the .onion suffix)
|
||||
const V3HostnameLength = 56
|
||||
|
||||
// Hidden service version
|
||||
const version = byte(0x03)
|
||||
|
||||
// Salt used to create checkdigits
|
||||
const salt = ".onion checksum"
|
||||
|
||||
func getCheckdigits(pub ed25519.PublicKey) []byte {
|
||||
// Calculate checksum sha3(".onion checksum" || publicKey || version)
|
||||
checkstr := []byte(salt)
|
||||
checkstr = append(checkstr, pub...)
|
||||
checkstr = append(checkstr, version)
|
||||
checksum := sha3.Sum256(checkstr)
|
||||
return checksum[:2]
|
||||
}
|
||||
|
||||
// GetTorV3Hostname converts an ed25519 public key to a valid tor onion hostname
|
||||
func GetTorV3Hostname(pub ed25519.PublicKey) string {
|
||||
// Construct onion address base32(publicKey || checkdigits || version)
|
||||
checkdigits := getCheckdigits(pub)
|
||||
combined := pub[:]
|
||||
combined = append(combined, checkdigits...)
|
||||
combined = append(combined, version)
|
||||
serviceID := base32.StdEncoding.EncodeToString(combined)
|
||||
return strings.ToLower(serviceID)
|
||||
}
|
||||
|
||||
// IsValidHostname returns true if the given address is a valid onion v3 address
|
||||
func IsValidHostname(address string) bool {
|
||||
if len(address) == V3HostnameLength {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// HashedPasswordAuthenticator authenticates to a Tor control port using a hashed password.
|
||||
// Note: This method is vulnerable to replay attacks by the host system (but so is cookie auth)
|
||||
type HashedPasswordAuthenticator struct {
|
||||
Password string
|
||||
}
|
||||
|
||||
// Authenticate uses the given hashed password to authenticate to the control port
|
||||
func (h HashedPasswordAuthenticator) Authenticate(controlport *control.Conn) error {
|
||||
return controlport.Authenticate(h.Password)
|
||||
}
|
||||
|
||||
// NewHashedPasswordAuthenticator creates a new hashed password authenticator
|
||||
func NewHashedPasswordAuthenticator(password string) tor.Authenticator {
|
||||
return HashedPasswordAuthenticator{Password: password}
|
||||
}
|
||||
|
||||
// NullAuthenticator exists to force always authenticating to a system tor.
|
||||
type NullAuthenticator struct {
|
||||
}
|
||||
|
||||
// Authenticate on a NullAuthenticator always results in failure.
|
||||
func (n NullAuthenticator) Authenticate(controlport *control.Conn) error {
|
||||
return errors.New("null authenticator provided, this control port is unsafe, start a new tor process instead")
|
||||
}
|
|
@ -0,0 +1,108 @@
|
|||
package tor
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"filippo.io/edwards25519"
|
||||
"git.openprivacy.ca/openprivacy/bine/torutil"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGenerateHashedPassword(t *testing.T) {
|
||||
// 16:C15305F97789414B601259E3EC5E76B8E55FC56A9F562B713F3D2BA257
|
||||
hp := generateHashedPassword([8]byte{0xC1, 0x53, 0x05, 0xF9, 0x77, 0x89, 0x41, 0x4B}, "examplehashedpassword")
|
||||
if hp != "16:C15305F97789414B601259E3EC5E76B8E55FC56A9F562B713F3D2BA257" {
|
||||
t.Fatalf("hashed passwords do not match. Expected %s, got %s", "16:C15305F97789414B601259E3EC5E76B8E55FC56A9F562B713F3D2BA257", hp)
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
err := NewTorrc().WithHashedPassword(password).Build(path)
|
||||
if err != nil {
|
||||
t.Errorf("Torrc file could not be written")
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,131 @@
|
|||
package tor
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"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
|
||||
}
|
||||
|
||||
// NewTorrc creates a new torrc builder
|
||||
func NewTorrc() *TorrcBuilder {
|
||||
return &TorrcBuilder{
|
||||
lines: []string{},
|
||||
}
|
||||
}
|
||||
|
||||
// WithSocksPort sets the SOCKS port of the tor process
|
||||
func (tb *TorrcBuilder) WithSocksPort(port int) *TorrcBuilder {
|
||||
tb.lines = append(tb.lines, fmt.Sprintf("SocksPort %v", port))
|
||||
return tb
|
||||
}
|
||||
|
||||
// WithControlPort sets the control port of the tor process
|
||||
func (tb *TorrcBuilder) WithControlPort(port int) *TorrcBuilder {
|
||||
tb.lines = append(tb.lines, fmt.Sprintf("ControlPort %v", port))
|
||||
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 {
|
||||
if strings.HasPrefix(line, "SocksPort") {
|
||||
tb.lines[i] = fmt.Sprintf("%v OnionTrafficOnly", line)
|
||||
return tb
|
||||
}
|
||||
}
|
||||
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
|
||||
if _, err := io.ReadFull(rand.Reader, salt[:]); err != nil {
|
||||
panic("no randomness")
|
||||
}
|
||||
hashedpassword := generateHashedPassword(salt, password)
|
||||
tb.lines = append(tb.lines, fmt.Sprintf("HashedControlPassword %s", hashedpassword))
|
||||
return tb
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
// 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.
|
||||
func generateHashedPassword(salt [8]byte, password string) string {
|
||||
c := 96
|
||||
count := (16 + (c & 15)) << ((c >> 4) + 6)
|
||||
tmp := append(salt[:], []byte(password)...)
|
||||
slen := len(tmp)
|
||||
d := sha1.New()
|
||||
for count != 0 {
|
||||
if count > slen {
|
||||
d.Write(tmp)
|
||||
count -= slen
|
||||
} else {
|
||||
d.Write(tmp[:count])
|
||||
count = 0
|
||||
}
|
||||
}
|
||||
hashed := d.Sum([]byte{})
|
||||
return fmt.Sprintf("16:%s%s%s", strings.ToUpper(hex.EncodeToString(salt[:])),
|
||||
strings.ToUpper(hex.EncodeToString([]byte{byte(c)})),
|
||||
strings.ToUpper(hex.EncodeToString(hashed)))
|
||||
}
|
386
torProvider.go
386
torProvider.go
|
@ -1,386 +0,0 @@
|
|||
package connectivity
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"git.openprivacy.ca/openprivacy/log"
|
||||
"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"
|
||||
)
|
||||
|
||||
// Error captures various common ricochet errors
|
||||
type Error string
|
||||
|
||||
func (e Error) Error() string { return string(e) }
|
||||
|
||||
const (
|
||||
// CannotResolveLocalTCPAddressError is thrown when a local ricochet connection has the wrong format.
|
||||
CannotResolveLocalTCPAddressError = Error("CannotResolveLocalTCPAddressError")
|
||||
// CannotDialLocalTCPAddressError is thrown when a connection to a local ricochet address fails.
|
||||
CannotDialLocalTCPAddressError = Error("CannotDialLocalTCPAddressError")
|
||||
// CannotDialRicochetAddressError is thrown when a connection to a ricochet address fails.
|
||||
CannotDialRicochetAddressError = 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 = 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
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
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()
|
||||
}
|
37
torUtils.go
37
torUtils.go
|
@ -1,37 +0,0 @@
|
|||
package connectivity
|
||||
|
||||
import (
|
||||
"encoding/base32"
|
||||
"strings"
|
||||
"golang.org/x/crypto/ed25519"
|
||||
"golang.org/x/crypto/sha3"
|
||||
)
|
||||
|
||||
// V3HostnameLength is the length of a Tor V3 Onion Address (without the .onion suffix)
|
||||
const V3HostnameLength = 56
|
||||
|
||||
// Hidden service version
|
||||
const version = byte(0x03)
|
||||
|
||||
// Salt used to create checkdigits
|
||||
const salt = ".onion checksum"
|
||||
|
||||
func getCheckdigits(pub ed25519.PublicKey) []byte {
|
||||
// Calculate checksum sha3(".onion checksum" || publicKey || version)
|
||||
checkstr := []byte(salt)
|
||||
checkstr = append(checkstr, pub...)
|
||||
checkstr = append(checkstr, version)
|
||||
checksum := sha3.Sum256(checkstr)
|
||||
return checksum[:2]
|
||||
}
|
||||
|
||||
// GetTorV3Hostname converts an ed25519 public key to a valid tor onion hostname
|
||||
func GetTorV3Hostname(pub ed25519.PublicKey) string {
|
||||
// Construct onion address base32(publicKey || checkdigits || version)
|
||||
checkdigits := getCheckdigits(pub)
|
||||
combined := pub[:]
|
||||
combined = append(combined, checkdigits...)
|
||||
combined = append(combined, version)
|
||||
serviceID := base32.StdEncoding.EncodeToString(combined)
|
||||
return strings.ToLower(serviceID)
|
||||
}
|
Loading…
Reference in New Issue