commit 43142fdce9dc92158eddc8a11c92fd31ff74d329
parent 573769406a8be94de602fc1c7e38a8dc24991503
Author: parazyd <parazyd@dyne.org>
Date: Mon, 11 Jan 2021 16:09:43 +0100
Refactor repository and rewrite some parts of the code.
tor-dam is now a single binary, without the external python dependency.
When running, it will spawn a new Tor instance, and a new redis-server
instance. Their info can be retrieved with netstat(8).
The handshake logic now only checks the signature in 2/2, as the signing
in 1/2 was redundant and unnecessary.
Have fun.
Diffstat:
39 files changed, 1379 insertions(+), 2028 deletions(-)
diff --git a/Makefile b/Makefile
@@ -1,16 +0,0 @@
-# See LICENSE file for copyright and license details.
-
-PREFIX ?= /usr/local
-
-all:
- @echo 'Run "make install" to install into $(DESTDIR)$(PREFIX)'
-
-install:
- @make -C python install
- @make -C contrib install install-init
-
-uninstall:
- @make -C python uninstall
- @make -C contrib uninstall
-
-.PHONY: all install uninstall
diff --git a/README.md b/README.md
@@ -1,74 +1,35 @@
-Tor Distributed Announce Mechanism (Tor DAM)
+tor-dam (Tor Distributed Announce Mechanism)
============================================
Protocol and tooling for mapping machines in the Tor network running
this software.
-[![GoDoc](https://godoc.org/github.com/parazyd/tor-dam?status.svg)](https://godoc.org/github.com/parazyd/tor-dam)
-
![Network visualization](https://raw.githubusercontent.com/parazyd/tor-dam/master/contrib/network.gif)
+
Installation
------------
```
-go get -u github.com/parazyd/tor-dam/...
-```
-
-### Dependencies
-
-#### Go
-
-```
-golang.org/x/crypto/ed25519
-golang.org/x/crypto/sha3
-golang.org/x/net/proxy
-github.com/go-redis/redis
-```
-
-#### Python 3
-
-```
-https://stem.torproject.org/
-```
-
-The Go dependencies should be pulled in with `go get`. You can install
-`stem` possibly with your package manager, or download it from the
-website itself. `stem` needs to be at least version `1.7.0`.
-
-To install everything else, go to the directory where go has downloaded
-tor-dam and run `make install` as root.
-
-External software dependencies include `redis` and `tor`. You can
-retrieve them using your package manager. Tor has to be at least version
-`0.3`, to support V3 hidden services.
-
-Tor needs to have ControlPort enabled, and has to allow either
-CookieAuthentication or a password, for stem to authenticate and be able
-to create hidden services and retrieve hidden service descriptors.
-
-Redis is our storage backend where information about nodes is held.
-
-Working configurations are provided in the `contrib` directory.
-
-
-### Operation example(s)
-
-By default, ports 13010:13010,13011:13011,5000:5000 are mapped by
-tor-dam. (see: tor-dam/pkg/damlib/config.go:48)
-
-To serve a basic echo server behind this, issue the following on the
-recipient side:
-
-```
-nc -l 5000
-```
-
-and the following on the sender's side:
-
-```
-echo 'HELLO' | torsocks nc <address.onion> 5000
-```
-
-You can find the onion address either in redis, or in the `.dam`
-directory.
+go get github.com/parazyd/tor-dam
+```
+
+Protocol
+--------
+
+* Every node has an HTTP API allowing to list other nodes and announce
+ new ones.
+* They keep propagating to all trusted nodes they know.
+* Announcing implies the need of knowledge of at least one node.
+ * It is possible to make this random enough once there are at least
+ 6 nodes in the network.
+* A node announces itself to others by sending a JSON-formatted HTTP
+ POST request to one or more active nodes.
+ * Once the initial POST request is received, the receiving node will
+ ACK and return a random string (nonce) back to the requester for
+ them to sign with their cryptographic key.
+ * The requester will try to sign this nonce and return it back to
+ the node it's announcing to, so the node can confirm the requester
+ is in actual posession of the private key.
+* tor-dam **does not validate** if a node should be trusted or not.
+ This is a layer that has to be implemented with external software.
diff --git a/TODO.md b/TODO.md
@@ -1,2 +0,0 @@
-* Network tags, part of network A, part of network B
- * keep it in redis
diff --git a/announce.go b/announce.go
@@ -0,0 +1,179 @@
+package main
+
+/*
+ * Copyright (c) 2017-2021 Ivan Jelincic <parazyd@dyne.org>
+ *
+ * This file is part of tor-dam
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+import (
+ "bytes"
+ "compress/gzip"
+ "crypto/ed25519"
+ "crypto/rand"
+ "encoding/base64"
+ "encoding/json"
+ "io/ioutil"
+ "log"
+ "math/big"
+ "os"
+ "strings"
+)
+
+func fetchNodeList(epLists []string, remote bool) ([]string, error) {
+ var ns, nl []string
+
+ log.Println("Building a list of nodes")
+
+ // Remote network entrypoints
+ if !remote {
+ for _, i := range epLists {
+ log.Println("Fetching", i)
+ n, err := httpGet(i)
+ if err != nil {
+ return nil, err
+ }
+ ns = parseDirs(ns, n)
+ }
+ }
+
+ // Local workdir/dirs.txt
+ ld := strings.Join([]string{*workdir, "dirs.txt"}, "/")
+ if _, err := os.Stat(ld); err == nil {
+ ln, err := ioutil.ReadFile(ld)
+ if err != nil {
+ return nil, err
+ }
+ ns = parseDirs(ns, ln)
+ }
+
+ // Local nodes from redis
+ nodes, _ := rcli.Keys(rctx, "*.onion").Result()
+ for _, i := range nodes {
+ valid, err := rcli.HGet(rctx, i, "valid").Result()
+ if err != nil {
+ // Possible RedisCli bug, possible Redis bug. To be investigated.
+ // Sometimes it returns err, but it's empty and does not say what's
+ // happening exactly.
+ continue
+ }
+ if valid == "1" {
+ ns = append(ns, i)
+ }
+ }
+
+ // Remove possible dupes. Duplicates can cause race conditions and are
+ // redundant to the entire logic.
+ // TODO: Work this in above automatically (by changing the var type)
+ encounter := map[string]bool{}
+ for i := range ns {
+ encounter[ns[i]] = true
+ }
+ ns = []string{}
+ for key := range encounter {
+ ns = append(ns, key)
+ }
+
+ if len(ns) < 1 {
+ log.Fatal("Couldn't find any nodes to announce to. Exiting...")
+ } else if len(ns) <= 6 {
+ log.Printf("Found %d nodes\n", len(ns))
+ nl = ns
+ } else {
+ log.Printf("Found %d nodes. Picking out 6 at random\n", len(ns))
+ for i := 0; i <= 5; i++ {
+ n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(ns))))
+ nl = append(nl, ns[n.Int64()])
+ ns[n.Int64()] = ns[len(ns)-1]
+ ns = ns[:len(ns)-1]
+ }
+ }
+
+ return nl, nil
+}
+
+func announce(addr string, vals map[string]string) (bool, error) {
+ msg, _ := json.Marshal(vals)
+
+ log.Println("Announcing keypair to", addr)
+ resp, err := httpPost("http://"+addr+":49371"+"/announce", msg)
+ if err != nil {
+ return false, err
+ }
+
+ // Parse server's reply
+ var m Message
+ dec := json.NewDecoder(resp.Body)
+ if err := dec.Decode(&m); err != nil {
+ return false, err
+ }
+
+ if resp.StatusCode != 200 {
+ log.Printf("%s returned error: %s\n", addr, m.Secret)
+ return false, nil
+ }
+
+ log.Println("Got nonce from", addr)
+
+ sig := ed25519.Sign(signingKey, []byte(m.Secret))
+
+ vals["secret"] = m.Secret
+ vals["message"] = m.Secret
+ vals["signature"] = base64.StdEncoding.EncodeToString(sig)
+ msg, _ = json.Marshal(vals)
+
+ log.Println("Sending back signed secret to", addr)
+ resp, err = httpPost("http://"+addr+":49371"+"/announce", msg)
+ if err != nil {
+ return false, err
+ }
+
+ dec = json.NewDecoder(resp.Body)
+ if err := dec.Decode(&m); err != nil {
+ return false, err
+ }
+
+ if resp.StatusCode != 200 {
+ log.Printf("%s returned error: %s\n", addr, m.Secret)
+ return false, nil
+ }
+
+ log.Printf("%s handshake valid\n", addr)
+ data, err := base64.StdEncoding.DecodeString(m.Secret)
+ if err != nil {
+ // Not a list of nodes
+ log.Printf("%s replied: %s\n", addr, m.Secret)
+ return true, nil
+ }
+
+ log.Println("Got node data, processing...")
+ b := bytes.NewReader(data)
+ r, _ := gzip.NewReader(b)
+ nodes := make(map[string]map[string]interface{})
+ dec = json.NewDecoder(r)
+ if err = dec.Decode(&nodes); err != nil {
+ return false, err
+ }
+
+ for k, v := range nodes {
+ log.Printf("Adding %s to redis\n", k)
+ if _, err := rcli.HSet(rctx, k, v).Result(); err != nil {
+ log.Fatal(err)
+ }
+ }
+
+ return true, nil
+}
diff --git a/api.go b/api.go
@@ -0,0 +1,191 @@
+package main
+
+/*
+ * Copyright (c) 2017-2021 Ivan Jelincic <parazyd@dyne.org>
+ *
+ * This file is part of tor-dam
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+import (
+ "encoding/json"
+ "log"
+ "net/http"
+ "strings"
+)
+
+func postback(rw http.ResponseWriter, data map[string]string, ret int) error {
+ val, err := json.Marshal(data)
+ if err != nil {
+ return err
+ }
+
+ rw.Header().Set("Content-Type", "application/json")
+ rw.WriteHeader(ret)
+ if _, err := rw.Write(val); err != nil {
+ return err
+ }
+ return nil
+}
+
+func handleAnnounce(rw http.ResponseWriter, req *http.Request) {
+ var r map[string]string
+ var n Node
+
+ if req.Method != "POST" || req.Header["Content-Type"][0] != "application/json" {
+ r = map[string]string{"secret": "Invalid request format"}
+ if err := postback(rw, r, 400); err != nil {
+ log.Fatal(err)
+ }
+ return
+ }
+
+ dec := json.NewDecoder(req.Body)
+ if err := dec.Decode(&n); err != nil {
+ log.Println("Failed decoding request:", err)
+ return
+ }
+
+ // Bail out as soon as possible
+ if len(n.Address) == 0 || len(n.Message) == 0 || len(n.Signature) == 0 {
+ r = map[string]string{"secret": "Invalid request format"}
+ if err := postback(rw, r, 400); err != nil {
+ log.Fatal(err)
+ }
+ return
+ }
+
+ if !validateOnionAddress(n.Address) {
+ log.Println("Invalid onion address:", n.Address)
+ r = map[string]string{"secret": "Invalid onion address"}
+ if err := postback(rw, r, 400); err != nil {
+ log.Fatal(err)
+ }
+ return
+ }
+
+ rq := map[string]string{
+ "address": n.Address,
+ "message": n.Message,
+ "pubkey": n.Pubkey,
+ "signature": n.Signature,
+ "secret": n.Secret,
+ }
+
+ // First handshake
+ if len(n.Message) != 88 || len(n.Secret) != 88 {
+ valid, msg := firstHandshake(rq)
+ r = map[string]string{"secret": msg}
+ if valid {
+ log.Printf("%s: 1/2 handskake valid\n", n.Address)
+ log.Println("Sending nonce to", n.Address)
+ if err := postback(rw, r, 200); err != nil {
+ log.Fatal(err)
+ }
+ return
+ }
+ log.Printf("%s: 1/2 handshake invalid: %s\n", n.Address, msg)
+ // Delete it all from redis
+ // TODO: Can this be abused?
+ if _, err := rcli.Del(rctx, n.Address).Result(); err != nil {
+ log.Fatal(err)
+ }
+ return
+ }
+
+ // Second handshake
+ if len(rq["secret"]) == 88 && len(rq["message"]) == 88 {
+ valid, msg := secondHandshake(rq)
+ r = map[string]string{"secret": msg}
+
+ if valid {
+ log.Printf("%s: 2/2 handshake valid\n", n.Address)
+ isTrusted, err := rcli.HGet(rctx, n.Address, "trusted").Result()
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ // Assume our name is what was requested
+ us := strings.TrimSuffix(req.Host, ":49371")
+ nodemap := make(map[string]map[string]string)
+
+ if isTrusted == "1" {
+ // The node is marked as trusted so we'll teack it about other
+ // trusted nodes we know about.
+ log.Printf("%s is trusted. Propagating knowledge...\n", n.Address)
+ nodes, err := rcli.Keys(rctx, "*.onion").Result()
+ if err != nil {
+ log.Fatal(err)
+ }
+ for _, i := range nodes {
+ if i == n.Address {
+ continue
+ }
+ nodedata, err := rcli.HGetAll(rctx, i).Result()
+ if err != nil {
+ log.Fatal(err)
+ }
+ if nodedata["trusted"] == "1" {
+ nodemap[i] = nodedata
+ delete(nodemap[i], "secret")
+ }
+ }
+ } else {
+ log.Printf("%s is not trusted. Propagating self...", n.Address)
+ // The node doesn't have trust in the network. We will only
+ // teach it about ourself.
+ nodedata, err := rcli.HGetAll(rctx, us).Result()
+ if err != nil {
+ log.Fatal(err)
+ }
+ nodemap[us] = nodedata
+ delete(nodemap[us], "secret")
+ }
+
+ nodestr, err := json.Marshal(nodemap)
+ if err != nil {
+ log.Fatal(err)
+ }
+ comp, err := gzipEncode(nodestr)
+ if err != nil {
+ log.Fatal(err)
+ }
+ r = map[string]string{"secret": comp}
+ if err := postback(rw, r, 200); err != nil {
+ log.Fatal(err)
+ }
+
+ publishToRedis('M', n.Address)
+ return
+ }
+
+ // If we haven't returned so far, the handshake is invalid
+ log.Printf("%s: 2/2 handshake invalid\n", n.Address)
+ // Delete it all from redis
+ // TODO: Can this be abused?
+ publishToRedis('D', n.Address)
+ if _, err := rcli.Del(rctx, n.Address).Result(); err != nil {
+ log.Fatal(err)
+ }
+ if err := postback(rw, r, 400); err != nil {
+ log.Fatal(err)
+ }
+ return
+ }
+}
+
+func handleElse(rw http.ResponseWriter, req *http.Request) {
+ log.Println("Got handleElse")
+}
diff --git a/cmd/dam-client/main.go b/cmd/dam-client/main.go
@@ -1,338 +0,0 @@
-package main
-
-/*
- * Copyright (c) 2017-2018 Dyne.org Foundation
- * tor-dam is written and maintained by Ivan Jelincic <parazyd@dyne.org>
- *
- * This file is part of tor-dam
- *
- * 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 <http://www.gnu.org/licenses/>.
- */
-
-import (
- "bufio"
- "bytes"
- "compress/gzip"
- "crypto/rand"
- "encoding/base64"
- "encoding/json"
- "flag"
- "io/ioutil"
- "log"
- "math/big"
- "os"
- "os/exec"
- "strings"
- "sync"
- "time"
-
- "golang.org/x/crypto/ed25519"
-
- lib "github.com/parazyd/tor-dam/pkg/damlib"
-)
-
-type msgStruct struct {
- Secret string
-}
-
-var (
- noremote = flag.Bool("noremote", false, "Don't fetch remote entrypoints.")
- gen = flag.Bool("gen", false, "Only (re)generate keypairs and exit cleanly.")
- annint = flag.Int("ai", 5, "Announce interval (in minutes)")
- remoteentry = flag.String("remoteentry", "https://dam.decodeproject.eu/dirs.txt", "Remote list of entrypoints. (comma-separated)")
- portmap = flag.String("portmap", "13010:13010,13011:13011,5000:5000", "Map of ports forwarded to/from Tor.")
-)
-
-func clientInit(gen bool) error {
- pub, priv, err := lib.GenEd25519()
- if err != nil {
- return err
- }
- if err := lib.SavePrivEd25519(lib.PrivKeyPath, priv); err != nil {
- return err
- }
- if err := lib.SaveSeedEd25519(lib.SeedPath, priv.Seed()); err != nil {
- return err
- }
- if err := os.Chmod(lib.PrivKeyPath, 0600); err != nil {
- return err
- }
- if err := os.Chmod(lib.SeedPath, 0600); err != nil {
- return err
- }
- onionaddr := lib.OnionFromPubkeyEd25519(pub)
- if err := ioutil.WriteFile("hostname", onionaddr, 0600); err != nil {
- return err
- }
- if gen {
- log.Println("Our hostname is:", string(onionaddr))
- os.Exit(0)
- }
- return nil
-}
-
-func fetchNodeList(epLists []string, remote bool) ([]string, error) {
- var nodeslice, nodelist []string
-
- log.Println("Fetching a list of nodes.")
-
- // Remote network entrypoints
- if !(remote) {
- for _, i := range epLists {
- log.Println("Fetching", i)
- n, err := lib.HTTPDownload(i)
- if err != nil {
- return nil, err
- }
- nodeslice = lib.ParseDirs(nodeslice, n)
- }
- }
-
- // Local ~/.dam/directories.txt
- if _, err := os.Stat("directories.txt"); err == nil {
- ln, err := ioutil.ReadFile("directories.txt")
- if err != nil {
- return nil, err
- }
- nodeslice = lib.ParseDirs(nodeslice, ln)
- }
-
- // Local nodes known to Redis
- nodes, _ := lib.RedisCli.Keys(lib.Rctx, "*.onion").Result()
- for _, i := range nodes {
- valid, err := lib.RedisCli.HGet(lib.Rctx, i, "valid").Result()
- if err != nil {
- // Possible RedisCli bug, possible Redis bug. To be investigated.
- // Sometimes it returns err, but it's nil and does not say what's
- // happening exactly.
- continue
- }
- if valid == "1" {
- nodeslice = append(nodeslice, i)
- }
- }
-
- // Remove possible duplicates. Duplicates can cause race conditions and are
- // redundant to the entire logic.
- encounter := map[string]bool{}
- for i := range nodeslice {
- encounter[nodeslice[i]] = true
- }
- nodeslice = []string{}
- for key := range encounter {
- nodeslice = append(nodeslice, key)
- }
-
- if len(nodeslice) < 1 {
- log.Fatalln("Couldn't fetch any nodes to announce to. Exiting.")
- } else if len(nodeslice) <= 6 {
- log.Printf("Found only %d nodes.\n", len(nodeslice))
- nodelist = nodeslice
- } else {
- log.Println("Found enough directories. Picking out 6 random ones.")
- for i := 0; i <= 5; i++ {
- n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(nodeslice))))
- nodelist = append(nodelist, nodeslice[n.Int64()])
- nodeslice[n.Int64()] = nodeslice[len(nodeslice)-1]
- nodeslice = nodeslice[:len(nodeslice)-1]
- }
- }
- return nodelist, nil
-}
-
-func announce(node string, vals map[string]string, privkey ed25519.PrivateKey) (bool, error) {
- msg, _ := json.Marshal(vals)
-
- log.Println("Announcing keypair to:", node)
- resp, err := lib.HTTPPost("http://"+node+"/announce", msg)
- if err != nil {
- return false, err
- }
-
- // Parse server's reply
- var m msgStruct
- decoder := json.NewDecoder(resp.Body)
- if err := decoder.Decode(&m); err != nil {
- return false, err
- }
-
- if resp.StatusCode == 400 {
- log.Printf("%s fail. Reply: %s\n", node, m.Secret)
- return false, nil
- }
-
- if resp.StatusCode == 200 {
- log.Printf("%s success. 1/2 handshake valid.", node)
-
- sig, err := lib.SignMsgEd25519([]byte(m.Secret), privkey)
- if err != nil {
- return false, err
- }
- encodedSig := base64.StdEncoding.EncodeToString(sig)
-
- vals["secret"] = m.Secret
- vals["message"] = m.Secret
- vals["signature"] = encodedSig
-
- msg, _ := json.Marshal(vals)
-
- log.Printf("%s: success. Sending back signed secret.\n", node)
- resp, err := lib.HTTPPost("http://"+node+"/announce", msg)
- if err != nil {
- return false, err
- }
- decoder = json.NewDecoder(resp.Body)
- if err := decoder.Decode(&m); err != nil {
- return false, err
- }
-
- if resp.StatusCode == 200 {
- log.Printf("%s success. 2/2 handshake valid.\n", node)
- data, err := base64.StdEncoding.DecodeString(m.Secret)
- if err != nil {
- // Not a list of nodes.
- log.Printf("%s replied: %s\n", node, m.Secret)
- return true, nil
- }
- log.Println("Got node data. Processing...")
- b := bytes.NewReader(data)
- r, _ := gzip.NewReader(b)
- nodes := make(map[string]map[string]interface{})
- decoder = json.NewDecoder(r)
- if err = decoder.Decode(&nodes); err != nil {
- return false, err
- }
- for k, v := range nodes {
- log.Printf("Adding %s to Redis.\n", k)
- _, err = lib.RedisCli.HMSet(lib.Rctx, k, v).Result()
- lib.CheckError(err)
- }
- return true, nil
- }
- log.Printf("%s fail. Reply: %s\n", node, m.Secret)
- return false, nil
- }
-
- return false, nil
-}
-
-func main() {
- flag.Parse()
-
- // Network entrypoints. These files hold the lists of nodes we can announce
- // to initially. Format is "DIR:unlikelynamefora.onion", other lines are
- // ignored and can be used as comments or similar.
- epLists := strings.Split(*remoteentry, ",")
-
- if _, err := os.Stat(lib.Workdir); os.IsNotExist(err) {
- err := os.Mkdir(lib.Workdir, 0700)
- lib.CheckError(err)
- }
- err := os.Chdir(lib.Workdir)
- lib.CheckError(err)
-
- if _, err = os.Stat(lib.PrivKeyPath); os.IsNotExist(err) || *gen {
- err = clientInit(*gen)
- lib.CheckError(err)
- }
-
- // Map it to the flag
- lib.TorPortMap = "80:49371," + *portmap
-
- log.Println("Starting up the hidden service.")
- cmd := exec.Command("damhs.py", "-k", lib.PrivKeyPath, "-p", lib.TorPortMap)
- defer cmd.Process.Kill()
- stdout, err := cmd.StdoutPipe()
- lib.CheckError(err)
-
- err = cmd.Start()
- lib.CheckError(err)
-
- scanner := bufio.NewScanner(stdout)
- ok := false
- go func() {
- // If we do not manage to publish our descriptor, we shall exit.
- t1 := time.Now().Unix()
- for !(ok) {
- t2 := time.Now().Unix()
- if t2-t1 > 90 {
- log.Fatalln("Too much time has passed for publishing descriptor.")
- }
- time.Sleep(1000 * time.Millisecond)
- }
- }()
- for !(ok) {
- scanner.Scan()
- status := scanner.Text()
- if status == "OK" {
- log.Println("Hidden service is now running.")
- ok = true
- }
- }
-
- onionaddr, err := ioutil.ReadFile("hostname")
- lib.CheckError(err)
- log.Println("Our hostname is:", string(onionaddr))
-
- for {
- log.Println("Announcing to nodes...")
- var ann = 0 // Track of successful authentications.
- var wg sync.WaitGroup
- nodes, err := fetchNodeList(epLists, *noremote)
- if err != nil {
- // No route to host, or failed download. Try later.
- log.Println("Failed to fetch any nodes. Retrying in a minute.")
- time.Sleep(60 * time.Second)
- continue
- }
-
- privkey, err := lib.LoadEd25519KeyFromSeed(lib.SeedPath)
- lib.CheckError(err)
-
- pubkey := privkey.Public().(ed25519.PublicKey)
- onionaddr := lib.OnionFromPubkeyEd25519(pubkey)
- encodedPub := base64.StdEncoding.EncodeToString([]byte(pubkey))
-
- sig, err := lib.SignMsgEd25519([]byte(lib.PostMsg), privkey)
- lib.CheckError(err)
- encodedSig := base64.StdEncoding.EncodeToString(sig)
-
- nodevals := map[string]string{
- "address": string(onionaddr),
- "pubkey": encodedPub,
- "message": lib.PostMsg,
- "signature": encodedSig,
- "secret": "",
- }
-
- for _, i := range nodes {
- wg.Add(1)
- go func(x string) {
- valid, err := announce(x, nodevals, privkey)
- if err != nil {
- log.Printf("%s: %s\n", x, err.Error())
- }
- if valid {
- ann++
- }
- wg.Done()
- }(i)
- }
- wg.Wait()
-
- log.Printf("%d successful authentications.\n", ann)
- log.Printf("Waiting %d min before next announce.\n", *annint)
- time.Sleep(time.Duration(*annint) * time.Minute)
- }
-}
diff --git a/cmd/dam-dir/main.go b/cmd/dam-dir/main.go
@@ -1,273 +0,0 @@
-package main
-
-/*
- * Copyright (c) 2017-2018 Dyne.org Foundation
- * tor-dam is written and maintained by Ivan Jelincic <parazyd@dyne.org>
- *
- * This file is part of tor-dam
- *
- * 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 <http://www.gnu.org/licenses/>.
- */
-
-import (
- "encoding/json"
- "flag"
- "log"
- "net/http"
- "os"
- "strconv"
- "sync"
- "time"
-
- lib "github.com/parazyd/tor-dam/pkg/damlib"
-)
-
-// ListenAddress controls where our HTTP API daemon is listening.
-const ListenAddress = "127.0.0.1:49371"
-
-type nodeStruct struct {
- Address string
- Message string
- Signature string
- Secret string
- Pubkey string
- Firstseen int64
- Lastseen int64
- Valid int64
-}
-
-var (
- testnet = flag.Bool("t", false, "Mark all new nodes valid initially.")
- ttl = flag.Int64("ttl", 0, "Set expiry time in minutes (TTL) for nodes.")
- redconf = flag.String("redconf", "/usr/local/share/tor-dam/redis.conf", "Path to redis' redis.conf.")
-)
-
-func postback(rw http.ResponseWriter, data map[string]string, retCode int) error {
- jsonVal, err := json.Marshal(data)
- if err != nil {
- return err
- }
- rw.Header().Set("Content-Type", "application/json")
- rw.WriteHeader(retCode)
- rw.Write(jsonVal)
- return nil
-}
-
-func handlePost(rw http.ResponseWriter, request *http.Request) {
- var ret map[string]string
- var n nodeStruct
-
- if request.Method != "POST" || request.Header["Content-Type"][0] != "application/json" {
- ret = map[string]string{"secret": "Invalid request format."}
- if err := postback(rw, ret, 400); err != nil {
- lib.CheckError(err)
- }
- return
- }
-
- decoder := json.NewDecoder(request.Body)
- if err := decoder.Decode(&n); err != nil {
- log.Println("Failed decoding request:", err)
- return
- }
-
- // Bail out as soon as possible.
- if len(n.Address) == 0 || len(n.Message) == 0 || len(n.Signature) == 0 {
- ret = map[string]string{"secret": "Invalid request format."}
- if err := postback(rw, ret, 400); err != nil {
- lib.CheckError(err)
- }
- return
- }
- if !(lib.ValidateOnionAddress(n.Address)) {
- log.Println("Invalid onion address. Got:", n.Address)
- ret = map[string]string{"secret": "Invalid onion address."}
- if err := postback(rw, ret, 400); err != nil {
- lib.CheckError(err)
- }
- return
- }
-
- req := map[string]string{
- "address": n.Address,
- "message": n.Message,
- "pubkey": n.Pubkey,
- "signature": n.Signature,
- "secret": n.Secret,
- }
-
- // First handshake
- if len(n.Message) != 88 && len(n.Secret) != 88 {
- valid, msg := lib.ValidateFirstHandshake(req)
- ret = map[string]string{"secret": msg}
- if valid {
- log.Printf("%s: 1/2 handshake valid.\n", n.Address)
- log.Println("Sending nonce.")
- if err := postback(rw, ret, 200); err != nil {
- lib.CheckError(err)
- }
- return
- }
- log.Printf("%s: 1/2 handshake invalid: %s\n", n.Address, msg)
- // Delete it all from redis.
- _, err := lib.RedisCli.Del(lib.Rctx, n.Address).Result()
- lib.CheckError(err)
- if err := postback(rw, ret, 400); err != nil {
- lib.CheckError(err)
- }
- return
- }
-
- // Second handshake
- if len(req["secret"]) == 88 && len(req["message"]) == 88 {
- valid, msg := lib.ValidateSecondHandshake(req)
- ret = map[string]string{"secret": msg}
-
- if valid {
- log.Printf("%s: 2/2 handshake valid.\n", n.Address)
- hasConsensus, err := lib.RedisCli.HGet(lib.Rctx, n.Address, "valid").Result()
- lib.CheckError(err)
-
- us := request.Host // Assume our name is what was requested as the URL.
- nodemap := make(map[string]map[string]string)
-
- if hasConsensus == "1" {
- // The node does have consensus, we'll teach it about the valid
- // nodes we know.
- log.Printf("%s has consensus. Propagating our nodes to it...\n", n.Address)
- nodes, err := lib.RedisCli.Keys(lib.Rctx, "*.onion").Result()
- lib.CheckError(err)
- for _, i := range nodes {
- if i == n.Address {
- continue
- }
- nodedata, err := lib.RedisCli.HGetAll(lib.Rctx, i).Result()
- lib.CheckError(err)
- if nodedata["valid"] == "1" {
- nodemap[i] = nodedata
- delete(nodemap[i], "secret")
- }
- }
- } else {
- log.Printf("%s does not have consensus. Propagating ourself to it...\n", n.Address)
- // The node doesn't have consensus in the network. We will only
- // teach it about ourself.
- nodedata, err := lib.RedisCli.HGetAll(lib.Rctx, us).Result()
- lib.CheckError(err)
- nodemap[us] = nodedata
- delete(nodemap[us], "secret")
- }
-
- nodestr, err := json.Marshal(nodemap)
- lib.CheckError(err)
- comp, err := lib.GzipEncode(nodestr)
- lib.CheckError(err)
- ret = map[string]string{"secret": comp}
- if err := postback(rw, ret, 200); err != nil {
- lib.CheckError(err)
- }
-
- lib.PublishToRedis("am", n.Address)
-
- return
- }
-
- // If we have't returned so far, the handshake is invalid.
- log.Printf("%s: 2/2 handshake invalid.\n", n.Address)
- // Delete it all from redis.
- lib.PublishToRedis("d", n.Address)
- _, err := lib.RedisCli.Del(lib.Rctx, n.Address).Result()
- lib.CheckError(err)
- if err := postback(rw, ret, 400); err != nil {
- lib.CheckError(err)
- }
- return
- }
-}
-
-func pollNodeTTL(interval int64) {
- for {
- log.Println("Polling redis for expired nodes")
- nodes, err := lib.RedisCli.Keys(lib.Rctx, "*.onion").Result()
- lib.CheckError(err)
- now := time.Now().Unix()
-
- for _, i := range nodes {
- res, err := lib.RedisCli.HGet(lib.Rctx, i, "lastseen").Result()
- lib.CheckError(err)
- lastseen, err := strconv.Atoi(res)
- lib.CheckError(err)
-
- diff := (now - int64(lastseen)) / 60
- if diff > interval {
- log.Printf("Deleting %s from redis because of expiration\n", i)
- lib.PublishToRedis("d", i)
- lib.RedisCli.Del(lib.Rctx, i)
- }
- }
- time.Sleep(time.Duration(interval) * time.Minute)
- }
-}
-
-// handleElse is a noop for anything that isn't /announce. We don't care about
-// other requests (yet).
-func handleElse(rw http.ResponseWriter, request *http.Request) {}
-
-func main() {
- flag.Parse()
- var wg sync.WaitGroup
- if *testnet {
- log.Println("Enabling testnet")
- lib.Testnet = true
- }
-
- // Chdir to our working directory.
- if _, err := os.Stat(lib.Workdir); os.IsNotExist(err) {
- err := os.Mkdir(lib.Workdir, 0700)
- lib.CheckError(err)
- }
- err := os.Chdir(lib.Workdir)
- lib.CheckError(err)
-
- if _, err := lib.RedisCli.Ping(lib.Rctx).Result(); err != nil {
- // We assume redis is not running. Start it up.
- cmd, err := lib.StartRedis(*redconf)
- defer cmd.Process.Kill()
- lib.CheckError(err)
- }
-
- if lib.Testnet {
- log.Println("Will mark all nodes valid by default.")
- }
-
- mux := http.NewServeMux()
- mux.HandleFunc("/announce", handlePost)
- mux.HandleFunc("/", handleElse)
- srv := &http.Server{
- Addr: ListenAddress,
- Handler: mux,
- ReadTimeout: 30 * time.Second,
- WriteTimeout: 30 * time.Second,
- }
- wg.Add(1)
- go srv.ListenAndServe()
- log.Println("Listening on", ListenAddress)
-
- if *ttl > 0 {
- log.Printf("Enabling TTL polling (%d minute expire time).\n", *ttl)
- go pollNodeTTL(*ttl)
- }
-
- wg.Wait()
-}
diff --git a/cmd/dam-gource/main.go b/cmd/dam-gource/main.go
@@ -1,42 +0,0 @@
-package main
-
-/*
- * Copyright (c) 2018 Dyne.org Foundation
- * tor-dam is written and maintained by Ivan Jelincic <parazyd@dyne.org>
- *
- * This file is part of tor-dam
- *
- * 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 <http://www.gnu.org/licenses/>.
- */
-
-import (
- "fmt"
- "os"
-
- lib "github.com/parazyd/tor-dam/pkg/damlib"
-)
-
-func main() {
- pubsub := lib.RedisCli.Subscribe(lib.Rctx, lib.PubSubChan)
- _, err := pubsub.Receive(lib.Rctx)
- lib.CheckError(err)
- fmt.Fprintf(os.Stderr, "Subscribed to %s channel in Redis\n", lib.PubSubChan)
-
- ch := pubsub.Channel()
-
- fmt.Fprintf(os.Stderr, "Listening to messages...\n")
- for msg := range ch {
- fmt.Println(msg.Payload)
- }
-}
diff --git a/config.go b/config.go
@@ -0,0 +1,36 @@
+package main
+
+/*
+ * Copyright (c) 2017-2021 Ivan Jelincic <parazyd@dyne.org>
+ *
+ * This file is part of tor-dam
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+import (
+ "crypto/ed25519"
+ "net"
+)
+
+const (
+ seedName = "ed25519.seed"
+ pubsubChan = "tordam"
+)
+
+var (
+ redisAddr *net.TCPAddr
+ torAddr *net.TCPAddr
+ signingKey ed25519.PrivateKey
+)
diff --git a/contrib/Makefile b/contrib/Makefile
@@ -1,35 +0,0 @@
-# See LICENSE file for copyright and license details.
-
-PREFIX ?= /usr/local
-
-SRC =\
- redis.conf \
- torrc
-
-all:
- @echo 'Run "make install" to install to $(DESTDIR)$(PREFIX)/share/tor-dam'
- @echo 'Run "make install-init" to install initscripts to $(DESTDIR)/etc'
-
-install:
- @echo 'Installing to $(DESTDIR)$(PREFIX)/share/tor-dam'
- mkdir -p $(DESTDIR)$(PREFIX)/share/tor-dam
- cp -f $(SRC) $(DESTDIR)$(PREFIX)/share/tor-dam
-
-install-init:
- @echo 'Installing to $(DESTDIR)/etc/init.d and $(DESTDIR)/etc/conf.d'
- mkdir -p $(DESTDIR)/etc/init.d $(DESTDIR)/etc/conf.d
- cp -f dam-dir.init $(DESTDIR)/etc/init.d/dam-dir
- cp -f dam-client.init $(DESTDIR)/etc/init.d/dam-client
- chmod 755 $(DESTDIR)/etc/init.d/dam-dir
- chmod 755 $(DESTDIR)/etc/init.d/dam-client
- cp -f dam-dir.conf $(DESTDIR)/etc/conf.d/dam-dir
- cp -f dam-client.conf $(DESTDIR)/etc/conf.d/dam-client
-
-uninstall:
- @echo 'Uninstalling from $(DESTDIR)$(PREFIX)/share/tor-dam'
- rm -rf $(DESTDIR)$(PREFIX)/share/tor-dam
- @echo 'Uninstalling initscripts from $(DESTDIR)/etc'
- rm -f $(DESTDIR)/etc/init.d/dam-dir $(DESTDIR)/etc/init.d/dam-client
- rm -f $(DESTDIR)/etc/conf.d/dam-dir $(DESTDIR)/etc/conf.d/dam-client
-
-.PHONY: all install install-init uninstall
diff --git a/contrib/README.md b/contrib/README.md
@@ -0,0 +1,43 @@
+contrib
+=======
+
+Some files here could be helpful for you to find a usecase for tor-dam.
+
+### `echo_send.py` and `echo_recv.py`
+
+These two Python programs can be seen as a reference echo client/server
+implementation for working over SOCKS5. With these, you can use some
+onion address and port created and opened by tor-dam.
+
+```
+$ tor-dam -p "6969:6969" -d ./echo-dam
+$ sleep 1
+$ hostname="$(cat ./echo-dam/hs/hostname)"
+$ ./echo_recv.py -l 127.0.0.1 -p 6969 &
+$ ./echo_send.py -a "$hostname" -p 6969 -t "$torsocksport"
+```
+
+N.B. You can find `$torsocksport` using `netstat(8)` or whatever
+similar too.
+
+
+### `gource.go`
+
+This is a Golang implementation of a Redis pubsub client, and was used
+to create [network.gif](network.gif) that can be seen in this directory.
+The internal format used for publishing is:
+
+```
+%s|%s|%s|%s
+```
+
+which translates to:
+
+```
+timestamp|onion_address|modification_type|onion_address
+```
+
+```
+$ redishost="127.0.0.1:35918" # You can find this in netstat
+$ go run gource.go -r "$redishost" | gource --log-format custom -
+```
diff --git a/contrib/dam-client.conf b/contrib/dam-client.conf
@@ -1,13 +0,0 @@
-# /etc/conf.d/dam-client
-
-# User to run as
-damuid="decode"
-
-# Group to run as
-damgid="decode"
-
-# Path to logfile
-damlog="/var/log/tor-dam/dam-client.log"
-
-# Commandline flags
-#damopts="-d"
diff --git a/contrib/dam-client.init b/contrib/dam-client.init
@@ -1,31 +0,0 @@
-#!/sbin/openrc-run
-# Copyright 1999-2018 Gentoo Foundation
-# Distributed under the terms of the GNU General Public License v2
-
-command="/home/$damuid/go/bin/dam-client"
-pidfile="/var/run/dam-client.pid"
-
-description="Tor DAM client"
-
-depend() {
- after tor dam-dir ntp
-}
-
-start() {
- ebegin "Starting $description"
- _h="$(getent passwd $damuid | cut -d: -f6)"
- mkdir -p $(dirname $damlog)
- chown $damuid:$damgid $(dirname $damlog)
- supervise-daemon -d $_h -e HOME=$_h -u $damuid -g $damgid \
- --pidfile $pidfile -1 $damlog -2 $damlog \
- --start $command $damopts
-}
-
-stop() {
- ebegin "Stopping $description"
- dcli="$(pgrep -P $(cat $pidfile))"
- dahs="$(pgrep -P $dcli)"
- supervise-daemon --stop $command -p $pidfile
- kill $dahs || true
- kill $dcli || true
-}
diff --git a/contrib/dam-dir.conf b/contrib/dam-dir.conf
@@ -1,13 +0,0 @@
-# /etc/conf.d/dam-dir
-
-# User to run as
-damuid="decode"
-
-# Group to run as
-damgid="decode"
-
-# Path to logfile
-damlog="/var/log/tor-dam/dam-dir.log"
-
-# Commandline flags
-#damopts="-t"
diff --git a/contrib/dam-dir.init b/contrib/dam-dir.init
@@ -1,32 +0,0 @@
-#!/sbin/openrc-run
-# Copyright 1999-2018 Gentoo Foundation
-# Distributed under the terms of the GNU General Public License v2
-
-command="/home/$damuid/go/bin/dam-dir"
-pidfile="/var/run/dam-dir.pid"
-
-description="Tor DAM server"
-
-depend() {
- after logger ntp
- before tor dam-client
-}
-
-start() {
- ebegin "Starting $description"
- _h="$(getent passwd $damuid | cut -d: -f6)"
- mkdir -p $(dirname $damlog)
- chown $damuid:$damgid $(dirname $damlog)
- supervise-daemon -d $_h -e HOME=$_h -u $damuid -g $damgid \
- --pidfile $pidfile -1 $damlog -2 $damlog \
- --start $command $damopts
-}
-
-stop() {
- ebegin "Stopping $description"
- ddir="$(pgrep -P $(cat $pidfile))"
- rdis="$(pgrep -P $ddir)"
- supervise-daemon --stop $command -p $pidfile
- kill $rdis || true
- kill $ddir || true
-}
diff --git a/contrib/echo_recv.py b/contrib/echo_recv.py
@@ -1,21 +1,21 @@
#!/usr/bin/env python3
-# Copyright (c) 2019 Dyne.org Foundation
-# tor-dam is written and maintained by Ivan Jelincic <parazyd@dyne.org>
+# Copyright (c) 2017-2021 Ivan Jelincic <parazyd@dyne.org>
#
# This file is part of tor-dam
#
# 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
+# it under the terms of the GNU 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.
+# GNU 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 <http://www.gnu.org/licenses/>.
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
from argparse import ArgumentParser
from socket import socket, AF_INET, SOCK_STREAM
@@ -28,7 +28,7 @@ s = socket(AF_INET, SOCK_STREAM)
s.bind((args.listen, args.port))
s.listen(1)
-conn, addr = s.accept()
+conn, ddr = s.accept()
while 1:
data = conn.recv(1024)
if not data:
diff --git a/contrib/echo_send.py b/contrib/echo_send.py
@@ -1,36 +1,37 @@
#!/usr/bin/env python3
-# Copyright (c) 2019 Dyne.org Foundation
-# tor-dam is written and maintained by Ivan Jelincic <parazyd@dyne.org>
+# Copyright (c) 2017-2021 Ivan Jelincic <parazyd@dyne.org>
#
# This file is part of tor-dam
#
# 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
+# it under the terms of the GNU 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.
+# GNU 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 <http://www.gnu.org/licenses/>.
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
from argparse import ArgumentParser
from socket import socket, AF_INET, SOCK_STREAM
import socks
parser = ArgumentParser()
-parser.add_argument('-a', '--address', default='127.0.0.1')
+parser.add_argument('-a', '--address', default='some.onion')
parser.add_argument('-p', '--port', default=5000)
+parser.add_argument('-t', '--tor', default='127.0.0.1:9050')
args = parser.parse_args()
if '.onion' in args.address:
- s = socks.socksocket(AF_INET, SOCK_STREAM)
- s.set_proxy(socks.SOCKS5, "localhost", 9050)
+ s = socks.socksocket(AF_INET, SOCK_STREAM)
+ s.set_proxy(socks.SOCKS5, args.tor.split()[0], int(args.tor.split()[1]))
else:
- s = socket(AF_INET, SOCK_STREAM)
+ s = socket(AF_INET, SOCK_STREAM)
s.connect((args.address, args.port))
s.send(b'HELLO')
diff --git a/contrib/gource.go b/contrib/gource.go
@@ -0,0 +1,59 @@
+package main
+
+/*
+ * Copyright (c) 2017-2021 Ivan Jelincic <parazyd@dyne.org>
+ *
+ * This file is part of tor-dam
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+import (
+ "context"
+ "flag"
+ "fmt"
+ "log"
+
+ "github.com/go-redis/redis"
+)
+
+var (
+ redisAddr = flag.String("-r", "127.0.0.1:39148", "host:port for redis")
+ rctx = context.Background()
+ rcli *redis.Client
+)
+
+func main() {
+ flag.Parse()
+
+ rcli = redis.NewClient(&redis.Options{
+ Addr: *redisAddr,
+ Password: "",
+ DB: 0,
+ })
+
+ // "tordam" is the hardcoded name of the channel
+ pubsub := rcli.Subscribe(rctx, "tordam")
+ _, err := pubsub.Receive(rctx)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ log.Println("Subscribed to channel in redis")
+
+ ch := pubsub.Channel()
+ for msg := range ch {
+ fmt.Println(msg.Payload)
+ }
+}
diff --git a/contrib/redis.conf b/contrib/redis.conf
@@ -1,20 +0,0 @@
-#
-# Redis configuration for tor-dam
-#
-
-daemonize no
-
-bind 127.0.0.1
-port 6379
-
-databases 1
-dbfilename dam-dir.rdb
-
-save 900 1
-save 300 10
-save 60 10000
-
-rdbcompression yes
-rdbchecksum yes
-
-stop-writes-on-bgsave-error no
diff --git a/contrib/torrc b/contrib/torrc
@@ -1,16 +0,0 @@
-#
-# Minimal torrc so tor will work out of the box
-#
-
-User tor
-PIDFile /var/run/tor/tor.pid
-Log notice syslog
-DataDirectory /var/lib/tor/data
-
-ControlPort 9051
-CookieAuthentication 1
-HashedControlPassword 16:6091EDA9F3F5F8EB60C8423561CB8C46B3CCF033E88FEACA1FC8BDBB9A
-
-SocksPort 9050
-
-ClientRejectInternalAddresses 1
diff --git a/crypto.go b/crypto.go
@@ -0,0 +1,63 @@
+package main
+
+/*
+ * Copyright (c) 2017-2021 Ivan Jelincic <parazyd@dyne.org>
+ *
+ * This file is part of tor-dam
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+import (
+ "crypto/ed25519"
+ "crypto/rand"
+ "encoding/base64"
+ "io/ioutil"
+ "log"
+ "os"
+ "strings"
+)
+
+func generateED25519Keypair(dir string) error {
+ _, sk, err := ed25519.GenerateKey(rand.Reader)
+ if err != nil {
+ return err
+ }
+
+ if err := os.MkdirAll(dir, 0700); err != nil {
+ return err
+ }
+
+ seedpath := strings.Join([]string{dir, seedName}, "/")
+
+ log.Println("Writing ed25519 key seed to", seedpath)
+ return ioutil.WriteFile(seedpath,
+ []byte(base64.StdEncoding.EncodeToString(sk.Seed())), 0600)
+}
+
+func loadED25519Seed(file string) (ed25519.PrivateKey, error) {
+ log.Println("Reading ed25519 seed from", file)
+
+ data, err := ioutil.ReadFile(file)
+ if err != nil {
+ return nil, err
+ }
+
+ dec, err := base64.StdEncoding.DecodeString(string(data))
+ if err != nil {
+ return nil, err
+ }
+
+ return ed25519.NewKeyFromSeed(dec), nil
+}
diff --git a/helpers.go b/helpers.go
@@ -0,0 +1,88 @@
+package main
+
+/*
+ * Copyright (c) 2017-2021 Ivan Jelincic <parazyd@dyne.org>
+ *
+ * This file is part of tor-dam
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+import (
+ "bytes"
+ "compress/gzip"
+ "crypto/rand"
+ "encoding/base64"
+ "fmt"
+ "math/big"
+ "strings"
+)
+
+func genRandomASCII(length int) (string, error) {
+ var res string
+ for {
+ if len(res) == length {
+ return res, nil
+ }
+ num, err := rand.Int(rand.Reader, big.NewInt(int64(127)))
+ if err != nil {
+ return "", err
+ }
+ n := num.Int64()
+ if n > 32 && n < 127 {
+ res += fmt.Sprint(n)
+ }
+ }
+}
+
+func gzipEncode(data []byte) (string, error) {
+ var b bytes.Buffer
+ gz := gzip.NewWriter(&b)
+ if _, err := gz.Write(data); err != nil {
+ return "", err
+ }
+ if err := gz.Flush(); err != nil {
+ return "", err
+ }
+ if err := gz.Close(); err != nil {
+ return "", err
+ }
+
+ return base64.StdEncoding.EncodeToString(b.Bytes()), nil
+}
+
+func stringInSlice(str string, slice []string) bool {
+ for _, i := range slice {
+ if str == i {
+ return true
+ }
+ }
+ return false
+}
+
+func parseDirs(sl []string, data []byte) []string {
+ dirstr := string(data)
+ _dirs := strings.Split(dirstr, "\n")
+ for _, i := range _dirs {
+ if strings.HasPrefix(i, "DIR:") {
+ t := strings.Split(i, "DIR:")
+ if !stringInSlice(t[1], sl) {
+ if validateOnionAddress(t[1]) {
+ sl = append(sl, t[1])
+ }
+ }
+ }
+ }
+ return sl
+}
diff --git a/net.go b/net.go
@@ -0,0 +1,83 @@
+package main
+
+/*
+ * Copyright (c) 2017-2021 Ivan Jelincic <parazyd@dyne.org>
+ *
+ * This file is part of tor-dam
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+import (
+ "bytes"
+ "io/ioutil"
+ "net"
+ "net/http"
+
+ "golang.org/x/net/proxy"
+)
+
+func getListener() (*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
+}
+
+func httpPost(host string, data []byte) (*http.Response, error) {
+ httpTransp := &http.Transport{}
+ httpClient := &http.Client{Transport: httpTransp}
+ dialer, err := proxy.SOCKS5("tcp", torAddr.String(), nil, proxy.Direct)
+ if err != nil {
+ return nil, err
+ }
+ httpTransp.Dial = dialer.Dial
+
+ request, err := http.NewRequest("POST", host, bytes.NewBuffer(data))
+ if err != nil {
+ return nil, err
+ }
+
+ request.Header.Set("Content-Type", "application/json")
+ return httpClient.Do(request)
+}
+
+func httpGet(uri string) ([]byte, error) {
+ httpTransp := &http.Transport{}
+ httpClient := &http.Client{Transport: httpTransp}
+ dialer, err := proxy.SOCKS5("tcp", torAddr.String(), nil, proxy.Direct)
+ if err != nil {
+ return nil, err
+ }
+ httpTransp.Dial = dialer.Dial
+
+ request, err := http.NewRequest("GET", uri, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ res, err := httpClient.Do(request)
+ if err != nil {
+ return nil, err
+ }
+ defer res.Body.Close()
+ return ioutil.ReadAll(res.Body)
+}
diff --git a/pkg/damlib/config.go b/pkg/damlib/config.go
@@ -1,55 +0,0 @@
-package damlib
-
-/*
- * Copyright (c) 2017-2018 Dyne.org Foundation
- * tor-dam is written and maintained by Ivan Jelincic <parazyd@dyne.org>
- *
- * This file is part of tor-dam
- *
- * 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 <http://www.gnu.org/licenses/>.
- */
-
-import "os"
-
-// Workdir holds the path to the directory where we will Chdir on startup.
-var Workdir = os.Getenv("HOME") + "/.dam"
-
-// PrivKeyPath holds the name of where our private key is.
-const PrivKeyPath = "dam-private.key"
-
-// SeedPath holds the name of where our private key seed is.
-const SeedPath = "dam-private.seed"
-
-// PubSubChan is the name of the pub/sub channel we're publishing to in Redis.
-const PubSubChan = "tordam"
-
-// PostMsg holds the message we are signing with our private key.
-const PostMsg = "I am a DAM node!"
-
-// WelcomeMsg holds the message we return when welcoming a node.
-const WelcomeMsg = "Welcome to the DAM network!"
-
-// ProxyAddr is the address of our Tor SOCKS port.
-const ProxyAddr = "127.0.0.1:9050"
-
-// TorPortMap is a comma-separated string holding the mapping of ports
-// to be opened by the Tor Hidden Service. Format is "remote:local".
-var TorPortMap = "80:49371,13010:13010,13011:13011,5000:5000"
-
-// DirPort is the port where dam-dir will be listening.
-const DirPort = 49371
-
-// Testnet is flipped with a flag in dam-dir and represents if all new
-// nodes are initially marked valid or not.
-var Testnet = false
diff --git a/pkg/damlib/crypto_25519.go b/pkg/damlib/crypto_25519.go
@@ -1,137 +0,0 @@
-package damlib
-
-/*
- * Copyright (c) 2017-2018 Dyne.org Foundation
- * tor-dam is written and maintained by Ivan Jelincic <parazyd@dyne.org>
- *
- * This file is part of tor-dam
- *
- * 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 <http://www.gnu.org/licenses/>.
- */
-
-import (
- "crypto"
- "crypto/rand"
- "crypto/sha512"
- "encoding/base32"
- "encoding/base64"
- "io/ioutil"
- "log"
- "strings"
-
- "golang.org/x/crypto/ed25519"
- "golang.org/x/crypto/sha3"
-)
-
-// GenEd25519 generates an ed25519 keypair. Returns error on failure.
-func GenEd25519() (ed25519.PublicKey, ed25519.PrivateKey, error) {
- log.Println("Generating ed25519 keypair...")
-
- pk, sk, err := ed25519.GenerateKey(rand.Reader)
- if err != nil {
- return nil, nil, err
- }
- return pk, sk, nil
-}
-
-// SavePrivEd25519 writes a ed25519.PrivateKey type to a given string filename.
-// Expands ed25519.PrivateKey to (a || RH) form, writing base64. Returns error
-// upon failure.
-func SavePrivEd25519(filename string, key ed25519.PrivateKey) error {
- log.Println("Writing ed25519 private key to", filename)
-
- h := sha512.Sum512(key[:32])
- // Set bits so that h[:32] is a private scalar "a".
- h[0] &= 248
- h[31] &= 127
- h[31] |= 64
- // Since h[32:] is RH, h is now (a || RH)
- encoded := base64.StdEncoding.EncodeToString(h[:])
- return ioutil.WriteFile(filename, []byte(encoded), 0600)
-}
-
-// SaveSeedEd25519 saves the ed25519 private key seed to a given string filename
-// for later reuse. Returns error upon failure.
-func SaveSeedEd25519(filename string, key ed25519.PrivateKey) error {
- log.Println("Writing ed25519 private key seed to", filename)
-
- encoded := base64.StdEncoding.EncodeToString(key.Seed())
- return ioutil.WriteFile(filename, []byte(encoded), 0600)
-}
-
-// LoadEd25519KeyFromSeed loads a key from a given seed file and returns
-// ed25519.PrivateKey. Otherwise, on failure, it returns error.
-func LoadEd25519KeyFromSeed(filename string) (ed25519.PrivateKey, error) {
- log.Println("Loading ed25519 private key from seed in", filename)
-
- data, err := ioutil.ReadFile(filename)
- if err != nil {
- return nil, err
- }
-
- decoded, err := base64.StdEncoding.DecodeString(string(data))
- if err != nil {
- return nil, err
- }
- return ed25519.NewKeyFromSeed(decoded), nil
-}
-
-// SignMsgEd25519 signs a message using ed25519. Returns the signature in the
-// form of []byte, or returns an error if it fails.
-func SignMsgEd25519(message []byte, key ed25519.PrivateKey) ([]byte, error) {
- log.Println("Signing message...")
-
- sig, err := key.Sign(rand.Reader, message, crypto.Hash(0))
- if err != nil {
- return nil, err
- }
- return sig, nil
-}
-
-// OnionFromPubkeyEd25519 generates a valid onion address from a given ed25519
-// public key. Returns the onion address as a slice of bytes.
-//
-// Tor Spec excerpt from https://gitweb.torproject.org/torspec.git/tree/rend-spec-v3.txt
-// ---
-// 6. Encoding onion addresses [ONIONADDRESS]
-// The onion address of a hidden service includes its identity public key, a
-// version field and a basic checksum. All this information is then base32
-// encoded as shown below:
-//
-// onion_address = base32(PUBKEY | CHECKSUM | VERSION) + ".onion"
-// CHECKSUM = H(".onion checksum" | PUBKEY | VERSION)[:2]
-//
-// where:
-// - PUBKEY is the 32 bytes ed25519 master pubkey of the hidden service.
-// - VERSION is an one byte version field (default value '\x03')
-// - ".onion checksum" is a constant string
-// - CHECKSUM is truncated to two bytes before inserting it in onion_address
-func OnionFromPubkeyEd25519(pubkey ed25519.PublicKey) []byte {
- const salt = ".onion checksum"
- const version = byte(0x03)
-
- h := []byte(salt)
- h = append(h, pubkey...)
- h = append(h, version)
-
- csum := sha3.Sum256(h)
- checksum := csum[:2]
-
- enc := pubkey[:]
- enc = append(enc, checksum...)
- enc = append(enc, version)
-
- encoded := base32.StdEncoding.EncodeToString(enc)
- return []byte(strings.ToLower(encoded) + ".onion")
-}
diff --git a/pkg/damlib/helpers.go b/pkg/damlib/helpers.go
@@ -1,105 +0,0 @@
-package damlib
-
-/*
- * Copyright (c) 2017-2018 Dyne.org Foundation
- * tor-dam is written and maintained by Ivan Jelincic <parazyd@dyne.org>
- *
- * This file is part of tor-dam
- *
- * 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 <http://www.gnu.org/licenses/>.
- */
-
-import (
- "bytes"
- "compress/gzip"
- "crypto/rand"
- "encoding/base64"
- "log"
- "math/big"
- "strings"
-)
-
-// CheckError is a handler for errors. It takes an error type as an argument,
-// and issues a log.Fatalln, printing the error and exiting with os.Exit(1).
-func CheckError(err error) {
- if err != nil {
- log.Fatalln(err)
- }
-}
-
-// StringInSlice loops over a slice of strings and checks if a given string is
-// already an existing element. Returns true if so, and false if not.
-func StringInSlice(str string, slice []string) bool {
- for _, i := range slice {
- if str == i {
- return true
- }
- }
- return false
-}
-
-// GzipEncode compresses a given slice of bytes using gzip, and returns it as
-// a base64 encoded string. Returns error upon failure.
-func GzipEncode(data []byte) (string, error) {
- var b bytes.Buffer
- gz := gzip.NewWriter(&b)
- if _, err := gz.Write(data); err != nil {
- return "", err
- }
- if err := gz.Flush(); err != nil {
- return "", err
- }
- if err := gz.Close(); err != nil {
- return "", err
- }
- return base64.StdEncoding.EncodeToString(b.Bytes()), nil
-}
-
-// ParseDirs parses and appends a given slice of bytes and returns an appended
-// slice of strings with new contents.
-func ParseDirs(sl []string, data []byte) []string {
- dirStr := string(data)
- _dirs := strings.Split(dirStr, "\n")
- for _, j := range _dirs {
- if strings.HasPrefix(j, "DIR:") {
- t := strings.Split(j, "DIR:")
- if !(StringInSlice(t[1], sl)) {
- if ValidateOnionAddress(t[1]) {
- sl = append(sl, t[1])
- }
- }
- }
- }
- return sl
-}
-
-// GenRandomASCII generates a random ASCII string of a given length.
-// Takes length int as argument, and returns a string of that length on success
-// and error on failure.
-func GenRandomASCII(length int) (string, error) {
- var res string
- for {
- if len(res) >= length {
- return res, nil
- }
- num, err := rand.Int(rand.Reader, big.NewInt(int64(127)))
- if err != nil {
- return "", err
- }
- n := num.Int64()
- if n > 32 && n < 127 {
- res += string(n)
- }
- }
-}
diff --git a/pkg/damlib/helpers_test.go b/pkg/damlib/helpers_test.go
@@ -1,65 +0,0 @@
-package damlib
-
-/*
- * Copyright (c) 2018 Dyne.org Foundation
- * tor-dam is written and maintained by Ivan Jelincic <parazyd@dyne.org>
- *
- * This file is part of tor-dam
- *
- * 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 <http://www.gnu.org/licenses/>.
- */
-
-import (
- "testing"
-)
-
-func TestStringInSlice(t *testing.T) {
- sl := []string{"foo", "bar", "baz"}
- if !(StringInSlice("bar", sl)) {
- t.Fatal("\"bar\" should be in the slice.")
- }
- if StringInSlice("kek", sl) {
- t.Fatal("\"kek\" should not be in the slice.")
- }
-}
-
-func TestGzipEncode(t *testing.T) {
- data := "Compress this string"
- if _, err := GzipEncode([]byte(data)); err != nil {
- t.Fatal(err)
- }
-}
-
-func TestParseDirs(t *testing.T) {
- var sl []string
- data := `DIR:gphjf5g3d5ywehwrd7cv3czymtdc6ha67bqplxwbspx7tioxt7gxqiid.onion
-# Some random data
-DIR:vww6ybal4bd7szmgncyruucpgfkqahzddi37ktceo3ah7ngmcopnpyyd.onion`
-
- sl = ParseDirs(sl, []byte(data))
-
- if len(sl) != 2 {
- t.Fatal("Length of slice is not 2.")
- }
-}
-
-func TestGenRandomASCII(t *testing.T) {
- res, err := GenRandomASCII(64)
- if err != nil {
- t.Fatal(err)
- }
- if len(res) != 64 {
- t.Fatal("Length of ASCII string is not 64.")
- }
-}
diff --git a/pkg/damlib/net.go b/pkg/damlib/net.go
@@ -1,86 +0,0 @@
-package damlib
-
-/*
- * Copyright (c) 2017-2018 Dyne.org Foundation
- * tor-dam is written and maintained by Ivan Jelincic <parazyd@dyne.org>
- *
- * This file is part of tor-dam
- *
- * 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 <http://www.gnu.org/licenses/>.
- */
-
-import (
- "bytes"
- "io/ioutil"
- "log"
- "net/http"
- "net/url"
- "strings"
-
- "golang.org/x/net/proxy"
-)
-
-// HTTPPost sends an HTTP POST request to the given host.
-// Takes the host to request and the data to post as arguments.
-// If the host ends with ".onion", it will enable the request to be performed
-// over a SOCKS proxy, defined in ProxyAddr.
-// On success, it will return the http.Response. Otherwise, it returns an error.
-func HTTPPost(host string, data []byte) (*http.Response, error) {
- socksify := false
- parsedHost, err := url.Parse(host)
- if err != nil {
- return nil, err
- }
- hostname := parsedHost.Hostname()
- if strings.HasSuffix(hostname, ".onion") {
- socksify = true
- }
- httpTransp := &http.Transport{}
- httpClient := &http.Client{Transport: httpTransp}
- if socksify {
- log.Println("Detected a .onion request. Using SOCKS proxy.")
- dialer, err := proxy.SOCKS5("tcp", ProxyAddr, nil, proxy.Direct)
- if err != nil {
- return nil, err
- }
- httpTransp.Dial = dialer.Dial
- }
- request, err := http.NewRequest("POST", host, bytes.NewBuffer(data))
- if err != nil {
- return nil, err
- }
- request.Header.Set("Content-Type", "application/json")
-
- resp, err := httpClient.Do(request)
- if err != nil {
- return nil, err
- }
-
- return resp, nil
-}
-
-// HTTPDownload tries to download a given uri and return it as a slice of bytes.
-// On failure it will return an error.
-func HTTPDownload(uri string) ([]byte, error) {
- res, err := http.Get(uri)
- if err != nil {
- return nil, err
- }
- defer res.Body.Close()
- d, err := ioutil.ReadAll(res.Body)
- if err != nil {
- return nil, err
- }
- return d, nil
-}
diff --git a/pkg/damlib/redis.go b/pkg/damlib/redis.go
@@ -1,92 +0,0 @@
-package damlib
-
-/*
- * Copyright (c) 2017-2018 Dyne.org Foundation
- * tor-dam is written and maintained by Ivan Jelincic <parazyd@dyne.org>
- *
- * This file is part of tor-dam
- *
- * 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 <http://www.gnu.org/licenses/>.
- */
-
-import (
- "context"
- "fmt"
- "log"
- "os/exec"
- "time"
-
- "github.com/go-redis/redis"
-)
-
-// RedisAddress points us to our Redis instance.
-const RedisAddress = "127.0.0.1:6379"
-
-// Rctx is the context for Redis
-var Rctx = context.Background()
-
-// RedisCli is our global Redis client
-var RedisCli = redis.NewClient(&redis.Options{
- Addr: RedisAddress,
- Password: "",
- DB: 0,
-})
-
-// StartRedis is the function that will start up the Redis server. Takes the
-// path to a configuration file as an argument and returns error upon failure.
-func StartRedis(conf string) (*exec.Cmd, error) {
- log.Println("Starting up redis-server...")
- cmd := exec.Command("redis-server", conf)
- err := cmd.Start()
- if err != nil {
- return cmd, err
- }
-
- time.Sleep(500 * time.Millisecond)
- if _, err := RedisCli.Ping(Rctx).Result(); err != nil {
- return cmd, err
- }
-
- PubSub := RedisCli.Subscribe(Rctx, PubSubChan)
- if _, err := PubSub.Receive(Rctx); err != nil {
- return cmd, err
- }
-
- log.Printf("Created \"%s\" channel in Redis.\n", PubSubChan)
- return cmd, nil
-}
-
-// PublishToRedis is a function that publishes a node's status to Redis.
-// This is used for Gource visualization.
-func PublishToRedis(mt, address string) {
- var timestamp, username, modtype, onion, pubstr string
-
- nodedata, err := RedisCli.HGetAll(Rctx, address).Result()
- CheckError(err)
-
- timestamp = nodedata["lastseen"]
- if timestamp == nodedata["firstseen"] {
- modtype = "A"
- } else if mt == "d" {
- modtype = "D"
- } else {
- modtype = "M"
- }
- username = address
- onion = address
-
- pubstr = fmt.Sprintf("%s|%s|%s|%s", timestamp, username, modtype, onion)
-
- RedisCli.Publish(Rctx, PubSubChan, pubstr)
-}
diff --git a/pkg/damlib/validate.go b/pkg/damlib/validate.go
@@ -1,211 +0,0 @@
-package damlib
-
-/*
- * Copyright (c) 2017-2018 Dyne.org Foundation
- * tor-dam is written and maintained by Ivan Jelincic <parazyd@dyne.org>
- *
- * This file is part of tor-dam
- *
- * 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 <http://www.gnu.org/licenses/>.
- */
-
-import (
- "encoding/base64"
- "log"
- "regexp"
- "time"
-
- "golang.org/x/crypto/ed25519"
-)
-
-// ValidateOnionAddress matches a string against a regular expression matching
-// a Tor hidden service address. Returns true on success and false on failure.
-func ValidateOnionAddress(addr string) bool {
- re, _ := regexp.Compile(`^[a-z2-7](?:.{55})\.onion`)
- return len(re.FindString(addr)) == 62
-}
-
-// sanityCheck performs basic sanity checks against the incoming request.
-// Returns a boolean value according to the validity, and a string with an
-// according message.
-func sanityCheck(req map[string]string, handshake int) (bool, string) {
- if !(ValidateOnionAddress(req["address"])) {
- return false, "Invalid onion address"
- }
- if _, err := base64.StdEncoding.DecodeString(req["signature"]); err != nil {
- return false, err.Error()
- }
-
- if handshake == 2 {
- if _, err := base64.StdEncoding.DecodeString(req["message"]); err != nil {
- return false, err.Error()
- }
- if _, err := base64.StdEncoding.DecodeString(req["secret"]); err != nil {
- return false, err.Error()
- }
- }
- return true, ""
-}
-
-// ValidateFirstHandshake validates the first incoming handshake. It first calls
-// sanityCheck to validate it's actually working with proper data. Next, it will
-// look if the node is already found in redis. If so, it will fetch its public
-// hey from redis, otherwise it will take it from the initial request from the
-// incoming node. Once the public key is retrieved, it will validate the
-// received message signature against that key. If all is well, we consider the
-// request valid. Continuing, a random ASCII string will be generated and
-// encrypted with the retrieved public key. All this data will be written into
-// redis, and finally the encrypted (and base64 encoded) secret will be returned
-// along with a true boolean value. On any failure, the function will return
-// false, and produce an according string which is to be considered as an error
-// message.
-func ValidateFirstHandshake(req map[string]string) (bool, string) {
- if sane, what := sanityCheck(req, 1); !(sane) {
- return false, what
- }
-
- // Get the public key.
- var pubstr string
- var pubkey ed25519.PublicKey
- // Check if we have seen this node already.
- ex, err := RedisCli.Exists(Rctx, req["address"]).Result()
- CheckError(err)
- if ex == 1 {
- // We saw it so we should have the public key in redis.
- // If we do not, that is an internal error.
- pubstr, err = RedisCli.HGet(Rctx, req["address"], "pubkey").Result()
- CheckError(err)
- } else {
- // We take it from the announce.
- pubstr = req["pubkey"]
- }
-
- // Validate signature.
- msg := []byte(req["message"])
- sig, _ := base64.StdEncoding.DecodeString(req["signature"])
- deckey, err := base64.StdEncoding.DecodeString(pubstr)
- CheckError(err)
- pubkey = ed25519.PublicKey(deckey)
-
- if !(ed25519.Verify(pubkey, msg, sig)) {
- log.Println("crypto/ed25519: Signature verification failure.")
- return false, "Signature verification vailure."
- }
-
- // The request is valid at this point.
-
- // Make a random secret for them, and save our node info to redis.
- randString, err := GenRandomASCII(64)
- CheckError(err)
- encodedSecret := base64.StdEncoding.EncodeToString([]byte(randString))
-
- var info = map[string]interface{}{
- "address": req["address"],
- "message": encodedSecret,
- "signature": req["signature"],
- "secret": encodedSecret,
- "lastseen": time.Now().Unix(),
- } // Can not cast, need this for HMSet
- if ex != 1 { // We did not have this node in redis.
- info["pubkey"] = pubstr
- info["firstseen"] = time.Now().Unix()
- if Testnet {
- info["valid"] = 1
- } else {
- info["valid"] = 0
- }
- }
-
- log.Printf("%s: writing to redis\n", req["address"])
- _, err = RedisCli.HMSet(Rctx, req["address"], info).Result()
- CheckError(err)
-
- return true, encodedSecret
-}
-
-// ValidateSecondHandshake validates the second part of the handshake.
-// First basic sanity checks are performed to ensure we are working with valid
-// data.
-// Next, the according public key will be retrieved from redis. If no key is
-// found, we will consider the handshake invalid.
-// Now the decrypted secret that was sent to us will be compared with what we
-// have saved before. Upon proving they are the same, the signature will now
-// be validated. If all is well, we consider the request valid.
-// Further on, we will generate a new random ASCII string and save it in redis
-// to prevent further reuse of the already known string. Upon success, the
-// function will return true, and a welcome message. Upon failure, the function
-// will return false, and an according string which is to be considered an error
-// message.
-func ValidateSecondHandshake(req map[string]string) (bool, string) {
- if sane, what := sanityCheck(req, 2); !(sane) {
- return false, what
- }
-
- // Get the public key.
- var pubstr string
- var pubkey ed25519.PublicKey
- // Check if we have seen this node already.
- ex, err := RedisCli.Exists(Rctx, req["address"]).Result()
- CheckError(err)
- if ex == 1 {
- // We saw it so we should have the public key in redis.
- // If we do not, that is an internal error.
- pubstr, err = RedisCli.HGet(Rctx, req["address"], "pubkey").Result()
- CheckError(err)
- } else {
- log.Printf("%s tried to jump in 2/2 handshake before doing the first.\n", req["address"])
- return false, "We have not seen you before. Please authenticate properly."
- }
-
- localSec, err := RedisCli.HGet(Rctx, req["address"], "secret").Result()
- CheckError(err)
-
- if !(localSec == req["secret"] && localSec == req["message"]) {
- log.Printf("%s: Secrets don't match.\n", req["address"])
- return false, "Secrets don't match."
- }
-
- // Validate signature.
- msg := []byte(req["message"])
- sig, _ := base64.StdEncoding.DecodeString(req["signature"])
- deckey, err := base64.StdEncoding.DecodeString(pubstr)
- CheckError(err)
- pubkey = ed25519.PublicKey(deckey)
-
- if !(ed25519.Verify(pubkey, msg, sig)) {
- log.Println("crypto/ed25519: Signature verification failure")
- return false, "Signature verification failure."
- }
-
- // The request is valid at this point.
-
- // Make a new random secret to prevent reuse.
- randString, err := GenRandomASCII(64)
- CheckError(err)
- encodedSecret := base64.StdEncoding.EncodeToString([]byte(randString))
-
- var info = map[string]interface{}{
- "address": req["address"],
- "message": encodedSecret,
- "signature": req["signature"],
- "secret": encodedSecret,
- "lastseen": time.Now().Unix(),
- } // Can not cast, need this for HMSet
-
- log.Printf("%s: writing to redis\n", req["address"])
- _, err = RedisCli.HMSet(Rctx, req["address"], info).Result()
- CheckError(err)
-
- return true, WelcomeMsg
-}
diff --git a/pkg/damlib/validate_test.go b/pkg/damlib/validate_test.go
@@ -1,161 +0,0 @@
-package damlib
-
-/*
- * Copyright (c) 2018 Dyne.org Foundation
- * tor-dam is written and maintained by Ivan Jelincic <parazyd@dyne.org>
- *
- * This file is part of tor-dam
- *
- * 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 <http://www.gnu.org/licenses/>.
- */
-
-import (
- "encoding/base64"
- "testing"
-)
-
-func makeReq() map[string]string {
- return map[string]string{
- "address": "gphjf5g3d5ywehwrd7cv3czymtdc6ha67bqplxwbspx7tioxt7gxqiid.onion",
- "pubkey": "M86S9NsfcWIe0R/FXYs4ZMYvHB74YPXewZPv+aHXn80=",
- "message": "I am a DAM node!",
- "signature": "CWqptO9ZRIvYMIHd3XHXaVny+W23P8FGkfbn5lvUqeJbDcY3G8+B4G8iCCIQiZkxkMofe6RbstHn3L1x88c3AA==",
- "secret": "",
- }
-}
-
-func TestValidateOnionAddress(t *testing.T) {
- if !(ValidateOnionAddress("gphjf5g3d5ywehwrd7cv3czymtdc6ha67bqplxwbspx7tioxt7gxqiid.onion")) {
- t.Fatal("Validating a valid address failed.")
- }
- if ValidateOnionAddress("gphjf5g3d5ywe1wd.onion") {
- t.Fatal("Validating an invalid address succeeded.")
- }
-}
-
-func TestValidValidateFirstHandshake(t *testing.T) {
- cmd, _ := StartRedis("../../contrib/redis.conf")
- defer cmd.Process.Kill()
-
- if valid, _ := ValidateFirstHandshake(makeReq()); !(valid) {
- t.Fatal("Failed to validate first handshake.")
- }
-}
-
-func TestInvalidValidateFirstHandshake(t *testing.T) {
- cmd, _ := StartRedis("../../contrib/redis.conf")
- defer cmd.Process.Kill()
-
- // Invalid message for this signature.
- req := makeReq()
- req["message"] = "I am a bad DAM node!"
-
- if valid, _ := ValidateFirstHandshake(req); valid {
- t.Fatal("Invalid request passed as valid.")
- }
-}
-
-func TestValidValidateSecondHandshake(t *testing.T) {
- cmd, _ := StartRedis("../../contrib/redis.conf")
- defer cmd.Process.Kill()
-
- pk, sk, _ := GenEd25519()
- onionaddr := OnionFromPubkeyEd25519(pk)
-
- sig, err := SignMsgEd25519([]byte("I am a DAM node!"), sk)
- if err != nil {
- t.Fatal(err)
- }
- encodedSig := base64.StdEncoding.EncodeToString(sig)
- encodedPub := base64.StdEncoding.EncodeToString([]byte(pk))
-
- req := map[string]string{
- "address": string(onionaddr),
- "pubkey": encodedPub,
- "message": "I am a DAM node!",
- "signature": encodedSig,
- "secret": "",
- }
-
- valid, secret := ValidateFirstHandshake(req)
- if !(valid) {
- t.Fatal("Failed on first handshake.")
- }
-
- sig, err = SignMsgEd25519([]byte(secret), sk)
- if err != nil {
- t.Fatal(err)
- }
- encodedSig = base64.StdEncoding.EncodeToString(sig)
-
- req = map[string]string{
- "address": string(onionaddr),
- "pubkey": encodedPub,
- "message": secret,
- "signature": encodedSig,
- "secret": secret,
- }
-
- if valid, _ = ValidateSecondHandshake(req); !(valid) {
- t.Fatal("Failed to validate second handshake.")
- }
-}
-
-func TestInValidValidateSecondHandshake(t *testing.T) {
- cmd, _ := StartRedis("../../contrib/redis.conf")
- defer cmd.Process.Kill()
-
- pk, sk, _ := GenEd25519()
- onionaddr := OnionFromPubkeyEd25519(pk)
-
- sig, err := SignMsgEd25519([]byte("I am a DAM node!"), sk)
- if err != nil {
- t.Fatal(err)
- }
- encodedSig := base64.StdEncoding.EncodeToString(sig)
- encodedPub := base64.StdEncoding.EncodeToString([]byte(pk))
-
- req := map[string]string{
- "address": string(onionaddr),
- "pubkey": encodedPub,
- "message": "I am a DAM node!",
- "signature": encodedSig,
- "secret": "",
- }
-
- valid, secret := ValidateFirstHandshake(req)
- if !(valid) {
- t.Fatal("Failed on first handshake.")
- }
-
- sig, err = SignMsgEd25519([]byte(secret), sk)
- if err != nil {
- t.Fatal(err)
- }
- encodedSig = base64.StdEncoding.EncodeToString(sig)
-
- secret = "We're malicious!"
-
- req = map[string]string{
- "address": string(onionaddr),
- "pubkey": encodedPub,
- "message": secret,
- "signature": encodedSig,
- "secret": secret,
- }
-
- if valid, _ = ValidateSecondHandshake(req); valid {
- t.Fatal("Invalid second handshake passed as valid.")
- }
-}
diff --git a/protocol.md b/protocol.md
@@ -1,104 +0,0 @@
-Tor DAM Protocol
-================
-
-Abstract
---------
-
-* Every node has a HTTP API allowing to list other nodes and announce
- new ones.
-* They keep propagating to all valid nodes they know.
-* Announcing implies the need of knowledge of at least one node.
- * It is possible to make this random enough once there are at least 6
- nodes in the network.
-* A node announces itself to others by sending a JSON-formatted HTTP
- POST request to one or more active node.
- * Once the POST request is received, the node will validate the
- request and return a random string (nonce) back to the requester for
- them to sign with their cryptographic key.
- * The requester will try to sign this nonce and return it back to the
- node it's announcing to, so the node can confirm the requester is in
- actual posession of the private key.
-* Tor DAM **does not validate** if a node is malicious or not. This is a
- layer that has to be established with external software.
-
-
-Protocol
---------
-
-A node announcing itself has to do a JSON-formatted HTTP POST request to
-one or more active nodes with the format explained below. **N.B.** The
-strings shown in this document might not be valid, but they represent a
-correct example.
-
-* `address` holds the address of the Tor hidden service.
-* `pubkey` is the base64 encoded ed25519 public key of the Tor hidden
- service.
-* `message` is the message that has to be signed using the private key
- of this same hidden service.
-* `signature` is the base64 encoded signature of the above message.
-* `secret` is a string that is used for exchanging messages between the
- client and server.
-
-
-```
-{
- "address": "gphjf5g3d5ywehwrd7cv3czymtdc6ha67bqplxwbspx7tioxt7gxqiid.onion",
- "pubkey": "M86S9NsfcWIe0R/FXYs4ZMYvHB74YPXewZPv+aHXn80=",
- "message" "I am a DAM node!",
- "signature": "CWqptO9ZRIvYMIHd3XHXaVny+W23P8FGkfbn5lvUqeJbDcY3G8+B4G8iCCIQiZkxkMofe6RbstHn3L1x88c3AA==",
- "secret": ""
-}
-```
-
-Sending this as a POST request to a node will make it verify the
-signature, and following that, the node will generate a
-cryptographically secure random string, encode it using base64 and
-return it back to the client for them to sign:
-
-
-```
-{
- "secret": "NmtDOEsrLGI8eCk1TyxOfXcwRV5lI0Y5fnhbXlAhV1dGfTl8K2JAYEQrU2lAJ2UuJ2kjQF15Q30+SWVXTkFnXw=="
-}
-```
-
-The client will try to decode and sign this secret. Then it will be
-reencoded using base64 and sent back for verification to complete its
-part of the handshake. The POST request this time will contain the
-following data:
-
-
-```
-{
- "address": "gphjf5g3d5ywehwrd7cv3czymtdc6ha67bqplxwbspx7tioxt7gxqiid.onion",
- "pubkey": "M86S9NsfcWIe0R/FXYs4ZMYvHB74YPXewZPv+aHXn80=",
- "message": "NFU5PXU4LT4xVy5NW303IWo1SD0odSohOHEvPThoemM3LHdcW3NVcm1TU3RAPGM8Pi1UUXpKIipWWnlTUk5kIg==",
- "signature": "1cocZey3KpuRDfRrKcI3tc4hhJpwfXU3BC3o3VE8wkkCpCFJ5Xl3wl58GLSVS4BdbDAFrf+KFpjtDLhOuSMYAw==",
- "secret": "NFU5PXU4LT4xVy5NW303IWo1SD0odSohOHEvPThoemM3LHdcW3NVcm1TU3RAPGM8Pi1UUXpKIipWWnlTUk5kIg=="
-}
-```
-
-
-The node will verify the received secret against the public key it has
-archived already. If the verification yields no errors, we assume that
-the requester is actually in possession of the private key. If the node
-is not valid in our database, we will complete the handshake by
-welcoming the client into the network:
-
-```
-{
- "secret": "Welcome to the DAM network!"
-}
-```
-
-
-Further on, the node will append useful metadata to the struct. We will
-add the encoded public key, timestamps of when the client was first seen
-and last seen, and a field to indicate if the node is valid. The latter
-is not to be handled by Tor DAM, but rather an upper layer, which
-actually has consensus handling.
-
-If a requesting/announcing node is valid in another node's database, the
-remote node will then propagate back all the valid nodes it knows back
-to the client in a gzipped and base64 encoded JSON struct. The client
-will then process this and update its own database accordingly.
diff --git a/python/Makefile b/python/Makefile
@@ -1,20 +0,0 @@
-# See LICENSE file for copyright and license details.
-
-PREFIX ?= /usr/local
-
-BIN = damhs.py
-
-all:
- @echo 'Run "make install" to install to $(DESTDIR)$(PREFIX)/bin'
-
-install:
- @echo 'Installing to $(DESTDIR)$(PREFIX)/bin'
- mkdir -p $(DESTDIR)$(PREFIX)/bin
- cp -f $(BIN) $(DESTDIR)$(PREFIX)/bin
- for f in $(BIN); do chmod 755 "$(DESTDIR)$(PREFIX)/bin/$$f"; done
-
-uninstall:
- @echo 'Uninstalling from $(DESTDIR)$(PREFIX)/bin'
- for f in $(BIN); do rm -f "$(DESTDIR)$(PREFIX)/bin/$$f"; done
-
-.PHONY: all install uninstall
diff --git a/python/damhs.py b/python/damhs.py
@@ -1,81 +0,0 @@
-#!/usr/bin/env python3
-# Copyright (c) 2017-2018 Dyne.org Foundation
-# tor-dam is written and maintained by Ivan Jelincic <parazyd@dyne.org>
-#
-# This file is part of tor-dam
-#
-# 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 <http://www.gnu.org/licenses/>.
-
-"""
-Controller daemon running the ephemeral hidden service.
-
-Usage: damhs.py <path_to_private.key> <portmap>
-
-<portmap> is a comma-separated string of at least one of the
-following element: 80:49371 (80 is the remote, 49371 is local)
-"""
-
-from argparse import ArgumentParser
-from sys import stdout
-from time import sleep
-
-from stem.control import Controller
-
-
-def start_hs(ctl=None, ktype=None, kcont=None, portmap=None):
- """
- Function starting our ephemeral hidden service
- """
- return ctl.create_ephemeral_hidden_service(portmap, key_type=ktype,
- key_content=kcont,
- await_publication=True)
-
-
-def main():
- """
- Main loop
- """
- parser = ArgumentParser()
- parser.add_argument('-k', '--private-key',
- help='Path to the ed25519 private key',
- default='/home/decode/.dam/private.key')
- parser.add_argument('-p', '--port-map',
- help='Comma-separated string of local:remote ports',
- default='80:49731,5000:5000')
- args = parser.parse_args()
-
- ctl = Controller.from_port()
- ctl.authenticate(password='topkek')
-
- portmap = {}
- ports = args.port_map.split(',')
- for i in ports:
- tup = i.split(':')
- portmap[int(tup[0])] = int(tup[1])
-
- keyfile = args.private_key
- ktype = 'ED25519-V3'
- kcont = open(keyfile).read()
-
- service = start_hs(ctl=ctl, ktype=ktype, kcont=kcont, portmap=portmap)
-
- stdout.write('Started HS at %s.onion\n' % service.service_id)
- stdout.write('OK\n')
- stdout.flush()
- while True:
- sleep(60)
-
-
-if __name__ == '__main__':
- main()
diff --git a/redis.go b/redis.go
@@ -0,0 +1,141 @@
+package main
+
+/*
+ * Copyright (c) 2017-2021 Ivan Jelincic <parazyd@dyne.org>
+ *
+ * This file is part of tor-dam
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+import (
+ "context"
+ "fmt"
+ "log"
+ "os/exec"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/go-redis/redis"
+)
+
+// rctx is the Redis context (necessary in newer go-redis)
+var rctx = context.Background()
+var rcli *redis.Client
+
+func pollPrune(interval int64) {
+ for {
+ log.Println("Polling redis for expired nodes")
+ nodes, err := rcli.Keys(rctx, "*.onion").Result()
+ if err != nil {
+ log.Println("WARNING: Nonfatal error in pollPrune:", err.Error())
+ }
+ now := time.Now().Unix()
+
+ for _, i := range nodes {
+ res, err := rcli.HGet(rctx, i, "lastseen").Result()
+ if err != nil {
+ log.Println("WARNING: Nonfatal error in pollPrune:", err.Error())
+ continue
+ }
+ ls, err := strconv.Atoi(res)
+ if err != nil {
+ log.Println("WARNING: Nonfatal error in pollPrune:", err.Error())
+ continue
+ }
+
+ diff := (now - int64(ls)) / 60
+ if diff > interval {
+ log.Printf("Deleting %s (expired)\n", i)
+ publishToRedis('D', i)
+ rcli.Del(rctx, i)
+ }
+ }
+ time.Sleep(time.Duration(interval) * time.Minute)
+ }
+}
+
+func publishToRedis(mt rune, addr string) {
+ data, err := rcli.HGetAll(rctx, addr).Result()
+ if err != nil {
+ log.Println("WARNING: Nonfatal err in publishToRedis:", err.Error())
+ return
+ }
+
+ if data["lastseen"] == data["firstseen"] {
+ mt = 'A'
+ } else if mt != 'D' {
+ mt = 'M'
+ }
+
+ // TODO: First of the "addr" references could be alias/nickname
+
+ rcli.Publish(rctx, pubsubChan, fmt.Sprintf("%s|%s|%v|%s",
+ data["lastseen"], addr, mt, addr))
+}
+
+func newredisrc(dir string) string {
+ return fmt.Sprintf(`daemonize no
+bind %s
+port %d
+databases 1
+dir %s
+dbfilename tor-dam.rdb
+save 900 1
+save 300 10
+save 60 10000
+rdbcompression yes
+rdbchecksum yes
+stop-writes-on-bgsave-error no`,
+ redisAddr.IP.String(), redisAddr.Port, dir)
+}
+
+func spawnRedis() (*exec.Cmd, error) {
+ var err error
+ redisAddr, err = getListener()
+ if err != nil {
+ return nil, err
+ }
+
+ rcli = redis.NewClient(&redis.Options{
+ Addr: redisAddr.String(),
+ Password: "",
+ DB: 0,
+ })
+
+ log.Println("Forking Redis daemon on", redisAddr.String())
+
+ cmd := exec.Command("redis-server", "-")
+ cmd.Stdin = strings.NewReader(newredisrc(*workdir))
+
+ err = cmd.Start()
+ if err != nil {
+ return nil, err
+ }
+
+ time.Sleep(500 * time.Millisecond)
+ if _, err := rcli.Ping(rctx).Result(); err != nil {
+ return cmd, err
+ }
+
+ pubsub := rcli.Subscribe(rctx, pubsubChan)
+ if _, err := pubsub.Receive(rctx); err != nil {
+ return cmd, err
+ }
+
+ log.Printf("Created \"%s\" channel in Redis\n", pubsubChan)
+
+ return cmd, nil
+}
diff --git a/tor-dam.go b/tor-dam.go
@@ -0,0 +1,191 @@
+package main
+
+/*
+ * Copyright (c) 2017-2021 Ivan Jelincic <parazyd@dyne.org>
+ *
+ * This file is part of tor-dam
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+import (
+ "crypto/ed25519"
+ "encoding/base64"
+ "flag"
+ "fmt"
+ "io/ioutil"
+ "log"
+ "net"
+ "net/http"
+ "net/url"
+ "os"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+)
+
+var (
+ noremote = flag.Bool("n", false, "Don't fetch remote entrypoints")
+ generate = flag.Bool("g", false, "(Re)generate keys and exit")
+ annint = flag.Int("i", 5, "Announce interval (in minutes)")
+ remote = flag.String("r", "https://parazyd.org/pub/tmp/tor-dam-dirs.txt",
+ "Remote list of entrypoints (comma-separated)")
+ portmap = flag.String("p", "13010:13010,13011:13011,5000:5000",
+ "Map of ports forwarded to/from Tor")
+ expiry = flag.Int64("e", 0, "Node expiry time in minutes (0=unlimited)")
+ trustall = flag.Bool("t", false, "Trust all new nodes automatically")
+ listen = "127.0.0.1:49371"
+ //listen = flag.String("l", "127.0.0.1:49371",
+ //"Listen address for daemon (Will also map in Tor HS)")
+ workdir = flag.String("d", os.Getenv("HOME")+"/.dam", "Working directory")
+)
+
+func flagSanity() error {
+ for _, i := range strings.Split(*remote, ",") {
+ if _, err := url.ParseRequestURI(i); err != nil {
+ return fmt.Errorf("invalid URL \"%s\" in remote entrypoints", i)
+ }
+ }
+
+ for _, i := range strings.Split(*portmap, ",") {
+ t := strings.Split(i, ":")
+ if len(t) != 2 {
+ return fmt.Errorf("invalid portmap: %s (len != 2)", i)
+ }
+ if _, err := strconv.Atoi(t[0]); err != nil {
+ return fmt.Errorf("invalid portmap: %s (%s)", i, err)
+ }
+ if _, err := strconv.Atoi(t[1]); err != nil {
+ return fmt.Errorf("invalid portmap: %s (%s)", i, err)
+ }
+ }
+
+ if _, err := net.ResolveTCPAddr("tcp", listen); err != nil {
+ return fmt.Errorf("invalid listen address: %s (%s)", listen, err)
+ }
+
+ return nil
+}
+
+func main() {
+ flag.Parse()
+ var wg sync.WaitGroup
+ var err error
+
+ if err := flagSanity(); err != nil {
+ log.Fatal(err)
+ }
+
+ if *generate {
+ if err := generateED25519Keypair(*workdir); err != nil {
+ log.Fatal(err)
+ }
+ os.Exit(0)
+ }
+
+ signingKey, err = loadED25519Seed(strings.Join(
+ []string{*workdir, seedName}, "/"))
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ tor, err := spawnTor()
+ defer tor.Process.Kill()
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ red, err := spawnRedis()
+ defer red.Process.Kill()
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ mux := http.NewServeMux()
+ mux.HandleFunc("/announce", handleAnnounce)
+ mux.HandleFunc("/", handleElse)
+ srv := &http.Server{
+ Addr: listen,
+ Handler: mux,
+ ReadTimeout: 30 * time.Second,
+ WriteTimeout: 30 * time.Second,
+ }
+
+ go srv.ListenAndServe()
+ log.Println("tor-dam directory listening on", listen)
+
+ if *trustall {
+ log.Println("Trustall enabled, will mark all nodes trusted by default")
+ }
+
+ if *expiry > 0 {
+ log.Printf("Enabling db prune polling (%d minute interval)\n", *expiry)
+ go pollPrune(*expiry)
+ }
+
+ onionaddr, err := ioutil.ReadFile(strings.Join([]string{
+ *workdir, "hs", "hostname"}, "/"))
+ if err != nil {
+ log.Fatal(err)
+ }
+ onionaddr = []byte(strings.TrimSuffix(string(onionaddr), "\n"))
+ log.Printf("Our hostname is: %s\n", string(onionaddr))
+
+ // Network entrypoints. These files hold the lists of nodes we can announce
+ // to initially. Format is "DIR:unlikelynameforan.onion", other lines are
+ // ignored and can be used as comments or siimilar.
+ epLists := strings.Split(*remote, ",")
+
+ for {
+ log.Println("Announcing to nodes...")
+ var ann = 0 // Track of successful authentications
+ nodes, err := fetchNodeList(epLists, *noremote)
+ if err != nil {
+ // No route to host, or failed download. Try later.
+ log.Printf("Failed to fetch nodes, retrying in 1m (%s)\n", err)
+ time.Sleep(60 * time.Second)
+ continue
+ }
+
+ sigmsg := []byte("Hi tor-dam!")
+
+ nv := map[string]string{
+ "address": string(onionaddr),
+ "pubkey": base64.StdEncoding.EncodeToString(signingKey.Public().(ed25519.PublicKey)),
+ "message": string(sigmsg),
+ "signature": base64.StdEncoding.EncodeToString(ed25519.Sign(signingKey, sigmsg)),
+ "secret": "",
+ }
+
+ for _, i := range nodes {
+ wg.Add(1)
+ go func(x string) {
+ valid, err := announce(x, nv)
+ if err != nil {
+ log.Printf("%s: %s\n", x, err)
+ }
+ if valid {
+ ann++
+ }
+ wg.Done()
+ }(i)
+ }
+ wg.Wait()
+
+ log.Printf("%d successful authentications\n", ann)
+ log.Printf("Waiting %d min before next announce\n", *annint)
+ time.Sleep(time.Duration(*annint) * time.Minute)
+ }
+}
diff --git a/tor.go b/tor.go
@@ -0,0 +1,68 @@
+package main
+
+/*
+ * Copyright (c) 2017-2021 Ivan Jelincic <parazyd@dyne.org>
+ *
+ * This file is part of tor-dam
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+import (
+ "fmt"
+ "log"
+ "os/exec"
+ "strings"
+)
+
+func newtorrc(dir string) string {
+ var pm []string
+
+ for _, i := range strings.Split(*portmap, ",") {
+ 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 %s/tor
+SocksPort %s
+HiddenServiceDir %s/hs
+HiddenServicePort %s %s
+%s
+`,
+ dir, torAddr.String(), dir, strings.Split(listen, ":")[1],
+ listen, strings.Join(pm, "\n"))
+}
+
+func spawnTor() (*exec.Cmd, error) {
+ var err error
+ torAddr, err = getListener()
+ if err != nil {
+ return nil, err
+ }
+
+ log.Println("Forking Tor daemon on", torAddr.String())
+
+ cmd := exec.Command("tor", "-f", "-")
+ cmd.Stdin = strings.NewReader(newtorrc(*workdir))
+
+ err = cmd.Start()
+ if err != nil {
+ return nil, err
+ }
+
+ return cmd, nil
+}
diff --git a/types.go b/types.go
@@ -0,0 +1,37 @@
+package main
+
+/*
+ * Copyright (c) 2017-2021 Ivan Jelincic <parazyd@dyne.org>
+ *
+ * This file is part of tor-dam
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+// Message represents the message struct type
+type Message struct {
+ Secret string `json:"secret"`
+}
+
+// Node represents the node struct type
+type Node struct {
+ Address string `json:"address"`
+ Message string `json:"message"`
+ Signature string `json:"signature"`
+ Secret string `json:"secret"`
+ Pubkey string `json:"pubkey"`
+ Firstseen int64 `json:"firstseen"`
+ Lastseen int64 `json:"lastseen"`
+ Trusted int `json:"trusted"`
+}
diff --git a/validate.go b/validate.go
@@ -0,0 +1,158 @@
+package main
+
+/*
+ * Copyright (c) 2017-2021 Ivan Jelincic <parazyd@dyne.org>
+ *
+ * This file is part of tor-dam
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+import (
+ "crypto/ed25519"
+ "encoding/base64"
+ "log"
+ "regexp"
+ "time"
+)
+
+func validateOnionAddress(addr string) bool {
+ re, _ := regexp.Compile(`^[a-z2-7](?:.{55})\.onion`)
+ return len(re.FindString(addr)) == 62
+}
+
+// firstHandshake will take the incoming public key either from the request
+// or, if found, from redis. This key is stored, and a nonce is generated.
+// This nonce is returned back to the client to sign with the key. In the
+// second handshake, we verify this nonce signature against the retrieved
+// public key.
+func firstHandshake(req map[string]string) (bool, string) {
+ var pubstr string
+
+ // Check if we have seen this node already
+ ex, err := rcli.Exists(rctx, req["address"]).Result()
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ if ex == 1 {
+ // We saw it so we should hae the public key stored in redis.
+ // If we do not, that is an internal error.
+ pubstr, err = rcli.HGet(rctx, req["address"], "pubkey").Result()
+ if err != nil {
+ log.Fatal(err)
+ }
+ } else {
+ // We take it from the request
+ pubstr = req["pubkey"]
+ }
+
+ randString, err := genRandomASCII(64)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ enc := base64.StdEncoding.EncodeToString([]byte(randString))
+
+ var info = map[string]interface{}{
+ "address": req["address"],
+ "message": enc,
+ "signature": req["signature"],
+ "secret": enc,
+ "lastseen": time.Now().Unix(),
+ } // Can not cast, need this for HSet
+
+ if ex != 1 {
+ // We did not have this node in redis
+ info["pubkey"] = pubstr
+ info["firstseen"] = time.Now().Unix()
+ if *trustall {
+ info["trusted"] = 1
+ } else {
+ info["trusted"] = 0
+ }
+ }
+
+ log.Printf("%s: Writing to redis\n", req["address"])
+ if _, err := rcli.HSet(rctx, req["address"], info).Result(); err != nil {
+ log.Fatal(err)
+ }
+
+ return true, enc
+}
+
+func secondHandshake(req map[string]string) (bool, string) {
+ // Check if we have seen this node already
+ ex, err := rcli.Exists(rctx, req["address"]).Result()
+ if err != nil {
+ log.Fatal(err)
+ }
+ if ex != 1 {
+ log.Printf("%s tried to jump in 2/2 handshake before getting a nonce\n",
+ req["address"])
+ return false, "We have not seen you before. Authenticate properly."
+ }
+
+ // We saw it so we should have the public key in redis. If we do not,
+ // then it's an internal error.
+ pubstr, err := rcli.HGet(rctx, req["address"], "pubkey").Result()
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ lSec, err := rcli.HGet(rctx, req["address"], "secret").Result()
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ if lSec != req["secret"] || lSec != req["message"] {
+ log.Printf("%s: Secrets didn't match\n", req["address"])
+ return false, "Secrets didn't match."
+ }
+
+ // Validate signature.
+ msg := []byte(lSec)
+ sig, _ := base64.StdEncoding.DecodeString(req["signature"])
+ deckey, err := base64.StdEncoding.DecodeString(pubstr)
+ if err != nil {
+ log.Fatal(err)
+ }
+ pubkey := ed25519.PublicKey(deckey)
+
+ if !ed25519.Verify(pubkey, msg, sig) {
+ log.Println("crypto/ed25519: Signature verification failure")
+ return false, "Signature verification failure"
+ }
+
+ // The request is valid at this point
+
+ // Make a new random secret to prevent reuse.
+ randString, _ := genRandomASCII(64)
+ encSecret := base64.StdEncoding.EncodeToString([]byte(randString))
+
+ var info = map[string]interface{}{
+ "address": req["address"],
+ "message": encSecret,
+ "signature": req["signature"],
+ "secret": encSecret,
+ "lastseen": time.Now().Unix(),
+ } // TODO: Use struct
+
+ log.Printf("Adding %s to redis\n", req["address"])
+ if _, err := rcli.HSet(rctx, req["address"], info).Result(); err != nil {
+ log.Fatal(err)
+ }
+
+ return true, "Welcome to tor-dam"
+}