tordam

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

tor-dam.go (6110B)


      1 // Copyright (c) 2017-2021 Ivan Jelincic <parazyd@dyne.org>
      2 //
      3 // This file is part of tordam
      4 //
      5 // This program is free software: you can redistribute it and/or modify
      6 // it under the terms of the GNU Affero General Public License as published by
      7 // the Free Software Foundation, either version 3 of the License, or
      8 // (at your option) any later version.
      9 //
     10 // This program is distributed in the hope that it will be useful,
     11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
     12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
     13 // GNU Affero General Public License for more details.
     14 //
     15 // You should have received a copy of the GNU Affero General Public License
     16 // along with this program. If not, see <https://www.gnu.org/licenses/>.
     17 
     18 package main
     19 
     20 import (
     21 	"crypto/ed25519"
     22 	"crypto/rand"
     23 	"encoding/base64"
     24 	"encoding/json"
     25 	"flag"
     26 	"fmt"
     27 	"io/ioutil"
     28 	"log"
     29 	"net"
     30 	"os"
     31 	"path/filepath"
     32 	"strings"
     33 	"sync"
     34 	"time"
     35 
     36 	"github.com/creachadair/jrpc2"
     37 	"github.com/creachadair/jrpc2/handler"
     38 	"github.com/creachadair/jrpc2/server"
     39 	"github.com/parazyd/tordam"
     40 )
     41 
     42 var (
     43 	generate = flag.Bool("g", false, "(Re)generate keys and exit")
     44 	portmap  = flag.String("m", "13010:13010,13011:13011",
     45 		"Map of ports forwarded to/from Tor")
     46 	listen  = flag.String("l", "127.0.0.1:49371", "Local listen address")
     47 	datadir = flag.String("d", os.Getenv("HOME")+"/.dam", "Data directory")
     48 	seeds   = flag.String("s",
     49 		"p7qaewjgnvnaeihhyybmoofd5avh665kr3awoxlh5rt6ox743kjdr6qd.onion:49371",
     50 		"List of initial peers (comma-separated)")
     51 	noannounce = flag.Bool("n", false, "Do not announce to peers")
     52 )
     53 
     54 // generateED25519Keypair is a helper function to generate it, and save the
     55 // seed to a file for later reuse.
     56 func generateED25519Keypair(dir string) error {
     57 	_, sk, err := ed25519.GenerateKey(rand.Reader)
     58 	if err != nil {
     59 		return err
     60 	}
     61 	if err := os.MkdirAll(dir, 0700); err != nil {
     62 		return err
     63 	}
     64 
     65 	seedpath := filepath.Join(dir, "ed25519.seed")
     66 	log.Println("Writing ed25519 key seed to", seedpath)
     67 	return ioutil.WriteFile(seedpath,
     68 		[]byte(base64.StdEncoding.EncodeToString(sk.Seed())), 0600)
     69 }
     70 
     71 // loadED25519Seed is a helper function to read an existing key seed and
     72 // return an ed25519.PrivateKey.
     73 func loadED25519Seed(file string) (ed25519.PrivateKey, error) {
     74 	log.Println("Reading ed25519 seed from", file)
     75 
     76 	data, err := ioutil.ReadFile(file)
     77 	if err != nil {
     78 		return nil, err
     79 	}
     80 	dec, err := base64.StdEncoding.DecodeString(string(data))
     81 	if err != nil {
     82 		return nil, err
     83 	}
     84 	return ed25519.NewKeyFromSeed(dec), nil
     85 }
     86 
     87 // main here is the reference workflow of tor-dam's peer discovery. Its steps
     88 // are commented and implement a generic way of using the tordam library.
     89 func main() {
     90 	flag.Parse()
     91 	var wg sync.WaitGroup
     92 	var err error
     93 
     94 	// Initialize tordam logger
     95 	tordam.LogInit(os.Stdout)
     96 
     97 	// Assign the global tordam data directory
     98 	tordam.Cfg.Datadir = *datadir
     99 
    100 	// Generate the ed25519 keypair used for signing and validating
    101 	if *generate {
    102 		if err := generateED25519Keypair(tordam.Cfg.Datadir); err != nil {
    103 			log.Fatal(err)
    104 		}
    105 		os.Exit(0)
    106 	}
    107 
    108 	// Assign portmap to tordam Cfg global and validate it
    109 	tordam.Cfg.Portmap = strings.Split(*portmap, ",")
    110 	if err := tordam.ValidatePortmap(tordam.Cfg.Portmap); err != nil {
    111 		log.Fatal(err)
    112 	}
    113 
    114 	// Validate and assign the local listening address
    115 	tordam.Cfg.Listen, err = net.ResolveTCPAddr("tcp", *listen)
    116 	if err != nil {
    117 		log.Fatalf("invalid listen address: %s (%v)", *listen, err)
    118 	}
    119 
    120 	// Load the ed25519 signing key into the tordam global
    121 	tordam.SignKey, err = loadED25519Seed(
    122 		filepath.Join(tordam.Cfg.Datadir, "ed25519.seed"))
    123 	if err != nil {
    124 		log.Fatal(err)
    125 	}
    126 
    127 	// Spawn Tor daemon and let it settle
    128 	tor, err := tordam.SpawnTor(tordam.Cfg.Listen, tordam.Cfg.Portmap,
    129 		tordam.Cfg.Datadir)
    130 	defer func() {
    131 		if err := tor.Process.Kill(); err != nil {
    132 			log.Println(err)
    133 		}
    134 	}()
    135 	if err != nil {
    136 		log.Fatal(err)
    137 	}
    138 	time.Sleep(2 * time.Second)
    139 	log.Println("Started Tor daemon on", tordam.Cfg.TorAddr.String())
    140 
    141 	// Read the onion hostname from the datadir and map it into the
    142 	// global tordam.Onion variable
    143 	onionaddr, err := ioutil.ReadFile(
    144 		filepath.Join(tordam.Cfg.Datadir, "hs", "hostname"))
    145 	if err != nil {
    146 		log.Fatal(err)
    147 	}
    148 	onionaddr = []byte(strings.TrimSuffix(string(onionaddr), "\n"))
    149 	tordam.Onion = strings.Join([]string{
    150 		string(onionaddr), fmt.Sprint(tordam.Cfg.Listen.Port)}, ":")
    151 	log.Println("Our onion address is:", tordam.Onion)
    152 
    153 	// Start the JSON-RPC server with announce endpoints.
    154 	// This is done in the program rather than internally in the library
    155 	// because it is more useful and easier to add additional JSON-RPC
    156 	// endpoints to the same server if necessary.
    157 	l, err := net.Listen(jrpc2.Network(tordam.Cfg.Listen.String()),
    158 		tordam.Cfg.Listen.String())
    159 	if err != nil {
    160 		log.Fatal(err)
    161 	}
    162 	defer l.Close()
    163 	// JSON-RPC endpoints are assigned here
    164 	assigner := handler.ServiceMap{
    165 		// "ann" is the JSON-RPC endpoint for peer discovery/announcement
    166 		"ann": handler.NewService(tordam.Ann{}),
    167 	}
    168 	go func() {
    169 		if err := server.Loop(l, server.NewStatic(assigner), nil); err != nil {
    170 			log.Println(err)
    171 		}
    172 	}()
    173 	log.Println("Started JSON-RPC server on", tordam.Cfg.Listen.String())
    174 
    175 	// If decided to not announce to anyone
    176 	if *noannounce {
    177 		// We shall sit here and wait
    178 		wg.Add(1)
    179 		wg.Wait()
    180 	}
    181 
    182 	// Validate given seeds
    183 	for _, i := range strings.Split(*seeds, ",") {
    184 		if err := tordam.ValidateOnionInternal(i); err != nil {
    185 			log.Fatalf("invalid seed %s (%v)", i, err)
    186 		}
    187 	}
    188 
    189 	// Announce to initial seeds
    190 	var succ int = 0 // Track of successful announces
    191 	for _, i := range strings.Split(*seeds, ",") {
    192 		wg.Add(1)
    193 		go func(x string) {
    194 			if err := tordam.Announce(x); err != nil {
    195 				log.Println("error in announce:", err)
    196 			} else {
    197 				succ++
    198 			}
    199 			wg.Done()
    200 		}(i)
    201 	}
    202 	wg.Wait()
    203 
    204 	if succ < 1 {
    205 		log.Println("No successful announces.")
    206 	} else {
    207 		log.Printf("Successfully announced to %d peers.", succ)
    208 	}
    209 
    210 	// Marshal the global Peers map to JSON and print it out.
    211 	j, _ := json.Marshal(tordam.Peers)
    212 	fmt.Println(string(j))
    213 }