commit 64624b0a842c5cbee96503d7a347b5bec1711161
parent 2f8bd41a607d578b727c1c8ee20f10b2cebb1bdc
Author: parazyd <parazyd@dyne.org>
Date: Sun, 7 Mar 2021 20:22:05 +0100
Library implementation.
Diffstat:
A | announce_test.go | | | 64 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | config.go | | | 46 | ++++++++++++++++++++++++++++++++++++++++++++++ |
A | cryptohelpers.go | | | 37 | +++++++++++++++++++++++++++++++++++++ |
A | cryptohelpers_test.go | | | 28 | ++++++++++++++++++++++++++++ |
A | go.mod | | | 3 | +++ |
A | logging.go | | | 36 | ++++++++++++++++++++++++++++++++++++ |
A | net.go | | | 37 | +++++++++++++++++++++++++++++++++++++ |
A | net_test.go | | | 29 | +++++++++++++++++++++++++++++ |
A | peer.go | | | 33 | +++++++++++++++++++++++++++++++++ |
A | peer_announce.go | | | 107 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | rpc_announce.go | | | 194 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | sanity.go | | | 82 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | sanity_test.go | | | 57 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | tor.go | | | 69 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | tor_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)
+ }
+}