tordam

A library for peer discovery inside the Tor network
git clone https://git.parazyd.org/tordam
Log | Files | Refs | README | LICENSE

commit 64624b0a842c5cbee96503d7a347b5bec1711161
parent 2f8bd41a607d578b727c1c8ee20f10b2cebb1bdc
Author: parazyd <parazyd@dyne.org>
Date:   Sun,  7 Mar 2021 20:22:05 +0100

Library implementation.

Diffstat:
Aannounce_test.go | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aconfig.go | 46++++++++++++++++++++++++++++++++++++++++++++++
Acryptohelpers.go | 37+++++++++++++++++++++++++++++++++++++
Acryptohelpers_test.go | 28++++++++++++++++++++++++++++
Ago.mod | 3+++
Alogging.go | 36++++++++++++++++++++++++++++++++++++
Anet.go | 37+++++++++++++++++++++++++++++++++++++
Anet_test.go | 29+++++++++++++++++++++++++++++
Apeer.go | 33+++++++++++++++++++++++++++++++++
Apeer_announce.go | 107+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Arpc_announce.go | 194+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asanity.go | 82+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asanity_test.go | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ator.go | 69+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ator_test.go | 36++++++++++++++++++++++++++++++++++++
15 files changed, 858 insertions(+), 0 deletions(-)

