tor-dam

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

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:
DMakefile | 16----------------
MREADME.md | 87++++++++++++++++++++++---------------------------------------------------------
DTODO.md | 2--
Aannounce.go | 179+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapi.go | 191+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dcmd/dam-client/main.go | 338-------------------------------------------------------------------------------
Dcmd/dam-dir/main.go | 273-------------------------------------------------------------------------------
Dcmd/dam-gource/main.go | 42------------------------------------------
Aconfig.go | 36++++++++++++++++++++++++++++++++++++
Dcontrib/Makefile | 35-----------------------------------
Acontrib/README.md | 43+++++++++++++++++++++++++++++++++++++++++++
Dcontrib/dam-client.conf | 13-------------
Dcontrib/dam-client.init | 31-------------------------------
Dcontrib/dam-dir.conf | 13-------------
Dcontrib/dam-dir.init | 32--------------------------------
Mcontrib/echo_recv.py | 14+++++++-------
Mcontrib/echo_send.py | 21+++++++++++----------
Acontrib/gource.go | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dcontrib/redis.conf | 20--------------------
Dcontrib/torrc | 16----------------
Acrypto.go | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ahelpers.go | 88+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Anet.go | 83+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dpkg/damlib/config.go | 55-------------------------------------------------------
Dpkg/damlib/crypto_25519.go | 137-------------------------------------------------------------------------------
Dpkg/damlib/helpers.go | 105-------------------------------------------------------------------------------
Dpkg/damlib/helpers_test.go | 65-----------------------------------------------------------------
Dpkg/damlib/net.go | 86-------------------------------------------------------------------------------
Dpkg/damlib/redis.go | 92-------------------------------------------------------------------------------
Dpkg/damlib/validate.go | 211-------------------------------------------------------------------------------
Dpkg/damlib/validate_test.go | 161-------------------------------------------------------------------------------
Dprotocol.md | 104-------------------------------------------------------------------------------
Dpython/Makefile | 20--------------------
Dpython/damhs.py | 81-------------------------------------------------------------------------------
Aredis.go | 141+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ator-dam.go | 191+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ator.go | 68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atypes.go | 37+++++++++++++++++++++++++++++++++++++
Avalidate.go | 158+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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" +}