tordam

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

commit 2f8bd41a607d578b727c1c8ee20f10b2cebb1bdc
parent 625b81777d0403bc36ebada9ecafd04071aef6f3
Author: parazyd <parazyd@dyne.org>
Date:   Sun,  7 Mar 2021 20:07:26 +0100

Remove old code.

Diffstat:
MREADME.md | 49++++++++-----------------------------------------
Dannounce.go | 179-------------------------------------------------------------------------------
Dapi.go | 191-------------------------------------------------------------------------------
Dconfig.go | 36------------------------------------
Acontrib/tordam.png | 0
Dcrypto.go | 63---------------------------------------------------------------
Dhelpers.go | 88-------------------------------------------------------------------------------
Dnet.go | 83-------------------------------------------------------------------------------
Dredis.go | 141-------------------------------------------------------------------------------
Dtor-dam.go | 191-------------------------------------------------------------------------------
Dtor.go | 68--------------------------------------------------------------------
Dtypes.go | 37-------------------------------------
Dvalidate.go | 158-------------------------------------------------------------------------------
13 files changed, 8 insertions(+), 1276 deletions(-)

diff --git a/README.md b/README.md @@ -1,55 +1,22 @@ tor-dam (Tor Distributed Announce Mechanism) ============================================ -Protocol and tooling for mapping machines in the Tor network running -this software. +![tordam](contrib/tordam.png) -![Network visualization](https://raw.githubusercontent.com/parazyd/tor-dam/master/contrib/network.gif) +A library for peer discovery inside the Tor network. Installation ------------ ``` -go get github.com/parazyd/tor-dam +go get github.com/parazyd/tordam ``` -Usage ------ +Documentation +------------- -``` -Usage of ./tor-dam: - -d string - Working directory (default "/home/parazyd/.dam") - -e int - Node expiry time in minutes (0=unlimited) - -g (Re)generate keys and exit - -i int - Announce interval (in minutes) (default 5) - -n Don't fetch remote entrypoints - -p string - Map of ports forwarded to/from Tor (default "13010:13010,13011:13011,5000:5000") - -r string - Remote list of entrypoints (comma-separated) (default "https://parazyd.org/pub/tmp/tor-dam-dirs.txt") - -t Trust all new nodes automatically -``` +https://pkg.go.dev/github.com/parazyd/tordam -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. +tor-dam is a small library that can be used to facilitate peer to peer +services in the Tor network with simple mechanisms. diff --git a/announce.go b/announce.go @@ -1,179 +0,0 @@ -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 @@ -1,191 +0,0 @@ -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/config.go b/config.go @@ -1,36 +0,0 @@ -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/tordam.png b/contrib/tordam.png Binary files differ. diff --git a/crypto.go b/crypto.go @@ -1,63 +0,0 @@ -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 @@ -1,88 +0,0 @@ -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 @@ -1,83 +0,0 @@ -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/redis.go b/redis.go @@ -1,141 +0,0 @@ -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 @@ -1,191 +0,0 @@ -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 @@ -1,68 +0,0 @@ -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 @@ -1,37 +0,0 @@ -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 @@ -1,158 +0,0 @@ -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" -}