tLibrary implementation. - tordam - A library for peer discovery inside the Tor network
 (HTM) git clone https://git.parazyd.org/tordam
 (DIR) Log
 (DIR) Files
 (DIR) Refs
 (DIR) README
 (DIR) LICENSE
       ---
 (DIR) commit 64624b0a842c5cbee96503d7a347b5bec1711161
 (DIR) parent 2f8bd41a607d578b727c1c8ee20f10b2cebb1bdc
 (HTM) 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(-)
       ---
 (DIR) diff --git a/announce_test.go b/announce_test.go
       t@@ -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)
       +                }
       +        }
       +}
 (DIR) diff --git a/config.go b/config.go
       t@@ -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{}
 (DIR) diff --git a/cryptohelpers.go b/cryptohelpers.go
       t@@ -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
       +}
 (DIR) diff --git a/cryptohelpers_test.go b/cryptohelpers_test.go
       t@@ -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)
       +        }
       +}
 (DIR) diff --git a/go.mod b/go.mod
       t@@ -0,0 +1,3 @@
       +module github.com/parazyd/tordam
       +
       +go 1.16
 (DIR) diff --git a/logging.go b/logging.go
       t@@ -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)
       +}
 (DIR) diff --git a/net.go b/net.go
       t@@ -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
       +}
 (DIR) diff --git a/net_test.go b/net_test.go
       t@@ -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)
       +        }
       +}
 (DIR) diff --git a/peer.go b/peer.go
       t@@ -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
       +}
 (DIR) diff --git a/peer_announce.go b/peer_announce.go
       t@@ -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
       +}
 (DIR) diff --git a/rpc_announce.go b/rpc_announce.go
       t@@ -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
       +}
 (DIR) diff --git a/sanity.go b/sanity.go
       t@@ -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
       +}
 (DIR) diff --git a/sanity_test.go b/sanity_test.go
       t@@ -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)
       +                }
       +        }
       +}
 (DIR) diff --git a/tor.go b/tor.go
       t@@ -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()
       +}
 (DIR) diff --git a/tor_test.go b/tor_test.go
       t@@ -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)
       +        }
       +}