start working on a conversion of the torProvider into an i2pProvider

This commit is contained in:
idk 2020-12-27 17:29:07 -05:00
parent 38253594d7
commit 683120487d
Signed by: idki2p
GPG Key ID: D75C03B39B5E14E1
6 changed files with 564 additions and 0 deletions

454
i2p/i2pProvider.go Normal file
View File

@ -0,0 +1,454 @@
package tor
import (
// "context"
"errors"
"fmt"
"github.com/eyedeekay/go-i2pcontrol"
"github.com/eyedeekay/sam3"
//"github.com/eyedeekay/sam3/helper"
"git.openprivacy.ca/openprivacy/connectivity"
"git.openprivacy.ca/openprivacy/log"
"golang.org/x/crypto/ed25519"
"golang.org/x/crypto/sha3"
"io/ioutil"
"net"
"net/textproto"
"os"
"os/exec"
"path"
"regexp"
"strconv"
"strings"
"sync"
"time"
)
const (
minStatusIntervalMs = 200
maxStatusIntervalMs = 2000
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
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 {
os *sam3.StreamListener
tp *i2pProvider
}
type i2pProvider struct {
controlPort int
//t *sam3.SAMConn
dialer *sam3.StreamSession
appDirectory string
bundeledTorPath string
lock sync.Mutex
breakChan chan bool
childListeners map[string]*onionListenService
statusCallback func(int, string)
lastRestartTime time.Time
//authenticator tor.Authenticator
}
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 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 *i2pProvider) GetBootstrapStatus() (int, string) {
tp.lock.Lock()
defer tp.lock.Unlock()
reseeding, err := i2pcontrol.Reseeding()
if err != nil {
return -2, err.Error()
}
if reseeding {
return 25, "I2P Router is Reseeding"
}
stat, err := i2pcontrol.Status()
if err != nil {
return -2, err.Error()
}
if strings.Contains(stat, "Rejecting") {
return 50, "I2P Router is rejecting tunnels"
}
netstat, err := i2pcontrol.Status()
if err != nil {
return -2, err.Error()
}
if strings.Contains(netstat, "ERROR") {
return -1, netstat
}
return 100, ""
}
func (tp *i2pProvider) GetVersion() string {
return "3.1"
}
// WaitTillBootstrapped Blocks until underlying network is bootstrapped
func (tp *i2pProvider) WaitTillBootstrapped() {
for true {
progress, _ := tp.GetBootstrapStatus()
if progress == 100 {
break
}
time.Sleep(100 * time.Millisecond)
}
}
func (tp *i2pProvider) 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")
//}
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 *i2pProvider) Restart() {
tp.callStatusCallback(0, "rebooting")
tp.restart()
}
func (tp *i2pProvider) 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 *i2pProvider) 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 *i2pProvider) SetStatusCallback(callback func(int, string)) {
tp.lock.Lock()
defer tp.lock.Unlock()
tp.statusCallback = callback
}
func (tp *i2pProvider) callStatusCallback(prog int, status string) {
tp.lock.Lock()
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, controlPort int, authenticator tor.Authenticator) (connectivity.ACN, error) {
tp, err := startTor(appDirectory, bundledTorPath, controlPort, authenticator)
if err == nil {
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{})
}
func (tp *i2pProvider) checkVersion() error {
// attempt connect to system tor
log.Debugf("dialing system 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 nil
}
return fmt.Errorf("Tor version not supported: %v", pinfo.TorVersion)
}
}
}*/
return err
}
func startTor(appDirectory string, bundledTorPath string, controlPort int, authenticator tor.Authenticator) (*i2pProvider, 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 := &i2pProvider{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("launching system tor")
if err := tp.checkVersion(); err == nil {
controlport, err := dialControlPort(tp.controlPort)
if err == nil {
log.Debugf("creating tor handler fom system tor")
tp.t = createFromExisting(controlport, dataDir)
}
return tp, nil
}
// 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 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
} 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
}
err = tp.checkVersion()
if err == nil {
tp.t.DeleteDataDirOnClose = true
return tp, nil
}
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 *i2pProvider) 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 *i2pProvider) unregisterListener(id string) {
tp.lock.Lock()
defer tp.lock.Unlock()
delete(tp.childListeners, id)
}
func (tp *i2pProvider) 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 {
log.Warnf("monitorRestart calling tp.restart() with prog:%v\n", prog)
tp.restart()
}
if prog != lastBootstrapProgress {
tp.callStatusCallback(prog, status)
interval = minStatusIntervalMs
lastBootstrapProgress = prog
}
case <-tp.breakChan:
return
}
}
}
func (tp *i2pProvider) 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
}
}
}
func createFromExisting(controlport *control.Conn, datadir string) *tor.Tor {
t := &tor.Tor{
Process: nil,
Control: controlport,
ProcessCancelFunc: nil,
DataDir: datadir,
DeleteDataDirOnClose: true,
DebugWriter: nil,
StopProcessOnClose: false,
GeoIPCreatedFile: "",
GeoIPv6CreatedFile: "",
}
t.Control.DebugWriter = t.DebugWriter
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
}

36
i2p/i2pProvider_test.go Normal file
View File

@ -0,0 +1,36 @@
package tor
import (
"fmt"
"git.openprivacy.ca/openprivacy/log"
"path"
"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)
log.SetLevel(log.LevelDebug)
torpath := path.Join("..", "tmp/tor")
log.Debugf("setting tor path %v", torpath)
acn, err := NewTorACNWithAuth(path.Join("../testing/"), torpath, 9051, HashedPasswordAuthenticator{"examplehashedpassword"})
if err != nil {
t.Error(err)
return
}
acn.SetStatusCallback(getStatusCallback(progChan))
progress := 0
for progress < 100 {
progress = <-progChan
t.Logf("progress: %v", progress)
}
acn.Close()
}

32
i2p/i2pUtils.go Normal file
View File

@ -0,0 +1,32 @@
package tor
import (
"encoding/base32"
// "errors"
"golang.org/x/crypto/ed25519"
"golang.org/x/crypto/sha3"
"strings"
)
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]
}
// 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]
if GetTorV3Hostname(ed25519.PublicKey(pubkey)) == address {
return true
}
}
}
return false
}

24
i2p/i2pUtils_test.go Normal file
View File

@ -0,0 +1,24 @@
package tor
import (
"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 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)
}

9
i2p/sysProcAttr_rest.go Normal file
View File

@ -0,0 +1,9 @@
// +build !windows
package tor
import (
"syscall"
)
var sysProcAttr = &syscall.SysProcAttr{}

9
i2p/sysProcAttr_win.go Normal file
View File

@ -0,0 +1,9 @@
// +build windows
package tor
import (
"syscall"
)
var sysProcAttr = &syscall.SysProcAttr{HideWindow: true}