tor-dam

tor distributed announce mechanism (not a dht)
git clone https://git.parazyd.org/tor-dam
Log | Files | Refs | README | LICENSE

main.go (9039B)


      1 package main
      2 
      3 /*
      4  * Copyright (c) 2017-2018 Dyne.org Foundation
      5  * tor-dam is written and maintained by Ivan Jelincic <parazyd@dyne.org>
      6  *
      7  * This file is part of tor-dam
      8  *
      9  * This program is free software: you can redistribute it and/or modify
     10  * it under the terms of the GNU Affero General Public License as published by
     11  * the Free Software Foundation, either version 3 of the License, or
     12  * (at your option) any later version.
     13  *
     14  * This program is distributed in the hope that it will be useful,
     15  * but WITHOUT ANY WARRANTY; without even the implied warranty of
     16  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     17  * GNU Affero General Public License for more details.
     18  *
     19  * You should have received a copy of the GNU Affero General Public License
     20  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
     21  */
     22 
     23 import (
     24 	"bufio"
     25 	"bytes"
     26 	"compress/gzip"
     27 	"crypto/rand"
     28 	"encoding/base64"
     29 	"encoding/json"
     30 	"flag"
     31 	"io/ioutil"
     32 	"log"
     33 	"math/big"
     34 	"os"
     35 	"os/exec"
     36 	"strings"
     37 	"sync"
     38 	"time"
     39 
     40 	"golang.org/x/crypto/ed25519"
     41 
     42 	lib "github.com/parazyd/tor-dam/pkg/damlib"
     43 )
     44 
     45 type msgStruct struct {
     46 	Secret string
     47 }
     48 
     49 var (
     50 	noremote    = flag.Bool("noremote", false, "Don't fetch remote entrypoints.")
     51 	gen         = flag.Bool("gen", false, "Only (re)generate keypairs and exit cleanly.")
     52 	annint      = flag.Int("ai", 5, "Announce interval (in minutes)")
     53 	remoteentry = flag.String("remoteentry", "https://dam.decodeproject.eu/dirs.txt", "Remote list of entrypoints. (comma-separated)")
     54 	portmap     = flag.String("portmap", "13010:13010,13011:13011,5000:5000", "Map of ports forwarded to/from Tor.")
     55 )
     56 
     57 func clientInit(gen bool) error {
     58 	pub, priv, err := lib.GenEd25519()
     59 	if err != nil {
     60 		return err
     61 	}
     62 	if err := lib.SavePrivEd25519(lib.PrivKeyPath, priv); err != nil {
     63 		return err
     64 	}
     65 	if err := lib.SaveSeedEd25519(lib.SeedPath, priv.Seed()); err != nil {
     66 		return err
     67 	}
     68 	if err := os.Chmod(lib.PrivKeyPath, 0600); err != nil {
     69 		return err
     70 	}
     71 	if err := os.Chmod(lib.SeedPath, 0600); err != nil {
     72 		return err
     73 	}
     74 	onionaddr := lib.OnionFromPubkeyEd25519(pub)
     75 	if err := ioutil.WriteFile("hostname", onionaddr, 0600); err != nil {
     76 		return err
     77 	}
     78 	if gen {
     79 		log.Println("Our hostname is:", string(onionaddr))
     80 		os.Exit(0)
     81 	}
     82 	return nil
     83 }
     84 
     85 func fetchNodeList(epLists []string, remote bool) ([]string, error) {
     86 	var nodeslice, nodelist []string
     87 
     88 	log.Println("Fetching a list of nodes.")
     89 
     90 	// Remote network entrypoints
     91 	if !(remote) {
     92 		for _, i := range epLists {
     93 			log.Println("Fetching", i)
     94 			n, err := lib.HTTPDownload(i)
     95 			if err != nil {
     96 				return nil, err
     97 			}
     98 			nodeslice = lib.ParseDirs(nodeslice, n)
     99 		}
    100 	}
    101 
    102 	// Local ~/.dam/directories.txt
    103 	if _, err := os.Stat("directories.txt"); err == nil {
    104 		ln, err := ioutil.ReadFile("directories.txt")
    105 		if err != nil {
    106 			return nil, err
    107 		}
    108 		nodeslice = lib.ParseDirs(nodeslice, ln)
    109 	}
    110 
    111 	// Local nodes known to Redis
    112 	nodes, _ := lib.RedisCli.Keys(lib.Rctx, "*.onion").Result()
    113 	for _, i := range nodes {
    114 		valid, err := lib.RedisCli.HGet(lib.Rctx, i, "valid").Result()
    115 		if err != nil {
    116 			// Possible RedisCli bug, possible Redis bug. To be investigated.
    117 			// Sometimes it returns err, but it's nil and does not say what's
    118 			// happening exactly.
    119 			continue
    120 		}
    121 		if valid == "1" {
    122 			nodeslice = append(nodeslice, i)
    123 		}
    124 	}
    125 
    126 	// Remove possible duplicates. Duplicates can cause race conditions and are
    127 	// redundant to the entire logic.
    128 	encounter := map[string]bool{}
    129 	for i := range nodeslice {
    130 		encounter[nodeslice[i]] = true
    131 	}
    132 	nodeslice = []string{}
    133 	for key := range encounter {
    134 		nodeslice = append(nodeslice, key)
    135 	}
    136 
    137 	if len(nodeslice) < 1 {
    138 		log.Fatalln("Couldn't fetch any nodes to announce to. Exiting.")
    139 	} else if len(nodeslice) <= 6 {
    140 		log.Printf("Found only %d nodes.\n", len(nodeslice))
    141 		nodelist = nodeslice
    142 	} else {
    143 		log.Println("Found enough directories. Picking out 6 random ones.")
    144 		for i := 0; i <= 5; i++ {
    145 			n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(nodeslice))))
    146 			nodelist = append(nodelist, nodeslice[n.Int64()])
    147 			nodeslice[n.Int64()] = nodeslice[len(nodeslice)-1]
    148 			nodeslice = nodeslice[:len(nodeslice)-1]
    149 		}
    150 	}
    151 	return nodelist, nil
    152 }
    153 
    154 func announce(node string, vals map[string]string, privkey ed25519.PrivateKey) (bool, error) {
    155 	msg, _ := json.Marshal(vals)
    156 
    157 	log.Println("Announcing keypair to:", node)
    158 	resp, err := lib.HTTPPost("http://"+node+"/announce", msg)
    159 	if err != nil {
    160 		return false, err
    161 	}
    162 
    163 	// Parse server's reply
    164 	var m msgStruct
    165 	decoder := json.NewDecoder(resp.Body)
    166 	if err := decoder.Decode(&m); err != nil {
    167 		return false, err
    168 	}
    169 
    170 	if resp.StatusCode == 400 {
    171 		log.Printf("%s fail. Reply: %s\n", node, m.Secret)
    172 		return false, nil
    173 	}
    174 
    175 	if resp.StatusCode == 200 {
    176 		log.Printf("%s success. 1/2 handshake valid.", node)
    177 
    178 		sig, err := lib.SignMsgEd25519([]byte(m.Secret), privkey)
    179 		if err != nil {
    180 			return false, err
    181 		}
    182 		encodedSig := base64.StdEncoding.EncodeToString(sig)
    183 
    184 		vals["secret"] = m.Secret
    185 		vals["message"] = m.Secret
    186 		vals["signature"] = encodedSig
    187 
    188 		msg, _ := json.Marshal(vals)
    189 
    190 		log.Printf("%s: success. Sending back signed secret.\n", node)
    191 		resp, err := lib.HTTPPost("http://"+node+"/announce", msg)
    192 		if err != nil {
    193 			return false, err
    194 		}
    195 		decoder = json.NewDecoder(resp.Body)
    196 		if err := decoder.Decode(&m); err != nil {
    197 			return false, err
    198 		}
    199 
    200 		if resp.StatusCode == 200 {
    201 			log.Printf("%s success. 2/2 handshake valid.\n", node)
    202 			data, err := base64.StdEncoding.DecodeString(m.Secret)
    203 			if err != nil {
    204 				// Not a list of nodes.
    205 				log.Printf("%s replied: %s\n", node, m.Secret)
    206 				return true, nil
    207 			}
    208 			log.Println("Got node data. Processing...")
    209 			b := bytes.NewReader(data)
    210 			r, _ := gzip.NewReader(b)
    211 			nodes := make(map[string]map[string]interface{})
    212 			decoder = json.NewDecoder(r)
    213 			if err = decoder.Decode(&nodes); err != nil {
    214 				return false, err
    215 			}
    216 			for k, v := range nodes {
    217 				log.Printf("Adding %s to Redis.\n", k)
    218 				_, err = lib.RedisCli.HMSet(lib.Rctx, k, v).Result()
    219 				lib.CheckError(err)
    220 			}
    221 			return true, nil
    222 		}
    223 		log.Printf("%s fail. Reply: %s\n", node, m.Secret)
    224 		return false, nil
    225 	}
    226 
    227 	return false, nil
    228 }
    229 
    230 func main() {
    231 	flag.Parse()
    232 
    233 	// Network entrypoints. These files hold the lists of nodes we can announce
    234 	// to initially. Format is "DIR:unlikelynamefora.onion", other lines are
    235 	// ignored and can be used as comments or similar.
    236 	epLists := strings.Split(*remoteentry, ",")
    237 
    238 	if _, err := os.Stat(lib.Workdir); os.IsNotExist(err) {
    239 		err := os.Mkdir(lib.Workdir, 0700)
    240 		lib.CheckError(err)
    241 	}
    242 	err := os.Chdir(lib.Workdir)
    243 	lib.CheckError(err)
    244 
    245 	if _, err = os.Stat(lib.PrivKeyPath); os.IsNotExist(err) || *gen {
    246 		err = clientInit(*gen)
    247 		lib.CheckError(err)
    248 	}
    249 
    250 	// Map it to the flag
    251 	lib.TorPortMap = "80:49371," + *portmap
    252 
    253 	log.Println("Starting up the hidden service.")
    254 	cmd := exec.Command("damhs.py", "-k", lib.PrivKeyPath, "-p", lib.TorPortMap)
    255 	defer cmd.Process.Kill()
    256 	stdout, err := cmd.StdoutPipe()
    257 	lib.CheckError(err)
    258 
    259 	err = cmd.Start()
    260 	lib.CheckError(err)
    261 
    262 	scanner := bufio.NewScanner(stdout)
    263 	ok := false
    264 	go func() {
    265 		// If we do not manage to publish our descriptor, we shall exit.
    266 		t1 := time.Now().Unix()
    267 		for !(ok) {
    268 			t2 := time.Now().Unix()
    269 			if t2-t1 > 90 {
    270 				log.Fatalln("Too much time has passed for publishing descriptor.")
    271 			}
    272 			time.Sleep(1000 * time.Millisecond)
    273 		}
    274 	}()
    275 	for !(ok) {
    276 		scanner.Scan()
    277 		status := scanner.Text()
    278 		if status == "OK" {
    279 			log.Println("Hidden service is now running.")
    280 			ok = true
    281 		}
    282 	}
    283 
    284 	onionaddr, err := ioutil.ReadFile("hostname")
    285 	lib.CheckError(err)
    286 	log.Println("Our hostname is:", string(onionaddr))
    287 
    288 	for {
    289 		log.Println("Announcing to nodes...")
    290 		var ann = 0 // Track of successful authentications.
    291 		var wg sync.WaitGroup
    292 		nodes, err := fetchNodeList(epLists, *noremote)
    293 		if err != nil {
    294 			// No route to host, or failed download. Try later.
    295 			log.Println("Failed to fetch any nodes. Retrying in a minute.")
    296 			time.Sleep(60 * time.Second)
    297 			continue
    298 		}
    299 
    300 		privkey, err := lib.LoadEd25519KeyFromSeed(lib.SeedPath)
    301 		lib.CheckError(err)
    302 
    303 		pubkey := privkey.Public().(ed25519.PublicKey)
    304 		onionaddr := lib.OnionFromPubkeyEd25519(pubkey)
    305 		encodedPub := base64.StdEncoding.EncodeToString([]byte(pubkey))
    306 
    307 		sig, err := lib.SignMsgEd25519([]byte(lib.PostMsg), privkey)
    308 		lib.CheckError(err)
    309 		encodedSig := base64.StdEncoding.EncodeToString(sig)
    310 
    311 		nodevals := map[string]string{
    312 			"address":   string(onionaddr),
    313 			"pubkey":    encodedPub,
    314 			"message":   lib.PostMsg,
    315 			"signature": encodedSig,
    316 			"secret":    "",
    317 		}
    318 
    319 		for _, i := range nodes {
    320 			wg.Add(1)
    321 			go func(x string) {
    322 				valid, err := announce(x, nodevals, privkey)
    323 				if err != nil {
    324 					log.Printf("%s: %s\n", x, err.Error())
    325 				}
    326 				if valid {
    327 					ann++
    328 				}
    329 				wg.Done()
    330 			}(i)
    331 		}
    332 		wg.Wait()
    333 
    334 		log.Printf("%d successful authentications.\n", ann)
    335 		log.Printf("Waiting %d min before next announce.\n", *annint)
    336 		time.Sleep(time.Duration(*annint) * time.Minute)
    337 	}
    338 }