diff --git a/announce_test.go b/announce_test.go @@ -0,0 +1,64 @@ +// Copyright (c) 2017-2021 Ivan Jelincic <parazyd@dyne.org> +// +// This file is part of tordam +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +package tordam + +import ( + "context" + "crypto/ed25519" + "crypto/rand" + "encoding/base64" + "testing" +) + +func TestAnnounce(t *testing.T) { + pk, sk, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + + vals := []string{ + "p7qaewjgnvnaeihhyybmoofd5avh665kr3awoxlh5rt6ox743kjdr6qd.onion:666", + base64.StdEncoding.EncodeToString(pk), + "12345:54321,666:3521", + } + + ret, err := ann.Init(ann{}, context.Background(), vals) + if err != nil { + t.Fatal(err) + } + for _, i := range ret { + if _, err := base64.StdEncoding.DecodeString(i); err != nil { + t.Fatal(err) + } + } + + vals = []string{ + "p7qaewjgnvnaeihhyybmoofd5avh665kr3awoxlh5rt6ox743kjdr6qd.onion:666", + base64.StdEncoding.EncodeToString(ed25519.Sign(sk, []byte(ret[0]))), + } + + ret, err = ann.Validate(ann{}, context.Background(), vals) + if err != nil { + t.Fatal(err) + } + for _, i := range ret { + if err := validateOnionInternal(i); err != nil { + t.Fatal(err) + } + } +} diff --git a/config.go b/config.go @@ -0,0 +1,46 @@ +// Copyright (c) 2017-2021 Ivan Jelincic <parazyd@dyne.org> +// +// This file is part of tordam +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +package tordam + +import ( + "crypto/ed25519" + "net" +) + +// Config is the configuration structure, to be filled by library user. +type Config struct { + Listen *net.TCPAddr // Local listen address for the JSON-RPC server + TorAddr *net.TCPAddr // Tor SOCKS5 proxy address, filled by SpawnTor() + Datadir string // Path to data directory + Portmap []string // The peer's portmap, to be mapped in the Tor HS + Seeds []string // Initial peer(s) + Announce bool // Announce or not +} + +// SignKey is an ed25519 private key, to be assigned by library user. +var SignKey ed25519.PrivateKey + +// Onion is the library user's something.onion:port identifier. It can be read +// from the datadir once Tor is spawned. +var Onion string + +// Cfg is the global config structure, to be filled by library user. +var Cfg = Config{} + +// Peers is the global map of peers +var Peers = map[string]Peer{} diff --git a/cryptohelpers.go b/cryptohelpers.go @@ -0,0 +1,37 @@ +// Copyright (c) 2017-2021 Ivan Jelincic <parazyd@dyne.org> +// +// This file is part of tordam +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +package tordam + +import ( + "crypto/rand" + "encoding/base64" + "fmt" +) + +// RandomGarbage returns a base64 encoded string of n bytes. +func RandomGarbage(n int) (string, error) { + garbage := make([]byte, n) + read, err := rand.Read(garbage) + if err != nil { + return "", err + } + if n != read { + return "", fmt.Errorf("read %d, but requested %d bytes", read, n) + } + return base64.StdEncoding.EncodeToString(garbage), nil +} diff --git a/cryptohelpers_test.go b/cryptohelpers_test.go @@ -0,0 +1,28 @@ +// Copyright (c) 2017-2021 Ivan Jelincic <parazyd@dyne.org> +// +// This file is part of tordam +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +package tordam + +import ( + "testing" +) + +func TestRandomGarbage(t *testing.T) { + if _, err := RandomGarbage(128); err != nil { + t.Fatal(err) + } +} diff --git a/go.mod b/go.mod @@ -0,0 +1,3 @@ +module github.com/parazyd/tordam + +go 1.16 diff --git a/logging.go b/logging.go @@ -0,0 +1,36 @@ +// Copyright (c) 2017-2021 Ivan Jelincic <parazyd@dyne.org> +// +// This file is part of tordam +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +package tordam + +import ( + "log" + "strings" +) + +func rpcWarn(msg ...string) { + text := strings.Join(msg[1:], " ") + log.Printf("RPC warning: (%s) %s", msg[0], text) +} +func rpcInfo(msg ...string) { + text := strings.Join(msg[1:], " ") + log.Printf("RPC info: (%s) %s", msg[0], text) +} +func rpcInternalErr(msg ...string) { + text := strings.Join(msg[1:], " ") + log.Printf("RPC internal error: (%s) %s", msg[0], text) +} diff --git a/net.go b/net.go @@ -0,0 +1,37 @@ +// Copyright (c) 2017-2021 Ivan Jelincic <parazyd@dyne.org> +// +// This file is part of tordam +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +package tordam + +import "net" + +// GetAvailableListener is a helper function to return a *net.TCPAddr on some +// port that is available for listening on the system. It uses the :0 port +// which the kernel utilizes to return a random available port. +func GetAvailableListener() (*net.TCPAddr, error) { + addr, err := net.ResolveTCPAddr("tcp4", "localhost:0") + if err != nil { + return nil, err + } + + l, err := net.ListenTCP("tcp4", addr) + if err != nil { + return nil, err + } + defer l.Close() + return l.Addr().(*net.TCPAddr), nil +} diff --git a/net_test.go b/net_test.go @@ -0,0 +1,29 @@ +// Copyright (c) 2017-2021 Ivan Jelincic <parazyd@dyne.org> +// +// This file is part of tordam +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +package tordam + +import "testing" + +// GetAvailableListener is a helper function to return a *net.TCPAddr on some +// port that is available for listening on the system. It uses the :0 port +// which the kernel utilizes to return a random available port. +func TestGetAvailableListener(t *testing.T) { + if _, err := GetAvailableListener(); err != nil { + t.Fatal(err) + } +} diff --git a/peer.go b/peer.go @@ -0,0 +1,33 @@ +// Copyright (c) 2017-2021 Ivan Jelincic <parazyd@dyne.org> +// +// This file is part of tordam +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +package tordam + +import ( + "crypto/ed25519" +) + +// Peer is the base struct for any peer in the network. +type Peer struct { + Pubkey ed25519.PublicKey + Portmap []string + Nonce string + SelfRevoke string // Our revoke key we use to update our data + PeerRevoke string // Peer's revoke key if they wish to update their data + LastSeen int64 + Trusted int // Trusted is int because of possible levels of trust +} diff --git a/peer_announce.go b/peer_announce.go @@ -0,0 +1,107 @@ +// Copyright (c) 2017-2021 Ivan Jelincic <parazyd@dyne.org> +// +// This file is part of tordam +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +package tordam + +import ( + "context" + "crypto/ed25519" + "encoding/base64" + "log" + "strings" + + "github.com/creachadair/jrpc2" + "github.com/creachadair/jrpc2/channel" + "golang.org/x/net/proxy" +) + +// Announce is the function that announces to a certain onion address. Upon +// success, it appends the peers received from the endpoint to the global +// Peers map. +func Announce(onionaddr string) error { + socks, err := proxy.SOCKS5("tcp", Cfg.TorAddr.String(), nil, proxy.Direct) + if err != nil { + return err + } + + // conn, err := net.Dial(jrpc2.Network(Cfg.Listen), Cfg.Listen) + conn, err := socks.Dial("tcp", onionaddr) + if err != nil { + return err + } + defer conn.Close() + + cli := jrpc2.NewClient(channel.RawJSON(conn, conn), nil) + defer cli.Close() + ctx := context.Background() + + b64pk := base64.StdEncoding.EncodeToString( + SignKey.Public().(ed25519.PublicKey)) + + var resp [2]string + data := []string{Onion, b64pk, strings.Join(Cfg.Portmap, ",")} + + if peer, ok := Peers[onionaddr]; ok { + // Here the implication is that it's not our first announce, so we + // have received a revoke key to use in subsequent announces. + data = append(data, peer.SelfRevoke) + } + + if err := cli.CallResult(ctx, "ann.Init", data, &resp); err != nil { + return err + } + nonce := resp[0] + + // TODO: Think about this > + var peer Peer + if _, ok := Peers[onionaddr]; ok { + peer = Peers[onionaddr] + } + peer.SelfRevoke = resp[1] + Peers[onionaddr] = peer + + sig := base64.StdEncoding.EncodeToString( + ed25519.Sign(SignKey, []byte(nonce))) + + var newPeers []string + if err := cli.CallResult(ctx, "ann.Validate", + []string{onionaddr, sig}, &newPeers); err != nil { + return err + } + + return AppendPeers(newPeers) +} + +// AppendPeers appends given []string peers to the global Peers map. Usually +// received by validating ourself to a peer and them replying with a list of +// their valid peers. If a peer is not in format of "unlikelyname.onion:port", +// they will not be appended. +// As a placeholder, this function can return an error, but it has no reason +// to do so right now. +func AppendPeers(p []string) error { + for _, i := range p { + if _, ok := Peers[i]; ok { + continue + } + if err := validateOnionInternal(i); err != nil { + log.Printf("warning: received garbage peer (%v)", err) + continue + } + Peers[i] = Peer{} + } + return nil +} diff --git a/rpc_announce.go b/rpc_announce.go @@ -0,0 +1,194 @@ +// Copyright (c) 2017-2021 Ivan Jelincic <parazyd@dyne.org> +// +// This file is part of tordam +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +package tordam + +import ( + "context" + "crypto/ed25519" + "encoding/base64" + "errors" + "strings" + "time" +) + +type ann struct{} + +// Init takes three parameters: +// - onion: onionaddress:port where the peer and tordam can be reached +// - pubkey: ed25519 public signing key in base64 +// - portmap: List of ports available for communication +// - (optional) revoke: Revocation key for updating peer info +// {"jsonrpc":"2.0", +// "id": 1, +// "method": "ann.Init", +// "params": ["unlikelynameforan.onion:49371", "214=", "69:420,323:2354"] +// } +// Returns: +// - nonce: A random nonce which is to be signed by the client +// - revoke: A key which can be used to revoke key and portman and reannounce the peer +// {"jsonrpc":"2.0", +// "id":1, +// "result": ["somenonce", "somerevokekey"] +// } +// On any kind of failure returns an error and the reason. +func (ann) Init(ctx context.Context, vals []string) ([]string, error) { + if len(vals) != 3 && len(vals) != 4 { + return nil, errors.New("invalid parameters") + } + + onion := vals[0] + pubkey := vals[1] + portmap := strings.Split(vals[2], ",") + + if err := validateOnionInternal(onion); err != nil { + rpcWarn("ann.Init", err.Error()) + return nil, err + } + + rpcInfo("ann.Init", "got request for", onion) + + var peer Peer + reallySeen := false + peer, ok := Peers[onion] + if ok { + // We have seen this peer + if peer.Pubkey != nil || peer.PeerRevoke != "" { + reallySeen = true + } + } + + if reallySeen { + // Peer announced to us before + if len(vals) != 4 { + rpcWarn("ann.Init", "no revocation key provided") + return nil, errors.New("no revocation key provided") + } + revoke := vals[3] + if strings.Compare(revoke, peer.PeerRevoke) != 0 { + rpcWarn("ann.Init", "revocation key doesn't match") + return nil, errors.New("revocation key doesn't match") + } + } + + pk, err := base64.StdEncoding.DecodeString(pubkey) + if err != nil { + rpcWarn("ann.Init", "got invalid base64 public key") + return nil, errors.New("invalid base64 public key") + } else if len(pk) != 32 { + rpcWarn("ann.Init", "got invalid pubkey (len != 32)") + return nil, errors.New("invalid public key") + } + + if err := ValidatePortmap(portmap); err != nil { + rpcWarn("ann.Init", err.Error()) + return nil, err + } + + nonce, err := RandomGarbage(32) + if err != nil { + rpcInternalErr("ann.Init", err.Error()) + return nil, errors.New("internal error") + } + + newrevoke, err := RandomGarbage(128) + if err != nil { + rpcInternalErr("ann.Init", err.Error()) + return nil, errors.New("internal error") + } + + peer.Pubkey = pk + peer.Portmap = portmap + peer.Nonce = nonce + peer.PeerRevoke = newrevoke + peer.LastSeen = time.Now().Unix() + peer.Trusted = 0 + Peers[onion] = peer + + return []string{nonce, newrevoke}, nil +} + +// Validate takes two parameters: +// - onion: onionaddress:port where the peer and tordam can be reached +// - signature: base64 signature of the previously obtained nonce +// {"jsonrpc":"2.0", +// "id":2, +// "method": "ann.Announce", +// "params": ["unlikelynameforan.onion:49371", "deadbeef=="] +// } +// Returns: +// - peers: A list of known validated peers (max. 50) +// {"jsonrpc":"2.0", +// "id":2, +// "result": ["unlikelynameforan.onion:69", "yetanother.onion:420"] +// } +// On any kind of failure returns an error and the reason. +func (ann) Validate(ctx context.Context, vals []string) ([]string, error) { + if len(vals) != 2 { + return nil, errors.New("invalid parameters") + } + + onion := vals[0] + signature := vals[1] + + if err := validateOnionInternal(onion); err != nil { + rpcWarn("ann.Validate", err.Error()) + return nil, err + } + + rpcInfo("ann.Validate", "got request for", onion) + + peer, ok := Peers[onion] + if !ok { + rpcWarn("ann.Validate", onion, "not in peer map") + return nil, errors.New("this onion was not seen before") + } + + if peer.Pubkey == nil || peer.Nonce == "" { + rpcWarn("ann.Validate", onion, "tried to validate before init") + return nil, errors.New("tried to validate before init") + } + + sig, err := base64.StdEncoding.DecodeString(signature) + if err != nil { + rpcWarn("ann.Validate", "invalid base64 signature string") + return nil, errors.New("invalid base64 signature string") + } + + if !ed25519.Verify(peer.Pubkey, []byte(peer.Nonce), sig) { + rpcWarn("ann.Validate", "signature verification failed") + // delete(Peers, onion) + return nil, errors.New("signature verification failed") + } + + rpcInfo("ann.Validate", "validation success for", onion) + + var ret []string + for addr, data := range Peers { + if data.Trusted > 0 { + ret = append(ret, addr) + } + } + + peer.Nonce = "" + peer.Trusted = 1 + peer.LastSeen = time.Now().Unix() + Peers[onion] = peer + + rpcInfo("ann.Validate", "sending back list of peers to", onion) + return ret, nil +} diff --git a/sanity.go b/sanity.go @@ -0,0 +1,82 @@ +// Copyright (c) 2017-2021 Ivan Jelincic <parazyd@dyne.org> +// +// This file is part of tordam +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +package tordam + +import ( + "encoding/base32" + "errors" + "fmt" + "strconv" + "strings" +) + +// ValidateOnionAddresses checks if the given string is a valid Tor v3 Hidden +// service address. Returns error if not. +func ValidateOnionAddress(addr string) error { + aupp := strings.ToUpper(strings.TrimSuffix(addr, ".onion")) + if len(aupp) != 56 { + return fmt.Errorf("invalid v3 onion address (len != 56)") + } + + if _, err := base32.StdEncoding.DecodeString(aupp); err != nil { + return fmt.Errorf("invalid v3 onion address: %s", err) + } + + return nil +} + +func validateOnionInternal(onionaddr string) error { + splitOnion := strings.Split(onionaddr, ":") + if len(splitOnion) != 2 { + return errors.New("onion address doesn't contain a port") + } + + p, err := strconv.Atoi(splitOnion[1]) + if err != nil { + return errors.New("onion port is invalid (not a number)") + } + if p < 1 || p > 65535 { + return errors.New("onion port is invalid (!= 0 < port < 65536)") + } + + return ValidateOnionAddress(splitOnion[0]) +} + +// ValidatePortmap checks if the given []string holds valid portmaps in the +// form of port:port (e.g. 1234:48372). Returns error if any of the found +// portmaps are invalid. +func ValidatePortmap(pm []string) error { + for _, pmap := range pm { + ports := strings.Split(pmap, ":") + + if len(ports) != 2 { + return fmt.Errorf("invalid portmap: %s (len != 2)", pmap) + } + + for i := 0; i < 2; i++ { + p, err := strconv.Atoi(ports[i]) + if err != nil { + return fmt.Errorf("invalid port: %s (%s)", ports[i], err) + } + if p < 1 || p > 65535 { + return fmt.Errorf("invalid port: %d (!= 0 < %d < 65536)", p, p) + } + } + } + return nil +} diff --git a/sanity_test.go b/sanity_test.go @@ -0,0 +1,57 @@ +// Copyright (c) 2017-2021 Ivan Jelincic <parazyd@dyne.org> +// +// This file is part of tordam +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +package tordam + +import "testing" + +func TestValidateOnionAddress(t *testing.T) { + const val0 = "p7qaewjgnvnaeihhyybmoofd5avh665kr3awoxlh5rt6ox743kjdr6qd.onion" + const inv0 = "p7qaewjg1vnaeihhyybmoofd5avh665kr3awoxlh5rt6ox743kjdr6qd.onion" + const inv1 = "p7qaewjgvybmoofd5avh665kr3awoxlh5rt6ox743kjdr6qd.onion" + const inv2 = "p7qaewjgvybmoofd5avh665kr3awoxl1jdr6qd.onion" + + if err := ValidateOnionAddress(val0); err != nil { + t.Fatalf("valid onion address reported invalid: %s", val0) + } + + for _, i := range []string{inv0, inv1, inv2} { + if err := ValidateOnionAddress(i); err == nil { + t.Fatalf("invalid onion address reported valid: %s", i) + } + } +} + +func TestValidatePortmap(t *testing.T) { + val0 := []string{"1234:3215"} + val1 := []string{} + val2 := []string{"31983:35155", "31587:11"} + inv0 := []string{"1515:315foo"} + inv1 := []string{"101667:8130", "1305:3191"} + + for _, i := range [][]string{val0, val1, val2} { + if err := ValidatePortmap(i); err != nil { + t.Fatalf("valid portmap reported invalid: %v", i) + } + } + + for _, i := range [][]string{inv0, inv1} { + if err := ValidatePortmap(i); err == nil { + t.Fatalf("invalid portmap reported valid: %v", i) + } + } +} diff --git a/tor.go b/tor.go @@ -0,0 +1,69 @@ +// Copyright (c) 2017-2021 Ivan Jelincic <parazyd@dyne.org> +// +// This file is part of tordam +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +package tordam + +import ( + "fmt" + "net" + "os" + "os/exec" + "strings" +) + +func newtorrc(listener, torlistener *net.TCPAddr, portmap []string) string { + var pm []string + + for _, i := range pm { + p := strings.Split(i, ":") + pm = append(pm, fmt.Sprintf("HiddenServicePort %s %s", + p[0], strings.Join([]string{"127.0.0.1", p[1]}, ":"))) + } + + return fmt.Sprintf(` +Log warn syslog +RunAsDaemon 0 +DataDirectory tor +SocksPort %s +HiddenServiceDir hs +HiddenServicePort %d %s +%s +`, torlistener.String(), + listener.Port, listener.String(), strings.Join(pm, "\n")) +} + +func SpawnTor(listener *net.TCPAddr, portmap []string, datadir string) (*exec.Cmd, error) { + var err error + + if err = ValidatePortmap(portmap); err != nil { + return nil, err + } + + Cfg.TorAddr, err = GetAvailableListener() + if err != nil { + return nil, err + } + + if err := os.MkdirAll(datadir, 0700); err != nil { + return nil, err + } + + cmd := exec.Command("tor", "-f", "-") + cmd.Stdin = strings.NewReader(newtorrc(listener, Cfg.TorAddr, portmap)) + cmd.Dir = datadir + return cmd, cmd.Start() +} diff --git a/tor_test.go b/tor_test.go @@ -0,0 +1,36 @@ +// Copyright (c) 2017-2021 Ivan Jelincic <parazyd@dyne.org> +// +// This file is part of tordam +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +package tordam + +import ( + "os" + "testing" +) + +func TestSpawnTor(t *testing.T) { + l, err := GetAvailableListener() + if err != nil { + t.Fatal(err) + } + tor, err := SpawnTor(l, []string{"1234:1234"}, "tor_test") + defer tor.Process.Kill() + defer os.RemoveAll("tor_test") + if err != nil { + t.Fatal(err) + } +}