gnunet-go

GNUnet Bindings for Go
Log | Files | Refs | README | LICENSE

commit f425c2aeef06d1a6105678c8b058bdde65a26e78
parent 4261e07def81e7c3eb183b9d5c4059a2e9c53759
Author: Bernd Fix <brf@hoi-polloi.org>
Date:   Mon, 18 Jul 2022 11:16:34 +0200

Improved handling of pending DHT results.

Diffstat:
MREADME.md | 52+++++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/gnunet/build.sh | 2+-
Msrc/gnunet/cmd/gnunet-service-dht-go/main.go | 33++++++++++++++++++++++-----------
Msrc/gnunet/cmd/gnunet-service-gns-go/main.go | 9+++++----
Msrc/gnunet/cmd/gnunet-service-revocation-go/main.go | 9+++++----
Msrc/gnunet/cmd/peer_mockup/main.go | 40+++++++++++++++++++++++++++-------------
Msrc/gnunet/cmd/revoke-zonekey/main.go | 80++++++++++++++++++++++++++++++++++++++-----------------------------------------
Msrc/gnunet/cmd/vanityid/main.go | 10++++------
Msrc/gnunet/config/config.go | 56++++++++++++++++++++++----------------------------------
Msrc/gnunet/config/config_test.go | 2+-
Msrc/gnunet/config/gnunet-config.json | 14++++++++++----
Msrc/gnunet/core/core.go | 186+++++++++++++++++++++++++++-----------------------------------------------------
Msrc/gnunet/core/core_test.go | 32++++++++++++++++----------------
Msrc/gnunet/core/event.go | 17+++++++++++++++++
Msrc/gnunet/core/hello_test.go | 1-
Msrc/gnunet/core/peer.go | 13+++++++++----
Msrc/gnunet/core/peer_test.go | 8+++++---
Msrc/gnunet/crypto/gns.go | 109++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
Msrc/gnunet/crypto/gns_edkey.go | 46++++++++++++++++++++++++++--------------------
Msrc/gnunet/crypto/gns_pkey.go | 45++++++++++++++++++++++++++-------------------
Msrc/gnunet/crypto/gns_test.go | 18+++++++++++++-----
Msrc/gnunet/crypto/hash.go | 13++++++++++++-
Msrc/gnunet/crypto/key_exchange_test.go | 52++++++++++++++++++++++++++--------------------------
Msrc/gnunet/crypto/keys_test.go | 2+-
Msrc/gnunet/enums/blocktype_string.go | 2+-
Msrc/gnunet/enums/dht.go | 14++++++--------
Msrc/gnunet/enums/dht_block_type.go | 3++-
Msrc/gnunet/enums/gns.go | 3+++
Msrc/gnunet/enums/gns_type.go | 1+
Msrc/gnunet/enums/gnunet-dht.tpl | 3++-
Msrc/gnunet/enums/gnunet-gns.tpl | 1+
Msrc/gnunet/enums/gnunet-signature.tpl | 1+
Msrc/gnunet/enums/signature_purpose.go | 1+
Msrc/gnunet/go.mod | 3++-
Msrc/gnunet/go.sum | 6++++--
Msrc/gnunet/message/factory.go | 12+++++++++++-
Msrc/gnunet/message/msg_core.go | 30+++++++++++++++---------------
Asrc/gnunet/message/msg_dht_p2p.go | 473+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/gnunet/message/msg_hello.go | 20+++++++++++++++-----
Dsrc/gnunet/message/msg_hello_dht.go | 167-------------------------------------------------------------------------------
Msrc/gnunet/message/msg_namecache.go | 2+-
Msrc/gnunet/message/msg_transport.go | 13++++++++-----
Msrc/gnunet/message/types.go | 5+++--
Msrc/gnunet/service/client.go | 1-
Msrc/gnunet/service/connection.go | 42+++++++++++++++++++++++-------------------
Asrc/gnunet/service/dht/blocks/filters.go | 296+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/gnunet/service/dht/blocks/filters_test.go | 110+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/gnunet/service/dht/blocks/generic.go | 64++++++++++++++++++++++++++++++----------------------------------
Dsrc/gnunet/service/dht/blocks/generic_test.go | 67-------------------------------------------------------------------
Msrc/gnunet/service/dht/blocks/gns.go | 31+++++++++++++++++++++----------
Asrc/gnunet/service/dht/blocks/handlers.go | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/gnunet/service/dht/blocks/hello.go | 253++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Dsrc/gnunet/service/dht/blocks/types.go | 26--------------------------
Dsrc/gnunet/service/dht/bloomfilter.go | 123-------------------------------------------------------------------------------
Dsrc/gnunet/service/dht/dhtstore_test.go | 94-------------------------------------------------------------------------------
Asrc/gnunet/service/dht/messages.go | 380+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/gnunet/service/dht/module.go | 155++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Asrc/gnunet/service/dht/resulthandler.go | 351+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/gnunet/service/dht/routingtable.go | 305++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
Msrc/gnunet/service/dht/routingtable_test.go | 42++++++++++++++++++++++++++++++++----------
Msrc/gnunet/service/dht/rpc.go | 50++++++++++++++++++++++++++++++++++++++++----------
Msrc/gnunet/service/dht/service.go | 56+++++---------------------------------------------------
Msrc/gnunet/service/gns/block_handler.go | 6+++---
Msrc/gnunet/service/gns/dns.go | 4++--
Msrc/gnunet/service/gns/module.go | 30+++++++++++++++++-------------
Msrc/gnunet/service/gns/rpc.go | 4++--
Msrc/gnunet/service/gns/service.go | 22+++++++++++-----------
Msrc/gnunet/service/module.go | 20++++++++------------
Msrc/gnunet/service/namecache/module.go | 9++++++---
Msrc/gnunet/service/revocation/module.go | 7++++---
Msrc/gnunet/service/revocation/pow.go | 27++++++++-------------------
Msrc/gnunet/service/revocation/pow_test.go | 1-
Msrc/gnunet/service/revocation/rpc.go | 4++--
Msrc/gnunet/service/revocation/service.go | 9+++++----
Msrc/gnunet/service/rpc.go | 29+++++++++++++++++++----------
Msrc/gnunet/service/service.go | 3+--
Dsrc/gnunet/service/store.go | 502-------------------------------------------------------------------------------
Asrc/gnunet/service/store/database.go | 186+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/gnunet/service/store/dhtstore_test.go | 93+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/gnunet/service/store/store.go | 278+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/gnunet/service/store/store_fs.go | 287+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/gnunet/service/store/store_fs_meta.go | 174+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/gnunet/service/store/store_fs_meta.sql | 29+++++++++++++++++++++++++++++
Dsrc/gnunet/test/gnunet-dhtu/main.go | 206-------------------------------------------------------------------------------
Msrc/gnunet/transport/endpoint.go | 31++++++++++++++++---------------
Msrc/gnunet/transport/reader_writer.go | 41++++++++++++++++-------------------------
Msrc/gnunet/transport/responder.go | 8++++++++
Msrc/gnunet/transport/transport.go | 32+++++++++++++++++++++-----------
Msrc/gnunet/util/address.go | 56++++++++++++++++++++++----------------------------------
Msrc/gnunet/util/address_test.go | 13++++++++++---
Msrc/gnunet/util/array.go | 5+++++
Msrc/gnunet/util/base32.go | 4++--
Msrc/gnunet/util/base32_test.go | 2+-
Dsrc/gnunet/util/database.go | 185-------------------------------------------------------------------------------
Msrc/gnunet/util/fs.go | 2+-
Asrc/gnunet/util/map.go | 151++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/gnunet/util/misc.go | 105++++++++-----------------------------------------------------------------------
Asrc/gnunet/util/peer.go | 94+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dsrc/gnunet/util/peer_id.go | 59-----------------------------------------------------------
Msrc/gnunet/util/rnd.go | 12+++++++++---
Msrc/gnunet/util/time.go | 17++++++++++-------
101 files changed, 4423 insertions(+), 2459 deletions(-)

diff --git a/README.md b/README.md @@ -137,6 +137,56 @@ chown gnunet:gnunet /var/lib/gnunet/.local/share/gnunet/private_key.ecc chmod 600 /var/lib/gnunet/.local/share/gnunet/private_key.ecc ``` -For gnunet-go configuration files you need to paste the result of +For `gnunet-go` configuration files you need to paste the result of `echo "<hex.seed>" | xxd -r -p | base64` into the `PrivateSeed` field in the `NodeConfig` section. + +## Using gnunet-go in your own projects + +`gnunet-go` is not a standard Go module for direct use (via go.mod) in other +packages, but designed as a stand-alone application. The rationale behind was +to **not** hard link the code to a single Git provider. + +If you are interested in using (parts of) `gnunet-go` in your own projects, the +following step-by-step instructions show the easiest route. + +* `git clone https://github.com/bfix/gnunet-go` into folder +`/home/user/gnunet-go` (or any other folder if you adjust the instructions +accordingly). + +* create project folder and change into it +* run `go mod init test` (replace test with the name of your project) +* create a simple test `main.go` + +```go +package main + +import ( + "crypto/rand" + "fmt" + "gnunet/util" +) + +func main() { + a := make([]byte, 32) + rand.Read(a) + fmt.Println(util.EncodeBinaryToString(a)) +} +``` + +* edit `go.mod` and add at end of file: + +```bash +require gnunet v0.1.27 + +replace gnunet v0.1.27 => /home/user/gnunet-go/src/gnunet +``` + +* run `go mod tidy` +* build test program: `go build` +* run test program: `./test` + +The only disadvantage of this approach is that you have to update the source +code for `gnunet-go` yourself. From time to time or on demand, do a `git pull` +followed by a `go mod tidy` described above. No version control is supported +either because the dependency for `gnunet-go` is redirected to a local folder. diff --git a/src/gnunet/build.sh b/src/gnunet/build.sh @@ -1,4 +1,4 @@ #!/bin/bash -go generate ./... +[ "$1" = "withgen" ] && go generate ./... go install -v -gcflags "-N -l" ./... diff --git a/src/gnunet/cmd/gnunet-service-dht-go/main.go b/src/gnunet/cmd/gnunet-service-dht-go/main.go @@ -21,7 +21,6 @@ package main import ( "context" "flag" - "net/rpc" "os" "os/signal" "strings" @@ -58,7 +57,7 @@ func main() { flag.StringVar(&cfgFile, "c", "gnunet-config.json", "GNUnet configuration file") flag.StringVar(&socket, "s", "", "GNS service socket") flag.StringVar(&param, "p", "", "socket parameters (<key>=<value>,...)") - flag.IntVar(&logLevel, "L", logger.INFO, "DHT log level (default: INFO)") + flag.IntVar(&logLevel, "L", logger.DBG, "DHT log level (default: DBG)") flag.StringVar(&rpcEndp, "R", "", "JSON-RPC endpoint (default: none)") flag.Parse() @@ -71,7 +70,7 @@ func main() { // apply configuration logger.SetLogLevel(logLevel) if len(socket) == 0 { - socket = config.Cfg.GNS.Service.Socket + socket = config.Cfg.DHT.Service.Socket } params := make(map[string]string) if len(param) > 0 { @@ -80,7 +79,7 @@ func main() { params[kv[0]] = kv[1] } } else { - params = config.Cfg.GNS.Service.Params + params = config.Cfg.DHT.Service.Params } // instantiate core service @@ -93,8 +92,8 @@ func main() { defer c.Shutdown() // start a new DHT service - var dhtSrv service.Service - if dhtSrv, err = dht.NewService(ctx, c); err != nil { + var dhtSrv *dht.Service + if dhtSrv, err = dht.NewService(ctx, c, config.Cfg.DHT); err != nil { logger.Printf(logger.ERROR, "[dht] failed to create DHT service: %s\n", err.Error()) return } @@ -104,6 +103,14 @@ func main() { return } + // hande network size estimation: if a fixed number of peers are present + // in the network config, use that value; otherwise utilize the NSE + // algorithm (not implemented yet) + numPeers := config.Cfg.Network.NumPeers + if numPeers != 0 { + dhtSrv.SetNetworkSize(numPeers) + } + // handle command-line arguments for RPC if len(rpcEndp) > 0 { parts := strings.Split(rpcEndp, ":") @@ -115,8 +122,8 @@ func main() { } // start JSON-RPC server on request if ep := config.Cfg.RPC.Endpoint; len(ep) > 0 { - var rpc *rpc.Server - if rpc, err = service.StartRPC(ctx, ep); err != nil { + var rpc *service.JRPCServer + if rpc, err = service.RunRPCServer(ctx, ep); err != nil { logger.Printf(logger.ERROR, "[dht] RPC failed to start: %s", err.Error()) return } @@ -125,7 +132,7 @@ func main() { // handle bootstrap: collect known addresses bsList := make([]*util.Address, 0) - for _, bs := range config.Cfg.Bootstrap.Nodes { + for _, bs := range config.Cfg.Network.Bootstrap { // check for HELLO URL if strings.HasPrefix(bs, "gnunet://hello/") { var hb *blocks.HelloBlock @@ -147,7 +154,9 @@ func main() { } // send HELLO to all bootstrap addresses for _, addr := range bsList { - c.SendHello(ctx, addr) + if err := dhtSrv.SendHello(ctx, addr); err != nil { + logger.Printf(logger.ERROR, "[dht] send HELLO failed: %s", err.Error()) + } } // handle OS signals @@ -181,5 +190,7 @@ loop: // terminating service cancel() - srv.Stop() + if err := srv.Stop(); err != nil { + logger.Printf(logger.ERROR, "[dht] Failed to stop service: %s", err.Error()) + } } diff --git a/src/gnunet/cmd/gnunet-service-gns-go/main.go b/src/gnunet/cmd/gnunet-service-gns-go/main.go @@ -21,7 +21,6 @@ package main import ( "context" "flag" - "net/rpc" "os" "os/signal" "strings" @@ -109,8 +108,8 @@ func main() { } // start JSON-RPC server on request if ep := config.Cfg.RPC.Endpoint; len(ep) > 0 { - var rpc *rpc.Server - if rpc, err = service.StartRPC(ctx, ep); err != nil { + var rpc *service.JRPCServer + if rpc, err = service.RunRPCServer(ctx, ep); err != nil { logger.Printf(logger.ERROR, "[gns] RPC failed to start: %s", err.Error()) return } @@ -148,5 +147,7 @@ loop: // terminating service cancel() - srv.Stop() + if err = srv.Stop(); err != nil { + logger.Printf(logger.ERROR, "[gns] Failed to stop service: %s", err.Error()) + } } diff --git a/src/gnunet/cmd/gnunet-service-revocation-go/main.go b/src/gnunet/cmd/gnunet-service-revocation-go/main.go @@ -21,7 +21,6 @@ package main import ( "context" "flag" - "net/rpc" "os" "os/signal" "strings" @@ -109,8 +108,8 @@ func main() { } // start JSON-RPC server on request if ep := config.Cfg.RPC.Endpoint; len(ep) > 0 { - var rpc *rpc.Server - if rpc, err = service.StartRPC(ctx, ep); err != nil { + var rpc *service.JRPCServer + if rpc, err = service.RunRPCServer(ctx, ep); err != nil { logger.Printf(logger.ERROR, "[revocation] RPC failed to start: %s", err.Error()) return } @@ -148,5 +147,7 @@ loop: // terminating service cancel() - srv.Stop() + if err := srv.Stop(); err != nil { + logger.Printf(logger.ERROR, "[revocation] Failed to stop service: %s", err.Error()) + } } diff --git a/src/gnunet/cmd/peer_mockup/main.go b/src/gnunet/cmd/peer_mockup/main.go @@ -15,6 +15,7 @@ import ( "gnunet/message" "gnunet/service" + "github.com/bfix/gospel/crypto/ed25519" "github.com/bfix/gospel/logger" ) @@ -33,10 +34,9 @@ var ( }, } // configuration for remote node - remoteCfg = "3GXXMNb5YpIUO7ejIR2Yy0Cf5texuLfDjHkXcqbPxkc=" - remoteAddr = "udp://172.17.0.5:2086" + remoteCfg = "3GXXMNb5YpIUO7ejIR2Yy0Cf5texuLfDjHkXcqbPxkc=" - // top-level variables used accross functions + // top-level variables used across functions local *core.Peer // local peer (with private key) remote *core.Peer // remote peer c *core.Core @@ -79,7 +79,9 @@ func main() { if !asServer { // we start the message exchange - c.Send(ctx, remote.GetID(), message.NewTransportTCPWelcomeMsg(c.PeerID())) + if err := c.Send(ctx, remote.GetID(), message.NewTransportTCPWelcomeMsg(c.PeerID())); err != nil { + fmt.Printf("send message failed: %s", err.Error()) + } } // handle OS signals @@ -117,23 +119,27 @@ loop: // process incoming messages and send responses; it is used for protocol exploration only. // it tries to mimick the message flow between "real" GNUnet peers. func process(ctx context.Context, ev *core.Event) { - logger.Printf(logger.DBG, "<<< %s", ev.Msg.String()) switch msg := ev.Msg.(type) { - case *message.TransportTCPWelcomeMsg: - c.Send(ctx, ev.Peer, message.NewTransportPingMsg(ev.Peer, nil)) + if err := c.Send(ctx, ev.Peer, message.NewTransportPingMsg(ev.Peer, nil)); err != nil { + logger.Printf(logger.ERROR, "TransportTCPWelcomeMsg send failed: %s", err.Error()) + return + } case *message.HelloMsg: case *message.TransportPingMsg: mOut := message.NewTransportPongMsg(msg.Challenge, nil) if err := mOut.Sign(local.PrvKey()); err != nil { - logger.Println(logger.ERROR, "PONG: signing failed") + logger.Printf(logger.ERROR, "PONG signing failed: %s", err.Error()) + return + } + if err := c.Send(ctx, ev.Peer, mOut); err != nil { + logger.Printf(logger.ERROR, "TransportPongMsg send failed: %s", err.Error()) return } - c.Send(ctx, ev.Peer, mOut) logger.Printf(logger.DBG, ">>> %s", mOut) case *message.TransportPongMsg: @@ -148,7 +154,9 @@ func process(ctx context.Context, ev *core.Event) { case *message.SessionSynMsg: mOut := message.NewSessionSynAckMsg() mOut.Timestamp = msg.Timestamp - c.Send(ctx, ev.Peer, mOut) + if err := c.Send(ctx, ev.Peer, mOut); err != nil { + logger.Printf(logger.ERROR, "SessionSynAckMsg send failed: %s", err.Error()) + } logger.Printf(logger.DBG, ">>> %s", mOut) case *message.SessionQuotaMsg: @@ -157,7 +165,9 @@ func process(ctx context.Context, ev *core.Event) { case *message.SessionKeepAliveMsg: mOut := message.NewSessionKeepAliveRespMsg(msg.Nonce) - c.Send(ctx, ev.Peer, mOut) + if err := c.Send(ctx, ev.Peer, mOut); err != nil { + logger.Printf(logger.ERROR, "SessionKeepAliveRespMsg send failed: %s", err.Error()) + } logger.Printf(logger.DBG, ">>> %s", mOut) case *message.EphemeralKeyMsg: @@ -171,9 +181,13 @@ func process(ctx context.Context, ev *core.Event) { } remote.SetEphKeyMsg(msg) mOut := local.EphKeyMsg() - c.Send(ctx, ev.Peer, mOut) + if err := c.Send(ctx, ev.Peer, mOut); err != nil { + logger.Printf(logger.ERROR, "EphKeyMsg send failed: %s", err.Error()) + } logger.Printf(logger.DBG, ">>> %s", mOut) - secret = crypto.SharedSecret(local.EphPrvKey(), remote.EphKeyMsg().Public()) + pk := ed25519.NewPublicKeyFromBytes(remote.EphKeyMsg().Public().Data) + secret = crypto.SharedSecret(local.EphPrvKey(), pk) + fmt.Printf("Shared secret: %s\n", secret.String()) default: fmt.Printf("!!! %v\n", msg) diff --git a/src/gnunet/cmd/revoke-zonekey/main.go b/src/gnunet/cmd/revoke-zonekey/main.go @@ -43,10 +43,10 @@ import ( // State of RevData calculation const ( - S_NEW = iota // start new PoW calculation - S_CONT // continue PoW calculation - S_DONE // PoW calculation done - S_SIGNED // revocation data signed + StateNew = iota // start new PoW calculation + StateCont // continue PoW calculation + StateDone // PoW calculation done + StateSigned // revocation data signed ) // RevData is the storage layout for persistent data used by this program. @@ -67,7 +67,7 @@ func ReadRevData(filename string, bits int, zk *crypto.ZoneKey) (rd *RevData, er Rd: revocation.NewRevDataCalc(zk), Numbits: uint8(bits), T: util.NewRelativeTime(0), - State: S_NEW, + State: StateNew, } // read revocation object from file. If the file does not exist, a new @@ -80,24 +80,23 @@ func ReadRevData(filename string, bits int, zk *crypto.ZoneKey) (rd *RevData, er dataBuf := make([]byte, rd.size()) var n int if n, err = file.Read(dataBuf); err != nil { - err = fmt.Errorf("Error reading file: " + err.Error()) + err = fmt.Errorf("error reading file: " + err.Error()) return } if n != len(dataBuf) { - err = fmt.Errorf("File size mismatch") + err = fmt.Errorf("file size mismatch") return } if err = data.Unmarshal(&rd, dataBuf); err != nil { - err = fmt.Errorf("File corrupted: " + err.Error()) + err = fmt.Errorf("file corrupted: " + err.Error()) return } if !zk.Equal(&rd.Rd.RevData.ZoneKeySig.ZoneKey) { - err = fmt.Errorf("Zone key mismatch") + err = fmt.Errorf("zone key mismatch") return } - bits = int(rd.Numbits) if err = file.Close(); err != nil { - err = fmt.Errorf("Error closing file: " + err.Error()) + err = fmt.Errorf("error closing file: " + err.Error()) } return } @@ -106,24 +105,24 @@ func ReadRevData(filename string, bits int, zk *crypto.ZoneKey) (rd *RevData, er func (r *RevData) Write(filename string) (err error) { var file *os.File if file, err = os.Create(filename); err != nil { - return fmt.Errorf("Can't write to output file: " + err.Error()) + return fmt.Errorf("can't write to output file: " + err.Error()) } var buf []byte if buf, err = data.Marshal(r); err != nil { - return fmt.Errorf("Internal error: " + err.Error()) + return fmt.Errorf("internal error: " + err.Error()) } if len(buf) != r.size() { - return fmt.Errorf("Internal error: Buffer mismatch %d != %d", len(buf), r.size()) + return fmt.Errorf("internal error: Buffer mismatch %d != %d", len(buf), r.size()) } var n int if n, err = file.Write(buf); err != nil { - return fmt.Errorf("Can't write to output file: " + err.Error()) + return fmt.Errorf("can't write to output file: " + err.Error()) } if n != len(buf) { - return fmt.Errorf("Can't write data to output file!") + return fmt.Errorf("can't write data to output file") } if err = file.Close(); err != nil { - return fmt.Errorf("Error closing file: " + err.Error()) + return fmt.Errorf("error closing file: " + err.Error()) } return } @@ -138,7 +137,7 @@ func (r *RevData) size() int { // // (1) Generate the desired PoWs for the public zone key: // This process can be started, stopped and resumed, so the long -// calculation time (usually days or even weeks) can be interruped if +// calculation time (usually days or even weeks) can be interrupted if // desired. For security reasons you should only pass the "-z" argument to // this step but not the "-k" argument (private key) as it is not required // to calculate the PoWs. @@ -164,7 +163,7 @@ func main() { zonekey string // zonekey to be revoked prvkey string // private zonekey (base64-encoded key data) testing bool // test mode (no minimum difficulty) - filename string // name of file for persistance + filename string // name of file for persistence ) minDiff := revocation.MinDifficulty flag.IntVar(&bits, "b", minDiff+1, "Number of leading zero bits") @@ -227,27 +226,27 @@ func main() { // handle revocation data state switch rd.State { - case S_NEW: + case StateNew: log.Println("Starting new revocation calculation...") - rd.State = S_CONT + rd.State = StateCont - case S_CONT: + case StateCont: log.Printf("Revocation calculation started at %s\n", rd.Rd.Timestamp.String()) log.Printf("Time spent on calculation: %s\n", rd.T.String()) log.Printf("Last tested PoW value: %d\n", rd.Last) log.Println("Continuing...") - case S_DONE: + case StateDone: // calculation complete: sign with private key if sk == nil { log.Fatal("Need to sign revocation: private key is missing.") } log.Println("Signing revocation with private key") - if err := rd.Rd.Sign(sk); err != nil { + if err = rd.Rd.Sign(sk); err != nil { log.Fatal("Failed to sign revocation: " + err.Error()) } // write final revocation - rd.State = S_SIGNED + rd.State = StateSigned if err = rd.Write(filename); err != nil { log.Fatal("Failed to write revocation: " + err.Error()) } @@ -278,10 +277,10 @@ func main() { // The calculation was interrupted; we still need to compute // more and better PoWs... log.Printf("Incomplete revocation: Only %f zero bits on average!\n", average) - rd.State = S_CONT + rd.State = StateCont } else { // we have reached the required PoW difficulty - rd.State = S_DONE + rd.State = StateDone // check if we have a valid revocation. log.Println("Revocation calculation complete:") diff, rc := rd.Rd.Verify(false) @@ -313,22 +312,19 @@ func main() { sigCh := make(chan os.Signal, 5) signal.Notify(sigCh) loop: - for { - select { + for sig := range sigCh { // handle OS signals - case sig := <-sigCh: - switch sig { - case syscall.SIGKILL, syscall.SIGINT, syscall.SIGTERM: - log.Printf("Terminating (on signal '%s')\n", sig) - cancelFcn() - break loop - case syscall.SIGHUP: - log.Println("SIGHUP") - case syscall.SIGURG: - // TODO: https://github.com/golang/go/issues/37942 - default: - log.Println("Unhandled signal: " + sig.String()) - } + switch sig { + case syscall.SIGKILL, syscall.SIGINT, syscall.SIGTERM: + log.Printf("Terminating (on signal '%s')\n", sig) + cancelFcn() + break loop + case syscall.SIGHUP: + log.Println("SIGHUP") + case syscall.SIGURG: + // TODO: https://github.com/golang/go/issues/37942 + default: + log.Println("Unhandled signal: " + sig.String()) } } }() diff --git a/src/gnunet/cmd/vanityid/main.go b/src/gnunet/cmd/vanityid/main.go @@ -8,8 +8,9 @@ import ( "regexp" "time" - "github.com/bfix/gospel/crypto/ed25519" "gnunet/util" + + "github.com/bfix/gospel/crypto/ed25519" ) func main() { @@ -32,16 +33,13 @@ func main() { seed := make([]byte, 32) start := time.Now() for i := 0; ; i++ { - n, err := rand.Read(seed) - if err != nil || n != 32 { - panic(err) - } + _, _ = rand.Read(seed) prv := ed25519.NewPrivateKeyFromSeed(seed) pub := prv.Public().Bytes() id := util.EncodeBinaryToString(pub) for _, r := range reg { if r.MatchString(id) { - elapsed := time.Now().Sub(start) + elapsed := time.Since(start) s1 := hex.EncodeToString(seed) s2 := hex.EncodeToString(prv.D.Bytes()) fmt.Printf("%s [%s][%s] (%d tries, %s elapsed)\n", id, s1, s2, i, elapsed) diff --git a/src/gnunet/config/config.go b/src/gnunet/config/config.go @@ -21,6 +21,7 @@ package config import ( "encoding/json" "fmt" + "gnunet/util" "io/ioutil" "reflect" "regexp" @@ -56,12 +57,13 @@ type NodeConfig struct { } //---------------------------------------------------------------------- -// Bootstrap configuration +// Network configuration //---------------------------------------------------------------------- -// BootstrapConfig holds parameters for the initial connection to the network. -type BootstrapConfig struct { - Nodes []string `json:"nodes"` // bootstrap nodes +// NetworkConfig holds parameters for the initial connection to the network. +type NetworkConfig struct { + Bootstrap []string `json:"bootstrap"` // bootstrap nodes + NumPeers int `json:"numPeers"` // estimated number of peers (0 = use NSE) } //---------------------------------------------------------------------- @@ -94,33 +96,20 @@ type GNSConfig struct { } //---------------------------------------------------------------------- -// Generic parameter configuration (handle any key/value settings) -//---------------------------------------------------------------------- - -// ParameterConfig handle arbitrary values for a key strings. This necessary -// e.g. in the 'Storage' configuration, as custom storage implementations -// require different sets of parameters. -type ParameterConfig map[string]any - -// Get a parameter value with given type 'V' -func GetParam[V any](params ParameterConfig, key string) (i V, ok bool) { - var v any - if v, ok = params[key]; ok { - if i, ok = v.(V); ok { - return - } - } - return -} - -//---------------------------------------------------------------------- // DHT configuration //---------------------------------------------------------------------- // DHTConfig contains parameters for the distributed hash table (DHT) type DHTConfig struct { - Service *ServiceConfig `json:"service"` // socket for DHT service - Storage ParameterConfig `json:"storage"` // filesystem storage location + Service *ServiceConfig `json:"service"` // socket for DHT service + Storage util.ParameterSet `json:"storage"` // filesystem storage location + Routing *RoutingConfig `json:"routing"` // routing table configuration + Heartbeat int `json:"heartbeat"` // heartbeat intervall +} + +// RoutingConfig holds parameters for routing tables +type RoutingConfig struct { + PeerTTL int `json:"peerTTL"` // time-out for peers in table } //---------------------------------------------------------------------- @@ -129,8 +118,8 @@ type DHTConfig struct { // NamecacheConfig contains parameters for the local name cache type NamecacheConfig struct { - Service *ServiceConfig `json:"service"` // socket for Namecache service - Storage ParameterConfig `json:"storage"` // key/value cache + Service *ServiceConfig `json:"service"` // socket for Namecache service + Storage util.ParameterSet `json:"storage"` // key/value cache } //---------------------------------------------------------------------- @@ -139,8 +128,8 @@ type NamecacheConfig struct { // RevocationConfig contains parameters for the key revocation service type RevocationConfig struct { - Service *ServiceConfig `json:"service"` // socket for Revocation service - Storage ParameterConfig `json:"storage"` // persistance mechanism for revocation data + Service *ServiceConfig `json:"service"` // socket for Revocation service + Storage util.ParameterSet `json:"storage"` // persistence mechanism for revocation data } //---------------------------------------------------------------------- @@ -153,7 +142,7 @@ type Environment map[string]string // Config is the aggregated configuration for GNUnet. type Config struct { Local *NodeConfig `json:"local"` - Bootstrap *BootstrapConfig `json:"bootstrap"` + Network *NetworkConfig `json:"network"` Env Environment `json:"environ"` RPC *RPCConfig `json:"rpc"` DHT *DHTConfig `json:"dht"` @@ -193,7 +182,7 @@ func ParseConfigBytes(data []byte, subst bool) (err error) { } var ( - rx = regexp.MustCompile("\\$\\{([^\\}]*)\\}") + rx = regexp.MustCompile(`\$\{([^\}]*)\}`) ) // substString is a helper function to substitute environment variables @@ -215,7 +204,6 @@ func substString(s string, env map[string]string) string { // applySubstitutions traverses the configuration data structure // and applies string substitutions to all string values. func applySubstitutions(x interface{}, env map[string]string) { - var process func(v reflect.Value) process = func(v reflect.Value) { for i := 0; i < v.NumField(); i++ { @@ -224,7 +212,7 @@ func applySubstitutions(x interface{}, env map[string]string) { switch fld.Kind() { case reflect.String: // check for substitution - s := fld.Interface().(string) + s, _ := fld.Interface().(string) for { s1 := substString(s, env) if s1 == s { diff --git a/src/gnunet/config/config_test.go b/src/gnunet/config/config_test.go @@ -35,7 +35,7 @@ func TestConfigRead(t *testing.T) { t.Fatal(err) } // parse configuration - if err := ParseConfigBytes(data, false); err != nil { + if err = ParseConfigBytes(data, false); err != nil { t.Fatal(err) } // write configuration diff --git a/src/gnunet/config/gnunet-config.json b/src/gnunet/config/gnunet-config.json @@ -1,8 +1,10 @@ { - "bootstrap": { - "nodes": [ + "network": { + "bootstrap": [ + "ip+udp://172.17.0.5:10000", "gnunet://hello/7KTBJ90340HF1Q2GB0A57E2XJER4FDHX8HP5GHEB9125VPWPD27G/BNMDFN6HJCPWSPNBSEC06MC1K8QN1Z2DHRQSRXDTFR7FTBD4JHNBJ2RJAAEZ31FWG1Q3PMN3PXGZQ3Q7NTNEKQZFA7TE2Y46FM8E20R/1653499308?r5n+ip+udp=127.0.0.1%3A7654" - ] + ], + "numPeers": 10 }, "local": { "name": "ygng", @@ -33,7 +35,11 @@ "cache": false, "path": "/var/lib/gnunet/dht/store", "maxGB": 10 - } + }, + "routing": { + "peerTTL": 10800 + }, + "heartbeat": 900 }, "gns": { "service": { diff --git a/src/gnunet/core/core.go b/src/gnunet/core/core.go @@ -20,10 +20,10 @@ package core import ( "context" + "encoding/hex" "errors" "gnunet/config" "gnunet/message" - "gnunet/service/dht/blocks" "gnunet/transport" "gnunet/util" "net" @@ -47,7 +47,7 @@ type EndpointRef struct { id string // endpoint identifier in configuration ep transport.Endpoint // reference to endpoint addr *util.Address // public endpoint address - upnpId string // UPNP identifier (empty if unused) + upnpID string // UPNP identifier (empty if unused) } //---------------------------------------------------------------------- @@ -57,7 +57,7 @@ type Core struct { local *Peer // incoming messages from transport - incoming chan *transport.TransportMessage + incoming chan *transport.Message // reference to transport implementation trans *transport.Transport @@ -68,37 +68,39 @@ type Core struct { // list of known peers with addresses peers *util.PeerAddrList + // list of connected peers + connected *util.Map[string, bool] + // List of registered endpoints endpoints map[string]*EndpointRef - - // last HELLO message used; re-create if expired - lastHello *message.HelloDHTMsg } //---------------------------------------------------------------------- // NewCore creates and runs a new core instance. func NewCore(ctx context.Context, node *config.NodeConfig) (c *Core, err error) { - // instantiate peer var peer *Peer if peer, err = NewLocalPeer(node); err != nil { return } + logger.Printf(logger.DBG, "[core] Local node is %s", peer.GetID().String()) + // create new core instance - incoming := make(chan *transport.TransportMessage) + incoming := make(chan *transport.Message) c = &Core{ local: peer, incoming: incoming, listeners: make(map[string]*Listener), trans: transport.NewTransport(ctx, node.Name, incoming), peers: util.NewPeerAddrList(), + connected: util.NewMap[string, bool](), endpoints: make(map[string]*EndpointRef), } // add all local peer endpoints to transport. for _, epCfg := range node.Endpoints { var ( - upnpId string // upnp identifier + upnpID string // upnp identifier local *util.Address // local address remote *util.Address // remote address ep transport.Endpoint // endpoint reference @@ -113,7 +115,7 @@ func NewCore(ctx context.Context, node *config.NodeConfig) (c *Core, err error) // handle UPNP port forwarding protocol := transport.EpProtocol(epCfg.Network) var localA, remoteA string - if upnpId, remoteA, localA, err = c.trans.ForwardOpen(protocol, epCfg.Address[5:], epCfg.Port); err != nil { + if upnpID, remoteA, localA, err = c.trans.ForwardOpen(protocol, epCfg.Address[5:], epCfg.Port); err != nil { return } // parse local and remote addresses @@ -129,7 +131,7 @@ func NewCore(ctx context.Context, node *config.NodeConfig) (c *Core, err error) return } remote = local - upnpId = "" + upnpID = "" } // add endpoint for address if ep, err = c.trans.AddEndpoint(ctx, local); err != nil { @@ -148,7 +150,7 @@ func NewCore(ctx context.Context, node *config.NodeConfig) (c *Core, err error) id: epCfg.ID, ep: ep, addr: remote, - upnpId: upnpId, + upnpID: upnpID, } } // run message pump @@ -165,35 +167,20 @@ func (c *Core) pump(ctx context.Context) { case tm := <-c.incoming: logger.Printf(logger.DBG, "[core] Message received from %s: %s", tm.Peer, transport.Dump(tm.Msg, "json")) - // inspect message for peer state events - var ev *Event - switch msg := tm.Msg.(type) { - case *message.HelloDHTMsg: - - // verify integrity of message - if ok, err := msg.Verify(tm.Peer); !ok || err != nil { - logger.Println(logger.WARN, "[core] Received invalid DHT_P2P_HELLO message") - break - } - // keep peer addresses - aList, err := msg.Addresses() - if err != nil { - logger.Println(logger.WARN, "[core] Failed to parse addresses from DHT_P2P_HELLO message") - break - } - if err := c.Learn(ctx, tm.Peer, aList); err != nil { - logger.Println(logger.WARN, "[core] Failed to learn addresses from DHT_P2P_HELLO message: "+err.Error()) - break - } - + // check if peer is already connected (has an entry in PeerAddrist) + _, connected := c.connected.Get(tm.Peer.String()) + if !connected { + // no: mark connected + c.connected.Put(tm.Peer.String(), true) // generate EV_CONNECT event - ev = &Event{ + c.dispatch(&Event{ ID: EV_CONNECT, Peer: tm.Peer, - Msg: msg, - } - c.dispatch(ev) + }) + // grace period for connection signal + time.Sleep(time.Second) } + // set default responder (core) if no custom responder // is defined by the receiving endpoint. resp := tm.Resp @@ -204,13 +191,12 @@ func (c *Core) pump(ctx context.Context) { } } // generate EV_MESSAGE event - ev = &Event{ + c.dispatch(&Event{ ID: EV_MESSAGE, Peer: tm.Peer, Msg: tm.Msg, - Resp: tm.Resp, - } - c.dispatch(ev) + Resp: resp, + }) // wait for termination case <-ctx.Done(): @@ -234,12 +220,12 @@ func (c *Core) Send(ctx context.Context, peer *util.PeerID, msg message.Message) netw := "ip+udp" // try all addresses for peer - aList := c.peers.Get(peer.String(), netw) + aList := c.peers.Get(peer, netw) maybe := false // message may be sent... for _, addr := range aList { - logger.Printf(logger.WARN, "[core] Trying to send to %s", addr.URI()) + logger.Printf(logger.INFO, "[core] Trying to send to %s", addr.URI()) // send message to address - if err = c.send(ctx, addr, msg); err != nil { + if err = c.SendToAddr(ctx, addr, msg); err != nil { // if it is possible that the message was not sent, try next address if err != transport.ErrEndpMaybeSent { logger.Printf(logger.WARN, "[core] Failed to send to %s: %s", addr.URI(), err.Error()) @@ -252,15 +238,16 @@ func (c *Core) Send(ctx context.Context, peer *util.PeerID, msg message.Message) return } if maybe { - err = transport.ErrEndpMaybeSent + logger.Printf(logger.WARN, "[core] %s", transport.ErrEndpMaybeSent.Error()) + err = nil } else { err = ErrCoreNotSent } return } -// send message directly to address -func (c *Core) send(ctx context.Context, addr *util.Address, msg message.Message) error { +// SendToAddr message directly to address +func (c *Core) SendToAddr(ctx context.Context, addr *util.Address, msg message.Message) error { // assemble transport message tm := transport.NewTransportMessage(c.PeerID(), msg) // send on transport @@ -268,81 +255,16 @@ func (c *Core) send(ctx context.Context, addr *util.Address, msg message.Message } // Learn (new) addresses for peer -func (c *Core) Learn(ctx context.Context, peer *util.PeerID, addrs []*util.Address) (err error) { +func (c *Core) Learn(ctx context.Context, peer *util.PeerID, addrs []*util.Address) (newPeer bool) { // learn all addresses for peer - newPeer := false + newPeer = false for _, addr := range addrs { logger.Printf(logger.INFO, "[core] Learning %s for %s (expires %s)", addr.URI(), peer, addr.Expires) - newPeer = (c.peers.Add(peer.String(), addr) == 1) || newPeer - } - // new peer detected? - if newPeer { - // we added a previously unknown peer: send a HELLO - var msg *message.HelloDHTMsg - if msg, err = c.getHello(); err != nil { - return - } - logger.Printf(logger.INFO, "[core] Sending HELLO to %s: %s", peer, msg) - err = c.Send(ctx, peer, msg) - // no error if the message might have been sent - if err == transport.ErrEndpMaybeSent { - err = nil - } + newPeer = (c.peers.Add(peer, addr) == 1) || newPeer } return } -// Send the currently active HELLO to given network address -func (c *Core) SendHello(ctx context.Context, addr *util.Address) (err error) { - // get (buffered) HELLO - var msg *message.HelloDHTMsg - if msg, err = c.getHello(); err != nil { - return - } - logger.Printf(logger.INFO, "[core] Sending HELLO to %s: %s", addr.URI(), msg) - return c.send(ctx, addr, msg) -} - -// get the recent HELLO if it is defined and not expired; -// create a new HELLO otherwise. -func (c *Core) getHello() (msg *message.HelloDHTMsg, err error) { - if c.lastHello == nil || c.lastHello.Expires.Expired() { - // assemble new HELLO message - addrList := make([]*util.Address, 0) - for _, epRef := range c.endpoints { - addrList = append(addrList, epRef.addr) - } - node := c.local - var hello *blocks.HelloBlock - hello, err = node.HelloData(time.Hour, addrList) - if err != nil { - return - } - msg = message.NewHelloDHTMsg() - msg.NumAddr = uint16(len(hello.Addresses())) - msg.SetAddresses(hello.Addresses()) - if err = msg.Sign(c.local.prv); err != nil { - return - } - // save for later use - c.lastHello = msg - - // DEBUG - var ok bool - if ok, err = msg.Verify(c.PeerID()); !ok || err != nil { - if !ok { - err = errors.New("[core] failed to verify own HELLO") - } - logger.Println(logger.ERROR, err.Error()) - return - } - logger.Println(logger.DBG, "[core] New HELLO: "+transport.Dump(msg, "json")) - return - } - // we have a valid HELLO for re-use. - return c.lastHello, nil -} - // Addresses returns the list of listening endpoint addresses func (c *Core) Addresses() (list []*util.Address, err error) { for _, epRef := range c.endpoints { @@ -365,6 +287,29 @@ func (c *Core) PeerID() *util.PeerID { //---------------------------------------------------------------------- +// Signable interface for objects that can get signed by peer +type Signable interface { + // SignedData returns the byte array to be signed + SignedData() []byte + + // SetSignature returns the signature to the signable object + SetSignature(*util.PeerSignature) error +} + +// Sign a signable onject with private peer key +func (c *Core) Sign(obj Signable) error { + sd := obj.SignedData() + logger.Printf(logger.DBG, "[core] Signing data '%s'", hex.EncodeToString(sd)) + sig, err := c.local.prv.EdSign(sd) + if err != nil { + return err + } + logger.Printf(logger.DBG, "[core] --> signature '%s'", hex.EncodeToString(sig.Bytes())) + return obj.SetSignature(util.NewPeerSignature(sig.Bytes())) +} + +//---------------------------------------------------------------------- + // TryConnect is a function which allows the local peer to attempt the // establishment of a connection to another peer using an address. // When the connection attempt is successful, information on the new @@ -390,14 +335,6 @@ func (c *Core) Hold(peer *util.PeerID) {} // established by HOLD(). func (c *Core) Drop(peer *util.PeerID) {} -// L2NSE is ESTIMATE_NETWORK_SIZE(), a procedure that provides estimates -// on the base-2 logarithm of the network size L2NSE, that is the base-2 -// logarithm number of peers in the network, for use by the routing -// algorithm. -func (c *Core) L2NSE() float64 { - return 0. -} - //---------------------------------------------------------------------- // Event listener and event dispatch. //---------------------------------------------------------------------- @@ -418,11 +355,12 @@ func (c *Core) Unregister(name string) *Listener { // internal: dispatch event to listeners func (c *Core) dispatch(ev *Event) { + logger.Printf(logger.DBG, "[core] Dispatching %v...", ev) // dispatch event to listeners for _, l := range c.listeners { if l.filter.CheckEvent(ev.ID) { - mt := ev.Msg.Header().MsgType if ev.ID == EV_MESSAGE { + mt := ev.Msg.Header().MsgType if mt != 0 && !l.filter.CheckMsgType(mt) { // skip event return diff --git a/src/gnunet/core/core_test.go b/src/gnunet/core/core_test.go @@ -36,7 +36,6 @@ import ( // TestCoreSimple test a two node network func TestCoreSimple(t *testing.T) { - var ( peer1Cfg = &config.NodeConfig{ Name: "p1", @@ -75,11 +74,11 @@ func TestCoreSimple(t *testing.T) { }() // create and run nodes - node1, err := NewTestNode(t, ctx, peer1Cfg) + node1, err := NewTestNode(ctx, t, peer1Cfg) if err != nil { t.Fatal(err) } - node2, err := NewTestNode(t, ctx, peer2Cfg) + node2, err := NewTestNode(ctx, t, peer2Cfg) if err != nil { t.Fatal(err) } @@ -104,7 +103,6 @@ func TestCoreSimple(t *testing.T) { // TestCoreSimple test a two node network func TestCoreUPNP(t *testing.T) { - // configuration data var ( peer1Cfg = &config.NodeConfig{ @@ -143,12 +141,16 @@ func TestCoreUPNP(t *testing.T) { }() // create and run nodes - node1, err := NewTestNode(t, ctx, peer1Cfg) + node1, err := NewTestNode(ctx, t, peer1Cfg) if err != nil { + if err == transport.ErrTransNoUPNP { + t.Log("No UPnP available -- skipping test") + return + } t.Fatal(err) } defer node1.Shutdown() - node2, err := NewTestNode(t, ctx, peer2Cfg) + node2, err := NewTestNode(ctx, t, peer2Cfg) if err != nil { t.Fatal(err) } @@ -180,7 +182,7 @@ func TestDHTU(t *testing.T) { } // convert arguments var ( - rId *util.PeerID + rID *util.PeerID rAddr *util.Address err error ) @@ -211,14 +213,14 @@ func TestDHTU(t *testing.T) { }() // create and run node - node, err := NewTestNode(t, ctx, peerCfg) + node, err := NewTestNode(ctx, t, peerCfg) if err != nil { log.Fatal(err) } defer node.Shutdown() // learn bootstrap address (triggers HELLO) - node.Learn(ctx, rId, rAddr) + node.Learn(ctx, rID, rAddr) // run forever var ch chan struct{} @@ -246,14 +248,12 @@ func (n *TestNode) Learn(ctx context.Context, peer *util.PeerID, addr *util.Addr if peer != nil { label = peer.String() } - n.t.Logf("[%d] Learning %s for %s", n.id, addr.StringAll(), label) - if err := n.core.Learn(ctx, peer, []*util.Address{addr}); err != nil { - n.t.Log("Learn: " + err.Error()) - } + n.t.Logf("[%d] Learning %s for %s", n.id, addr.URI(), label) + n.core.Learn(ctx, peer, []*util.Address{addr}) } -func NewTestNode(t *testing.T, ctx context.Context, cfg *config.NodeConfig) (node *TestNode, err error) { - +func NewTestNode(ctx context.Context, t *testing.T, cfg *config.NodeConfig) (node *TestNode, err error) { + t.Helper() // create test node node = new(TestNode) node.t = t @@ -265,7 +265,7 @@ func NewTestNode(t *testing.T, ctx context.Context, cfg *config.NodeConfig) (nod } node.peer = node.core.Peer() t.Logf("[%d] Node %s starting", node.id, node.peer.GetID()) - t.Logf("[%d] --> %s", node.id, hex.EncodeToString(node.peer.GetID().Key)) + t.Logf("[%d] --> %s", node.id, hex.EncodeToString(node.peer.GetID().Data)) list, err := node.core.Addresses() if err != nil { diff --git a/src/gnunet/core/event.go b/src/gnunet/core/event.go @@ -19,6 +19,7 @@ package core import ( + "fmt" "gnunet/message" "gnunet/transport" "gnunet/util" @@ -29,6 +30,7 @@ import ( //---------------------------------------------------------------------- // Event types +//nolint:stylecheck // allow non-camel-case in constants const ( EV_CONNECT = iota // peer connected EV_DISCONNECT // peer disconnected @@ -82,6 +84,8 @@ func (f *EventFilter) CheckMsgType(mt uint16) bool { return ok } +//---------------------------------------------------------------------- + // Event sent to listeners type Event struct { ID int // event type @@ -91,6 +95,19 @@ type Event struct { Label string // event label (can be empty) } +// String returns a human-readable representation of an event. +func (e *Event) String() string { + s := "Event{" + if len(e.Label) > 0 { + s += "label=" + e.Label + "," + } + s += fmt.Sprintf("id=%d,peer=%s", e.ID, e.Peer) + if e.Msg != nil { + s += fmt.Sprintf(",msg=%d", e.Msg.Header().MsgType) + } + return s + "}" +} + //---------------------------------------------------------------------- // Listener for network events diff --git a/src/gnunet/core/hello_test.go b/src/gnunet/core/hello_test.go @@ -71,7 +71,6 @@ func TestHelloURLDirect(t *testing.T) { } func TestHelloURL(t *testing.T) { - // prepare peer and HELLO data peer, err := NewLocalPeer(peerCfg) if err != nil { diff --git a/src/gnunet/core/peer.go b/src/gnunet/core/peer.go @@ -103,11 +103,16 @@ func (p *Peer) HelloData(ttl time.Duration, a []*util.Address) (h *blocks.HelloB // assemble HELLO data h = new(blocks.HelloBlock) h.PeerID = p.GetID() - h.Expire = util.NewAbsoluteTime(time.Now().Add(ttl)) + h.Expires = util.NewAbsoluteTime(time.Now().Add(ttl)) h.SetAddresses(a) // sign data - err = h.Sign(p.prv) + sd := h.SignedData() + var sig *ed25519.EdSignature + if sig, err = p.prv.EdSign(sd); err != nil { + return + } + err = h.SetSignature(util.NewPeerSignature(sig.Bytes())) return } @@ -139,7 +144,7 @@ func (p *Peer) PubKey() *ed25519.PublicKey { // GetID returns the node ID (public key) in binary format func (p *Peer) GetID() *util.PeerID { return &util.PeerID{ - Key: util.Clone(p.pub.Bytes()), + Data: util.Clone(p.pub.Bytes()), } } @@ -151,7 +156,7 @@ func (p *Peer) GetIDString() string { // Sign a message with the (long-term) private key. func (p *Peer) Sign(msg []byte) (*ed25519.EdSignature, error) { if p.prv == nil { - return nil, fmt.Errorf("No private key") + return nil, fmt.Errorf("no private key") } return p.prv.EdSign(msg) } diff --git a/src/gnunet/core/peer_test.go b/src/gnunet/core/peer_test.go @@ -43,7 +43,6 @@ var ( ) func TestPeerHello(t *testing.T) { - // generate new local node node, err := NewLocalPeer(cfg) if err != nil { @@ -54,13 +53,16 @@ func TestPeerHello(t *testing.T) { // This hack will only work for direct listening addresses addrList := make([]*util.Address, 0) for _, epRef := range cfg.Endpoints { - addr, err := util.ParseAddress(epRef.Addr()) - if err != nil { + var addr *util.Address + if addr, err = util.ParseAddress(epRef.Addr()); err != nil { t.Fatal(err) } addrList = append(addrList, addr) } h, err := node.HelloData(TTL, addrList) + if err != nil { + t.Fatal(err) + } // convert to URL and back u := h.URL() diff --git a/src/gnunet/crypto/gns.go b/src/gnunet/crypto/gns.go @@ -28,6 +28,7 @@ import ( "gnunet/util" "github.com/bfix/gospel/data" + "github.com/bfix/gospel/logger" "github.com/bfix/gospel/math" "golang.org/x/crypto/hkdf" ) @@ -105,7 +106,7 @@ type ZoneKeyImpl interface { // Derive a zone key from this zone key based on a big integer // (key blinding). Returns the derived key and the blinding value. - Derive(h *math.Int) (ZoneKeyImpl, *math.Int) + Derive(h *math.Int) (ZoneKeyImpl, *math.Int, error) // BlockKey returns the key for block en-/decryption BlockKey(label string, expires util.AbsoluteTime) (skey []byte) @@ -126,7 +127,7 @@ type ZonePrivateImpl interface { // Derive a private key from this zone key based on a big integer // (key blinding). Returns the derived key and the blinding value. - Derive(h *math.Int) (ZonePrivateImpl, *math.Int) + Derive(h *math.Int) (ZonePrivateImpl, *math.Int, error) // Sign binary data and return the signature Sign(data []byte) (*ZoneSignature, error) @@ -143,10 +144,14 @@ type ZoneSigImpl interface { //---------------------------------------------------------------------- // Zone types //---------------------------------------------------------------------- + +//nolint:stylecheck // allow non-camel-case in constants var ( ZONE_PKEY = uint32(enums.GNS_TYPE_PKEY) ZONE_EDKEY = uint32(enums.GNS_TYPE_EDKEY) +) +var ( // register available zone types for BlockHandler ZoneTypes = []enums.GNSType{ enums.GNS_TYPE_PKEY, @@ -177,6 +182,7 @@ var ( // Error codes var ( ErrNoImplementation = errors.New("unknown zone implementation") + ErrUnknownZoneType = errors.New("unknown zone type") ) // GetImplementation return the factory for a given zone type. @@ -204,14 +210,14 @@ type ZonePrivate struct { } // NewZonePrivate returns a new initialized ZonePrivate instance -func NewZonePrivate(ztype uint32, d []byte) (*ZonePrivate, error) { +func NewZonePrivate(ztype uint32, d []byte) (zp *ZonePrivate, err error) { // get factory for given zone type impl, ok := zoneImpl[ztype] if !ok { return nil, ErrNoImplementation } // assemble private zone key - zp := &ZonePrivate{ + zp = &ZonePrivate{ ZoneKey{ ztype, nil, @@ -220,11 +226,13 @@ func NewZonePrivate(ztype uint32, d []byte) (*ZonePrivate, error) { nil, } zp.impl = impl.NewPrivate() - zp.impl.Init(d) + if err = zp.impl.Init(d); err != nil { + return + } zp.ZoneKey.KeyData = zp.impl.Public().Bytes() zp.ZoneKey.impl = impl.NewPublic() - zp.ZoneKey.impl.Init(zp.ZoneKey.KeyData) - return zp, nil + err = zp.ZoneKey.impl.Init(zp.ZoneKey.KeyData) + return } // KeySize returns the number of bytes of a key representation. @@ -237,17 +245,18 @@ func (zp *ZonePrivate) KeySize() uint { } // Derive key (key blinding) -func (zp *ZonePrivate) Derive(label, context string) (*ZonePrivate, *math.Int) { +func (zp *ZonePrivate) Derive(label, context string) (dzp *ZonePrivate, h *math.Int, err error) { // get factory for given zone type impl := zoneImpl[zp.Type] - // caclulate derived key - h := deriveH(zp.impl.Bytes(), label, context) + // calculate derived key + h = deriveH(zp.impl.Bytes(), label, context) var derived ZonePrivateImpl - derived, h = zp.impl.Derive(h) - + if derived, h, err = zp.impl.Derive(h); err != nil { + return + } // assemble derived pivate key - dzp := &ZonePrivate{ + dzp = &ZonePrivate{ ZoneKey{ zp.Type, nil, @@ -257,8 +266,8 @@ func (zp *ZonePrivate) Derive(label, context string) (*ZonePrivate, *math.Int) { } zp.ZoneKey.KeyData = derived.Public().Bytes() zp.ZoneKey.impl = impl.NewPublic() - zp.ZoneKey.impl.Init(zp.ZoneKey.KeyData) - return dzp, h + err = zp.ZoneKey.impl.Init(zp.ZoneKey.KeyData) + return } // ZoneSign data with a private key @@ -284,20 +293,21 @@ type ZoneKey struct { } // NewZoneKey returns a new initialized ZoneKey instance -func NewZoneKey(d []byte) (*ZoneKey, error) { +func NewZoneKey(d []byte) (zk *ZoneKey, err error) { // read zone key from data - zk := new(ZoneKey) - if err := data.Unmarshal(zk, d); err != nil { - return nil, err + zk = new(ZoneKey) + if err = data.Unmarshal(zk, d); err != nil { + return } // initialize implementation impl, ok := zoneImpl[zk.Type] if !ok { - return nil, errors.New("unknown zone type") + err = ErrUnknownZoneType + return } zk.impl = impl.NewPublic() - zk.impl.Init(zk.KeyData) - return zk, nil + err = zk.impl.Init(zk.KeyData) + return } // KeySize returns the number of bytes of a key representation. @@ -310,15 +320,18 @@ func (zk *ZoneKey) KeySize() uint { } // Derive key (key blinding) -func (zk *ZoneKey) Derive(label, context string) (*ZoneKey, *math.Int) { - h := deriveH(zk.KeyData, label, context) +func (zk *ZoneKey) Derive(label, context string) (dzk *ZoneKey, h *math.Int, err error) { + h = deriveH(zk.KeyData, label, context) var derived ZoneKeyImpl - derived, h = zk.impl.Derive(h) - return &ZoneKey{ + if derived, h, err = zk.impl.Derive(h); err != nil { + return + } + dzk = &ZoneKey{ Type: zk.Type, KeyData: derived.Bytes(), impl: derived, - }, h + } + return } // BlockKey returns the key for block en-/decryption @@ -338,15 +351,22 @@ func (zk *ZoneKey) Decrypt(data []byte, label string, expire util.AbsoluteTime) // Verify a zone signature func (zk *ZoneKey) Verify(data []byte, zs *ZoneSignature) (ok bool, err error) { - zk.withImpl() + if err = zk.withImpl(); err != nil { + return + } return zk.impl.Verify(data, zs) } // ID returns the human-readable zone identifier. func (zk *ZoneKey) ID() string { buf := new(bytes.Buffer) - binary.Write(buf, binary.BigEndian, zk.Type) - buf.Write(zk.KeyData) + err := binary.Write(buf, binary.BigEndian, zk.Type) + if err == nil { + _, err = buf.Write(zk.KeyData) + } + if err != nil { + logger.Printf(logger.ERROR, "[ZoneKey.ID] failed: %s", err.Error()) + } return util.EncodeBinaryToString(buf.Bytes()) } @@ -362,12 +382,13 @@ func (zk *ZoneKey) Equal(k *ZoneKey) bool { } // withImpl ensure that an implementation reference is available -func (zk *ZoneKey) withImpl() { +func (zk *ZoneKey) withImpl() (err error) { if zk.impl == nil { factory := zoneImpl[zk.Type] zk.impl = factory.NewPublic() - zk.impl.Init(zk.KeyData) + err = zk.impl.Init(zk.KeyData) } + return } //---------------------------------------------------------------------- @@ -382,27 +403,29 @@ type ZoneSignature struct { } // NewZoneSignature returns a new initialized ZoneSignature instance -func NewZoneSignature(d []byte) (*ZoneSignature, error) { +func NewZoneSignature(d []byte) (sig *ZoneSignature, err error) { // read signature - sig := new(ZoneSignature) - if err := data.Unmarshal(sig, d); err != nil { - return nil, err + sig = new(ZoneSignature) + if err = data.Unmarshal(sig, d); err != nil { + return } // initialize implementations impl, ok := zoneImpl[sig.Type] if !ok { - return nil, errors.New("unknown zone type") + err = ErrUnknownZoneType + return } // set signature implementation zs := impl.NewSignature() - zs.Init(sig.Signature) + err = zs.Init(sig.Signature) sig.impl = zs // set public key implementation zk := impl.NewPublic() - zk.Init(sig.KeyData) + if err = zk.Init(sig.KeyData); err != nil { + return + } sig.ZoneKey.impl = zk - - return sig, nil + return } // SigSize returns the number of bytes of a signature that can be @@ -435,6 +458,8 @@ func deriveH(key []byte, label, context string) *math.Int { data := append([]byte(label), []byte(context)...) rdr := hkdf.Expand(sha256.New, prk, data) b := make([]byte, 64) - rdr.Read(b) + if _, err := rdr.Read(b); err != nil { + logger.Printf(logger.ERROR, "[deriveH] failed: %s", err.Error()) + } return math.NewIntFromBytes(b) } diff --git a/src/gnunet/crypto/gns_edkey.go b/src/gnunet/crypto/gns_edkey.go @@ -26,6 +26,7 @@ import ( "github.com/bfix/gospel/crypto/ed25519" "github.com/bfix/gospel/data" + "github.com/bfix/gospel/logger" "github.com/bfix/gospel/math" "golang.org/x/crypto/hkdf" "golang.org/x/crypto/nacl/secretbox" @@ -76,15 +77,15 @@ func (pk *EDKEYPublicImpl) Bytes() []byte { // Derive a public key from this key based on a big integer // (key blinding). Returns the derived key and the blinding value. -func (pk *EDKEYPublicImpl) Derive(h *math.Int) (ZoneKeyImpl, *math.Int) { +func (pk *EDKEYPublicImpl) Derive(h *math.Int) (dPk ZoneKeyImpl, hOut *math.Int, err error) { // limit to allowed value range - h = h.Mod(ed25519.GetCurve().N) - derived := pk.pub.Mult(h) - dPk := &EDKEYPublicImpl{ + hOut = h.Mod(ed25519.GetCurve().N) + derived := pk.pub.Mult(hOut) + dPk = &EDKEYPublicImpl{ pk.ztype, derived, } - return dPk, h + return } // Encrypt binary data (of any size). Output can be larger than input @@ -137,8 +138,9 @@ func (pk *EDKEYPublicImpl) BlockKey(label string, expires util.AbsoluteTime) (sk kd := pk.Bytes() prk := hkdf.Extract(sha512.New, kd, []byte("gns-xsalsa-ctx-key")) rdr := hkdf.Expand(sha256.New, prk, []byte(label)) - rdr.Read(skey[:32]) - + if _, err := rdr.Read(skey[:32]); err != nil { + logger.Printf(logger.ERROR, "[EDKEYPublicImpl.BlockKey] failed: %s", err.Error()) + } // assemble initialization vector iv := &struct { Nonce []byte `size:"16"` // Nonce @@ -149,7 +151,9 @@ func (pk *EDKEYPublicImpl) BlockKey(label string, expires util.AbsoluteTime) (sk } prk = hkdf.Extract(sha512.New, kd, []byte("gns-xsalsa-ctx-iv")) rdr = hkdf.Expand(sha256.New, prk, []byte(label)) - rdr.Read(iv.Nonce) + if _, err := rdr.Read(iv.Nonce); err != nil { + logger.Printf(logger.ERROR, "[EDKEYPublicImpl.BlockKey] failed: %s", err.Error()) + } buf, _ := data.Marshal(iv) copy(skey[32:], buf) return @@ -188,30 +192,32 @@ func (pk *EDKEYPrivateImpl) Public() ZoneKeyImpl { // Derive a public key from this key based on a big integer // (key blinding). Returns the derived key and the blinding value. -func (pk *EDKEYPrivateImpl) Derive(h *math.Int) (ZonePrivateImpl, *math.Int) { +func (pk *EDKEYPrivateImpl) Derive(h *math.Int) (dPk ZonePrivateImpl, hOut *math.Int, err error) { // limit to allowed value range - h = h.Mod(ed25519.GetCurve().N) - derived := pk.prv.Mult(h) - dPk := &EDKEYPrivateImpl{ + hOut = h.Mod(ed25519.GetCurve().N) + derived := pk.prv.Mult(hOut) + dPk = &EDKEYPrivateImpl{ EDKEYPublicImpl{ pk.ztype, derived.Public(), }, derived, } - return dPk, h + return } // Sign binary data -func (pk *EDKEYPrivateImpl) Sign(data []byte) (*ZoneSignature, error) { - s, err := pk.prv.EdSign(data) - if err != nil { - return nil, err +func (pk *EDKEYPrivateImpl) Sign(data []byte) (sig *ZoneSignature, err error) { + var s *ed25519.EdSignature + if s, err = pk.prv.EdSign(data); err != nil { + return } sd := s.Bytes() sigImpl := new(EDKEYSigImpl) - sigImpl.Init(sd) - sig := &ZoneSignature{ + if err = sigImpl.Init(sd); err != nil { + return + } + sig = &ZoneSignature{ ZoneKey{ Type: pk.ztype, KeyData: pk.pub.Bytes(), @@ -219,7 +225,7 @@ func (pk *EDKEYPrivateImpl) Sign(data []byte) (*ZoneSignature, error) { sd, sigImpl, } - return sig, nil + return } //---------------------------------------------------------------------- diff --git a/src/gnunet/crypto/gns_pkey.go b/src/gnunet/crypto/gns_pkey.go @@ -27,6 +27,7 @@ import ( "github.com/bfix/gospel/crypto/ed25519" "github.com/bfix/gospel/data" + "github.com/bfix/gospel/logger" "github.com/bfix/gospel/math" "golang.org/x/crypto/hkdf" ) @@ -76,15 +77,15 @@ func (pk *PKEYPublicImpl) Bytes() []byte { // Derive a public key from this key based on a big integer // (key blinding). Returns the derived key and the blinding value. -func (pk *PKEYPublicImpl) Derive(h *math.Int) (ZoneKeyImpl, *math.Int) { +func (pk *PKEYPublicImpl) Derive(h *math.Int) (dPk ZoneKeyImpl, hOut *math.Int, err error) { // limit to allowed value range - h = h.Mod(ed25519.GetCurve().N) - derived := pk.pub.Mult(h) - dPk := &PKEYPublicImpl{ + hOut = h.Mod(ed25519.GetCurve().N) + derived := pk.pub.Mult(hOut) + dPk = &PKEYPublicImpl{ pk.ztype, derived, } - return dPk, h + return } // Encrypt binary data (of any size). Output can be larger than input @@ -114,7 +115,9 @@ func (pk *PKEYPublicImpl) BlockKey(label string, expires util.AbsoluteTime) (ske kd := pk.pub.Bytes() prk := hkdf.Extract(sha512.New, kd, []byte("gns-aes-ctx-key")) rdr := hkdf.Expand(sha256.New, prk, []byte(label)) - rdr.Read(skey[:32]) + if _, err := rdr.Read(skey[:32]); err != nil { + logger.Printf(logger.ERROR, "[PKEYPublicImpl.BlockKey] failed: %s", err.Error()) + } // assemble initialization vector iv := &struct { @@ -128,7 +131,9 @@ func (pk *PKEYPublicImpl) BlockKey(label string, expires util.AbsoluteTime) (ske } prk = hkdf.Extract(sha512.New, kd, []byte("gns-aes-ctx-iv")) rdr = hkdf.Expand(sha256.New, prk, []byte(label)) - rdr.Read(iv.Nonce) + if _, err := rdr.Read(iv.Nonce); err != nil { + logger.Printf(logger.ERROR, "[PKEYPublicImpl.BlockKey] failed: %s", err.Error()) + } buf, _ := data.Marshal(iv) copy(skey[32:], buf) return @@ -184,30 +189,32 @@ func (pk *PKEYPrivateImpl) Public() ZoneKeyImpl { // Derive a public key from this key based on a big integer // (key blinding). Returns the derived key and the blinding value. -func (pk *PKEYPrivateImpl) Derive(h *math.Int) (ZonePrivateImpl, *math.Int) { +func (pk *PKEYPrivateImpl) Derive(h *math.Int) (dPk ZonePrivateImpl, hOut *math.Int, err error) { // limit to allowed value range - h = h.Mod(ed25519.GetCurve().N) - derived := pk.prv.Mult(h) - dPk := &PKEYPrivateImpl{ + hOut = h.Mod(ed25519.GetCurve().N) + derived := pk.prv.Mult(hOut) + dPk = &PKEYPrivateImpl{ PKEYPublicImpl{ pk.ztype, derived.Public(), }, derived, } - return dPk, h + return } // Verify a signature for binary data -func (pk *PKEYPrivateImpl) Sign(data []byte) (*ZoneSignature, error) { - s, err := pk.prv.EcSign(data) - if err != nil { - return nil, err +func (pk *PKEYPrivateImpl) Sign(data []byte) (sig *ZoneSignature, err error) { + var s *ed25519.EcSignature + if s, err = pk.prv.EcSign(data); err != nil { + return } sd := s.Bytes() sigImpl := new(PKEYSigImpl) - sigImpl.Init(sd) - sig := &ZoneSignature{ + if err = sigImpl.Init(sd); err != nil { + return + } + sig = &ZoneSignature{ ZoneKey{ Type: pk.ztype, KeyData: pk.pub.Bytes(), @@ -215,7 +222,7 @@ func (pk *PKEYPrivateImpl) Sign(data []byte) (*ZoneSignature, error) { sd, sigImpl, } - return sig, nil + return } //---------------------------------------------------------------------- diff --git a/src/gnunet/crypto/gns_test.go b/src/gnunet/crypto/gns_test.go @@ -55,7 +55,9 @@ func TestDeriveBlockKey(t *testing.T) { // create and initialize new public zone key (PKEY) zkey := new(PKEYPublicImpl) - zkey.Init(PUB) + if err := zkey.Init(PUB); err != nil { + t.Fatal(err) + } // derive and check a key for symmetric cipher skey := zkey.BlockKey(LABEL, EXPIRE) @@ -267,7 +269,10 @@ func TestDeriveH(t *testing.T) { } // test key derivation - dpub, h := pub.Derive(LABEL, CONTEXT) + dpub, h, err := pub.Derive(LABEL, CONTEXT) + if err != nil { + t.Fatal(err) + } if !bytes.Equal(h.Bytes(), H) { if testing.Verbose() { t.Logf("H(computed) = %s\n", hex.EncodeToString(h.Bytes())) @@ -297,7 +302,6 @@ func TestDeriveH(t *testing.T) { } func TestHKDF_gnunet(t *testing.T) { - var ( // SALT as defined in GNUnet salt = []byte("key-derivation") @@ -332,7 +336,9 @@ func TestHKDF_gnunet(t *testing.T) { rdr := hkdf.Expand(sha256.New, prk, info) okm := make([]byte, len(OKM)) - rdr.Read(okm) + if _, err := rdr.Read(okm); err != nil { + t.Fatal(err) + } if testing.Verbose() { t.Log("OKM(computed) = " + hex.EncodeToString(okm)) } @@ -387,7 +393,9 @@ func TestHDKF(t *testing.T) { rdr := hkdf.Expand(sha512.New, prk, info) okm := make([]byte, len(OKM)) - rdr.Read(okm) + if _, err := rdr.Read(okm); err != nil { + t.Fatal(err) + } if testing.Verbose() { t.Log("OKM(computed) = " + hex.EncodeToString(okm)) } diff --git a/src/gnunet/crypto/hash.go b/src/gnunet/crypto/hash.go @@ -21,6 +21,7 @@ package crypto import ( "bytes" "crypto/sha512" + "encoding/hex" "gnunet/util" ) @@ -35,7 +36,17 @@ func (hc *HashCode) Equals(n *HashCode) bool { return bytes.Equal(hc.Bits, n.Bits) } -// NewHashCode creates a new (initalized) hash value +// Clone the hash code +func (hc *HashCode) Clone() *HashCode { + return NewHashCode(hc.Bits) +} + +// String returns a hex-representation of the hash code +func (hc *HashCode) String() string { + return hex.EncodeToString(hc.Bits) +} + +// NewHashCode creates a new (initialized) hash value func NewHashCode(buf []byte) *HashCode { hc := &HashCode{ Bits: make([]byte, 64), diff --git a/src/gnunet/crypto/key_exchange_test.go b/src/gnunet/crypto/key_exchange_test.go @@ -29,14 +29,14 @@ import ( ) var ( - d_1 = []byte{ + d1 = []byte{ 0x7F, 0xDE, 0x7A, 0xAA, 0xEA, 0x0D, 0xA1, 0x7A, 0x7B, 0xCB, 0x4F, 0x57, 0x49, 0xCC, 0xA9, 0xBE, 0xA7, 0xFB, 0x2B, 0x85, 0x77, 0xAD, 0xC9, 0x55, 0xDA, 0xB2, 0x68, 0xB2, 0xB4, 0xCC, 0x24, 0x78, } - d_2 = []byte{ + d2 = []byte{ 0x20, 0x3f, 0x2f, 0x8c, 0x54, 0xf4, 0x1a, 0xd3, 0x01, 0x9a, 0x56, 0x92, 0x19, 0xda, 0xee, 0x4f, 0xd2, 0x53, 0x55, 0xa6, 0x3c, 0xfc, 0x57, 0x40, @@ -54,11 +54,11 @@ var ( 0x05, 0xbd, 0x1b, 0x85, 0xd5, 0xfd, 0x63, 0x60, } - prv_1, prv_2 *ed25519.PrivateKey - pub_1, pub_2 *ed25519.PublicKey - ss_1, ss_2 []byte + prv1, prv2 *ed25519.PrivateKey + pub1, pub2 *ed25519.PublicKey + ss1, ss2 []byte - ED25519_N = ed25519.GetCurve().N + ed25519N = ed25519.GetCurve().N ) func calcSharedSecret() bool { @@ -67,29 +67,29 @@ func calcSharedSecret() bool { return x[:] } // compute shared secret - ss_1 = calc(prv_1, pub_2) - ss_2 = calc(prv_2, pub_1) - return bytes.Equal(ss_1, ss_2) + ss1 = calc(prv1, pub2) + ss2 = calc(prv2, pub1) + return bytes.Equal(ss1, ss2) } func TestDHE(t *testing.T) { // generate two key pairs - prv_1 = ed25519.NewPrivateKeyFromD(math.NewIntFromBytes(d_1)) - pub_1 = prv_1.Public() - prv_2 = ed25519.NewPrivateKeyFromD(math.NewIntFromBytes(d_2)) - pub_2 = prv_2.Public() + prv1 = ed25519.NewPrivateKeyFromD(math.NewIntFromBytes(d1)) + pub1 = prv1.Public() + prv2 = ed25519.NewPrivateKeyFromD(math.NewIntFromBytes(d2)) + pub2 = prv2.Public() if !calcSharedSecret() { t.Fatal("Shared secret mismatch") } if testing.Verbose() { - t.Logf("SS_1 = %s\n", hex.EncodeToString(ss_1)) - t.Logf("SS_2 = %s\n", hex.EncodeToString(ss_2)) + t.Logf("SS_1 = %s\n", hex.EncodeToString(ss1)) + t.Logf("SS_2 = %s\n", hex.EncodeToString(ss2)) } - if !bytes.Equal(ss_1[:], ss) { + if !bytes.Equal(ss1[:], ss) { t.Logf("SS(expected) = %s\n", hex.EncodeToString(ss)) - t.Logf("SS(computed) = %s\n", hex.EncodeToString(ss_1[:])) + t.Logf("SS(computed) = %s\n", hex.EncodeToString(ss1[:])) t.Fatal("Wrong shared secret:") } } @@ -98,19 +98,19 @@ func TestDHERandom(t *testing.T) { failed := 0 once := false for i := 0; i < 100; i++ { - prv_1 = ed25519.NewPrivateKeyFromD(math.NewIntRnd(ED25519_N)) - pub_1 = prv_1.Public() - prv_2 = ed25519.NewPrivateKeyFromD(math.NewIntRnd(ED25519_N)) - pub_2 = prv_2.Public() + prv1 = ed25519.NewPrivateKeyFromD(math.NewIntRnd(ed25519N)) + pub1 = prv1.Public() + prv2 = ed25519.NewPrivateKeyFromD(math.NewIntRnd(ed25519N)) + pub2 = prv2.Public() if !calcSharedSecret() { if !once { once = true - t.Logf("d1=%s\n", hex.EncodeToString(prv_1.D.Bytes())) - t.Logf("d2=%s\n", hex.EncodeToString(prv_2.D.Bytes())) - t.Logf("ss1=%s\n", hex.EncodeToString(ss_1)) - t.Logf("ss2=%s\n", hex.EncodeToString(ss_2)) - dd := prv_1.D.Mul(prv_2.D).Mod(ED25519_N) + t.Logf("d1=%s\n", hex.EncodeToString(prv1.D.Bytes())) + t.Logf("d2=%s\n", hex.EncodeToString(prv2.D.Bytes())) + t.Logf("ss1=%s\n", hex.EncodeToString(ss1)) + t.Logf("ss2=%s\n", hex.EncodeToString(ss2)) + dd := prv1.D.Mul(prv2.D).Mod(ed25519N) pk := sha512.Sum512(ed25519.NewPrivateKeyFromD(dd).Public().Q.X().Bytes()) t.Logf("ss0=%s\n", hex.EncodeToString(pk[:])) } diff --git a/src/gnunet/crypto/keys_test.go b/src/gnunet/crypto/keys_test.go @@ -58,7 +58,7 @@ var ( func TestPrvKey(t *testing.T) { if testing.Verbose() { t.Logf("PRIVATE (seed=%s)\n", hex.EncodeToString(seed)) - t.Logf(" d = %s\n", hex.EncodeToString(prv_1.D.Bytes())) + t.Logf(" d = %s\n", hex.EncodeToString(prv1.D.Bytes())) t.Logf(" ID = '%s'\n", util.EncodeBinaryToString(seed)) } diff --git a/src/gnunet/enums/blocktype_string.go b/src/gnunet/enums/blocktype_string.go @@ -40,7 +40,7 @@ var ( func (i BlockType) String() string { switch { - case 0 <= i && i <= 2: + case i <= 2: return _BlockType_name_0[_BlockType_index_0[i]:_BlockType_index_0[i+1]] case 6 <= i && i <= 13: i -= 6 diff --git a/src/gnunet/enums/dht.go b/src/gnunet/enums/dht.go @@ -16,18 +16,16 @@ // // SPDX-License-Identifier: AGPL3.0-or-later +//nolint:stylecheck // allow non-camel-case for constants package enums // DHT flags and settings const ( - DHT_RO_NONE = 0 // Default. Do nothing special. - DHT_RO_DEMULTIPLEX_EVERYWHERE = 1 // Each peer along the way should look at 'enc' - DHT_RO_RECORD_ROUTE = 2 // keep track of the route that the message took in the P2P network. - DHT_RO_FIND_PEER = 3 // This is a 'FIND-PEER' request, so approximate results are fine. - DHT_RO_BART = 4 // Possible message option for query key randomization. - DHT_RO_LAST_HOP = 16 // Flag given to monitors if this was the last hop for a GET/PUT. - - DHT_GNS_REPLICATION_LEVEL = 10 + DHT_RO_NONE = 0 // Default. Do nothing special. + DHT_RO_DEMULTIPLEX_EVERYWHERE = 1 // Each peer along the way should look at 'enc' + DHT_RO_RECORD_ROUTE = 2 // keep track of the route that the message took in the P2P network. + DHT_RO_FIND_APPROXIMATE = 4 // Approximate results are fine. + DHT_RO_TRUNCATED = 8 // Flag if path is truncated ) //go:generate go run generate.go gnunet-dht.rec gnunet-dht.tpl dht_block_type.go diff --git a/src/gnunet/enums/dht_block_type.go b/src/gnunet/enums/dht_block_type.go @@ -1,8 +1,9 @@ // Code generated by enum generator; DO NOT EDIT. +//nolint:stylecheck // allow non-camel-case for constants package enums -type BlockType int +type BlockType uint16 // DHT block types const ( diff --git a/src/gnunet/enums/gns.go b/src/gnunet/enums/gns.go @@ -16,6 +16,7 @@ // // SPDX-License-Identifier: AGPL3.0-or-later +//nolint:stylecheck // allow non-camel-case for constants package enums const ( @@ -31,6 +32,8 @@ const ( GNS_LO_LOCAL_MASTER = 2 // For the rightmost label, only look in the cache. GNS_MAX_BLOCK_SIZE = (63 * 1024) // Maximum size of a value that can be stored in a GNS block. + + GNS_REPLICATION_LEVEL = 10 ) //go:generate go run generate.go gnunet-gns.rec gnunet-gns.tpl gns_type.go diff --git a/src/gnunet/enums/gns_type.go b/src/gnunet/enums/gns_type.go @@ -1,5 +1,6 @@ // Code generated by enum generator; DO NOT EDIT. +//nolint:stylecheck // allow non-camel-case for constants package enums type GNSType int diff --git a/src/gnunet/enums/gnunet-dht.tpl b/src/gnunet/enums/gnunet-dht.tpl @@ -1,8 +1,9 @@ // Code generated by enum generator; DO NOT EDIT. +//nolint:stylecheck // allow non-camel-case for constants package enums -type BlockType int +type BlockType uint16 // DHT block types const ( diff --git a/src/gnunet/enums/gnunet-gns.tpl b/src/gnunet/enums/gnunet-gns.tpl @@ -1,5 +1,6 @@ // Code generated by enum generator; DO NOT EDIT. +//nolint:stylecheck // allow non-camel-case for constants package enums type GNSType int diff --git a/src/gnunet/enums/gnunet-signature.tpl b/src/gnunet/enums/gnunet-signature.tpl @@ -1,5 +1,6 @@ // Code generated by enum generator; DO NOT EDIT. +//nolint:stylecheck // allow non-camel-case for constants package enums type SigPurpose int diff --git a/src/gnunet/enums/signature_purpose.go b/src/gnunet/enums/signature_purpose.go @@ -1,5 +1,6 @@ // Code generated by enum generator; DO NOT EDIT. +//nolint:stylecheck // allow non-camel-case for constants package enums type SigPurpose int diff --git a/src/gnunet/go.mod b/src/gnunet/go.mod @@ -3,10 +3,11 @@ module gnunet go 1.18 require ( - github.com/bfix/gospel v1.2.14 + github.com/bfix/gospel v1.2.15 github.com/go-redis/redis/v8 v8.11.5 github.com/go-sql-driver/mysql v1.6.0 github.com/gorilla/mux v1.8.0 + github.com/gorilla/rpc v1.2.0 github.com/mattn/go-sqlite3 v1.14.13 github.com/miekg/dns v1.1.49 golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898 diff --git a/src/gnunet/go.sum b/src/gnunet/go.sum @@ -1,5 +1,5 @@ -github.com/bfix/gospel v1.2.14 h1:lIdagJvkebG+uYbVdfK6XbT1udnq/ezd/Gi54EaMtV0= -github.com/bfix/gospel v1.2.14/go.mod h1:cdu63bA9ZdfeDoqZ+vnWOcbY9Puwdzmf5DMxMGMznRI= +github.com/bfix/gospel v1.2.15 h1:f0t8dvihSXWvhnXDI2q7FCtG7LHg5qImjEWdzIN/luY= +github.com/bfix/gospel v1.2.15/go.mod h1:cdu63bA9ZdfeDoqZ+vnWOcbY9Puwdzmf5DMxMGMznRI= github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= @@ -11,6 +11,8 @@ github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfC github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/rpc v1.2.0 h1:WvvdC2lNeT1SP32zrIce5l0ECBfbAlmrmSBsuc57wfk= +github.com/gorilla/rpc v1.2.0/go.mod h1:V4h9r+4sF5HnzqbwIez0fKSpANP0zlYd3qR7p36jkTQ= github.com/huin/goupnp v1.0.0 h1:wg75sLpL6DZqwHQN6E1Cfk6mtfzS45z8OV+ic+DtHRo= github.com/huin/goupnp v1.0.0/go.mod h1:n9v9KO1tAxYH82qOn+UTIFQDmx5n1Zxd/ClZDMX7Bnc= github.com/huin/goutil v0.0.0-20170803182201-1ca381bf3150/go.mod h1:PpLOETDnJ0o3iZrZfqZzyLl6l7F3c6L1oWn7OICBi6o= diff --git a/src/gnunet/message/factory.go b/src/gnunet/message/factory.go @@ -66,8 +66,18 @@ func NewEmptyMessage(msgType uint16) (Message, error) { return NewDHTClientResultMsg(nil), nil case DHT_CLIENT_GET_RESULTS_KNOWN: return NewDHTClientGetResultsKnownMsg(nil), nil + + //------------------------------------------------------------------ + // DHT-P2P + //------------------------------------------------------------------ case DHT_P2P_HELLO: - return NewHelloDHTMsg(), nil + return NewDHTP2PHelloMsg(), nil + case DHT_P2P_GET: + return NewDHTP2PGetMsg(), nil + case DHT_P2P_PUT: + return NewDHTP2PPutMsg(), nil + case DHT_P2P_RESULT: + return NewDHTP2PResultMsg(), nil //------------------------------------------------------------------ // GNS diff --git a/src/gnunet/message/msg_core.go b/src/gnunet/message/msg_core.go @@ -36,17 +36,17 @@ type EphKeyBlock struct { Purpose *crypto.SignaturePurpose // signature purpose: SIG_ECC_KEY CreateTime util.AbsoluteTime // Time of key creation ExpireTime util.RelativeTime // Time to live for key - EphemeralKey []byte `size:"32"` // Ephemeral EdDSA public key + EphemeralKey *util.PeerPublicKey // Ephemeral EdDSA public key PeerID *util.PeerID // Peer identity (EdDSA public key) } // EphemeralKeyMsg announces a new transient key for a peer. The key is signed // by the issuing peer. type EphemeralKeyMsg struct { - MsgSize uint16 `order:"big"` // total size of message - MsgType uint16 `order:"big"` // CORE_EPHEMERAL_KEY (88) - SenderStatus uint32 `order:"big"` // enum PeerStateMachine - Signature []byte `size:"64"` // EdDSA signature + MsgSize uint16 `order:"big"` // total size of message + MsgType uint16 `order:"big"` // CORE_EPHEMERAL_KEY (88) + SenderStatus uint32 `order:"big"` // enum PeerStateMachine + Signature *util.PeerSignature `` // EdDSA signature SignedBlock *EphKeyBlock } @@ -56,7 +56,7 @@ func NewEphemeralKeyMsg() *EphemeralKeyMsg { MsgSize: 160, MsgType: CORE_EPHEMERAL_KEY, SenderStatus: 1, - Signature: make([]byte, 64), + Signature: util.NewPeerSignature(nil), SignedBlock: &EphKeyBlock{ Purpose: &crypto.SignaturePurpose{ Size: 88, @@ -64,7 +64,7 @@ func NewEphemeralKeyMsg() *EphemeralKeyMsg { }, CreateTime: util.AbsoluteTimeNow(), ExpireTime: util.NewRelativeTime(12 * time.Hour), - EphemeralKey: make([]byte, 32), + EphemeralKey: util.NewPeerPublicKey(nil), PeerID: util.NewPeerID(nil), }, } @@ -73,8 +73,8 @@ func NewEphemeralKeyMsg() *EphemeralKeyMsg { // String returns a human-readable representation of the message. func (m *EphemeralKeyMsg) String() string { return fmt.Sprintf("EphKeyMsg{peer=%s,ephkey=%s,create=%s,expire=%s,status=%d}", - util.EncodeBinaryToString(m.SignedBlock.PeerID.Key), - util.EncodeBinaryToString(m.SignedBlock.EphemeralKey), + util.EncodeBinaryToString(m.SignedBlock.PeerID.Data), + util.EncodeBinaryToString(m.SignedBlock.EphemeralKey.Data), m.SignedBlock.CreateTime, m.SignedBlock.ExpireTime, m.SenderStatus) } @@ -85,8 +85,8 @@ func (m *EphemeralKeyMsg) Header() *Header { } // Public extracts the public key of an announcing peer. -func (m *EphemeralKeyMsg) Public() *ed25519.PublicKey { - return m.SignedBlock.PeerID.PublicKey() +func (m *EphemeralKeyMsg) Public() *util.PeerPublicKey { + return util.NewPeerPublicKey(m.SignedBlock.PeerID.Data) } // Verify the integrity of the message data using the public key of the @@ -96,7 +96,7 @@ func (m *EphemeralKeyMsg) Verify(pub *ed25519.PublicKey) (bool, error) { if err != nil { return false, err } - sig, err := ed25519.NewEdSignatureFromBytes(m.Signature) + sig, err := ed25519.NewEdSignatureFromBytes(m.Signature.Data) if err != nil { return false, err } @@ -107,10 +107,10 @@ func (m *EphemeralKeyMsg) Verify(pub *ed25519.PublicKey) (bool, error) { // key and the corresponding GNUnet message to announce the new key. func NewEphemeralKey(peerID []byte, ltPrv *ed25519.PrivateKey) (*ed25519.PrivateKey, *EphemeralKeyMsg, error) { msg := NewEphemeralKeyMsg() - copy(msg.SignedBlock.PeerID.Key, peerID) + copy(msg.SignedBlock.PeerID.Data, peerID) seed := util.NewRndArray(32) prv := ed25519.NewPrivateKeyFromSeed(seed) - copy(msg.SignedBlock.EphemeralKey, prv.Public().Bytes()) + copy(msg.SignedBlock.EphemeralKey.Data, prv.Public().Bytes()) data, err := data.Marshal(msg.SignedBlock) if err != nil { @@ -120,7 +120,7 @@ func NewEphemeralKey(peerID []byte, ltPrv *ed25519.PrivateKey) (*ed25519.Private if err != nil { return nil, nil, err } - copy(msg.Signature, sig.Bytes()) + copy(msg.Signature.Data, sig.Bytes()) return prv, msg, nil } diff --git a/src/gnunet/message/msg_dht_p2p.go b/src/gnunet/message/msg_dht_p2p.go @@ -0,0 +1,473 @@ +// This file is part of gnunet-go, a GNUnet-implementation in Golang. +// Copyright (C) 2019-2022 Bernd Fix >Y< +// +// gnunet-go 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. +// +// gnunet-go 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/>. +// +// SPDX-License-Identifier: AGPL3.0-or-later + +package message + +import ( + "bytes" + "crypto/sha512" + "encoding/binary" + "errors" + "fmt" + "gnunet/crypto" + "gnunet/enums" + "gnunet/service/dht/blocks" + "gnunet/util" + "time" + + "github.com/bfix/gospel/crypto/ed25519" + "github.com/bfix/gospel/data" + "github.com/bfix/gospel/logger" +) + +//====================================================================== +// DHT-P2P is a next-generation implementation of the R5N DHT. +//====================================================================== + +// shared path element data across types +type pathElementData struct { + Expiration util.AbsoluteTime // expiration date + BlockHash *crypto.HashCode // block hash + PeerPredecessor *util.PeerID // predecessor peer + PeerSuccessor *util.PeerID // successor peer +} + +// helper type for signature creation/verification +type pathElementSignedData struct { + Size uint16 `order:"big"` // size of signed data + Purpose uint16 `order:"big"` // signature purpose (SIG_DHT_HOP) + Elem *pathElementData `` // path element data +} + +// PathElement is the full-fledged data assembly for a path element in +// PUT/GET pathes. It is assembled programatically (on generation[1] and +// verification[2]) and not transferred in messages directly. +// +// [1] spe = &PathElement{...} +// core.Sign(spe) +// msg.putpath[i] = spe.Wire() +// +// [2] pe = &PathElement{...,Signature: wire.sig} +// if !pe.Verify(peerId) { ... } +// +type PathElement struct { + pathElementData + Signature *util.PeerSignature // signature +} + +// NewPathElement creates a new path element from data +func NewPathElement(key *crypto.HashCode, pred, succ *util.PeerID) *PathElement { + return &PathElement{ + pathElementData: pathElementData{ + Expiration: util.AbsoluteTimeNow().Add(12 * time.Hour), + BlockHash: key, + PeerPredecessor: pred, + PeerSuccessor: succ, + }, + Signature: nil, + } +} + +// PathElementWire is the data stored and retrieved from messages +type PathElementWire struct { + Predecessor *util.PeerID // peer id of predecessor + Signature *util.PeerSignature // path signature +} + +// Size returns the size of a path element in wire format +func (pew *PathElementWire) Size() uint16 { + return 96 +} + +// SignedData gets the data to be signed by peer ('Signable' interface) +func (pe *PathElement) SignedData() []byte { + sd := &pathElementSignedData{ + Size: 80, + Purpose: uint16(enums.SIG_DHT_HOP), + Elem: &(pe.pathElementData), + } + buf, err := data.Marshal(sd) + if err != nil { + logger.Println(logger.ERROR, "can't serialize path element for signature") + return nil + } + return buf +} + +// SetSignature stores the generated signature. +func (pe *PathElement) SetSignature(sig *util.PeerSignature) error { + pe.Signature = sig + return nil +} + +// Wire returns the path element suitable for inclusion into messages +func (pe *PathElement) Wire() *PathElementWire { + return &PathElementWire{ + Predecessor: pe.PeerPredecessor, + Signature: pe.Signature, + } +} + +//---------------------------------------------------------------------- +// DHT-P2P-GET messages are used to request information from other +// peers in the DHT. +//---------------------------------------------------------------------- + +// DHTP2PGetMsg wire layout +type DHTP2PGetMsg struct { + MsgSize uint16 `order:"big"` // total size of message + MsgType uint16 `order:"big"` // DHT_P2P_GET (147) + BType uint32 `order:"big"` // content type of the payload + Flags uint16 `order:"big"` // processing flags + HopCount uint16 `order:"big"` // number of hops so far + ReplLevel uint16 `order:"big"` // Replication level + RfSize uint16 `order:"big"` // size of result filter + PeerFilter *blocks.PeerFilter `` // peer filter to prevent loops + Query *crypto.HashCode `` // query hash + ResFilter []byte `size:"RfSize"` // result filter + XQuery []byte `size:"*"` // extended query +} + +// NewDHTP2PGetMsg creates an empty DHT-P2P-Get message +func NewDHTP2PGetMsg() *DHTP2PGetMsg { + return &DHTP2PGetMsg{ + MsgSize: 208, // message size without ResFiter and XQuery + MsgType: DHT_P2P_GET, // DHT_P2P_GET (147) + BType: 0, // no block type defined + Flags: 0, // no flags defined + HopCount: 0, // no hops + ReplLevel: 0, // no replication level defined + RfSize: 0, // no result filter + PeerFilter: blocks.NewPeerFilter(), // allocate bloom filter + Query: crypto.NewHashCode(nil), // empty Query hash + ResFilter: nil, // empty result filter + XQuery: nil, // empty XQuery + } +} + +// String returns a human-readable representation of the message. +func (m *DHTP2PGetMsg) String() string { + return fmt.Sprintf("DHTP2PGetMsg{btype=%s,hops=%d,flags=%d}", + enums.BlockType(m.BType).String(), m.HopCount, m.Flags) +} + +// Header returns the message header in a separate instance. +func (m *DHTP2PGetMsg) Header() *Header { + return &Header{m.MsgSize, m.MsgType} +} + +// Clone message +func (m *DHTP2PGetMsg) Update(pf *blocks.PeerFilter, rf blocks.ResultFilter, hop uint16) *DHTP2PGetMsg { + buf := rf.Bytes() + ns := uint16(len(buf)) + return &DHTP2PGetMsg{ + MsgSize: m.MsgSize - m.RfSize + ns, + MsgType: DHT_P2P_GET, + BType: m.BType, + Flags: m.Flags, + HopCount: hop, + ReplLevel: m.ReplLevel, + RfSize: ns, + PeerFilter: pf.Clone(), + Query: m.Query, + ResFilter: buf, + XQuery: util.Clone(m.XQuery), + } +} + +//---------------------------------------------------------------------- +// DHT-P2P-PUT messages are used by other peers in the DHT to +// request block storage. +//---------------------------------------------------------------------- + +// DHTP2PPutMsg wire layout +type DHTP2PPutMsg struct { + MsgSize uint16 `order:"big"` // total size of message + MsgType uint16 `order:"big"` // DHT_P2P_PUT (146) + BType uint32 `order:"big"` // block type + Flags uint16 `order:"big"` // processing flags + HopCount uint16 `order:"big"` // message hops + ReplLvl uint16 `order:"big"` // replication level + PathL uint16 `order:"big"` // path length + Expiration util.AbsoluteTime `` // expiration date + PeerFilter *blocks.PeerFilter `` // peer bloomfilter + Key *crypto.HashCode `` // query key to block + Origin []byte `size:"(PESize)"` // truncated origin (if TRUNCATED flag set) + PutPath []*PathElementWire `size:"PathL"` // PUT path + LastSig []byte `size:"(PESize)"` // signature of last hop (if RECORD_ROUTE flag is set) + Block []byte `size:"*"` // block data +} + +// NewDHTP2PPutMsg creates an empty new DHTP2PPutMsg +func NewDHTP2PPutMsg() *DHTP2PPutMsg { + return &DHTP2PPutMsg{ + MsgSize: 218, // total size without path and block data + MsgType: DHT_P2P_PUT, // DHT_P2P_PUT (146) + BType: 0, // block type + Flags: 0, // processing flags + HopCount: 0, // message hops + ReplLvl: 0, // replication level + PathL: 0, // no PUT path + Expiration: util.AbsoluteTimeNever(), // expiration date + PeerFilter: blocks.NewPeerFilter(), // peer bloom filter + Key: crypto.NewHashCode(nil), // query key + Origin: nil, // no truncated path + PutPath: make([]*PathElementWire, 0), // empty PUT path + LastSig: nil, // no signature from last hop + Block: nil, // no block data + } +} + +// PESize calculates field sizes based on flags and attributes +func (m *DHTP2PPutMsg) PESize(field string) uint { + switch field { + case "Origin": + if m.Flags&enums.DHT_RO_TRUNCATED != 0 { + return 32 + } + case "LastSig": + if m.Flags&enums.DHT_RO_RECORD_ROUTE != 0 { + return 64 + } + } + return 0 +} + +// AddPutPath adds an element to the PUT path +func (m *DHTP2PPutMsg) AppendPutPath(pe *PathElement) { + pew := pe.Wire() + m.PutPath = append(m.PutPath, pew) + m.PathL++ + m.MsgSize += pew.Size() +} + +// String returns a human-readable representation of the message. +func (m *DHTP2PPutMsg) String() string { + return fmt.Sprintf("DHTP2PPutMsg{btype=%s,hops=%d,flags=%d}", + enums.BlockType(m.BType).String(), m.HopCount, m.Flags) +} + +// Header returns the message header in a separate instance. +func (m *DHTP2PPutMsg) Header() *Header { + return &Header{m.MsgSize, m.MsgType} +} + +//---------------------------------------------------------------------- +// DHT-P2P-RESULT messages are used to answer peer requests for +// bock retrieval. +//---------------------------------------------------------------------- + +// DHTP2PResultMsg wire layout +type DHTP2PResultMsg struct { + MsgSize uint16 `order:"big"` // total size of message + MsgType uint16 `order:"big"` // DHT_P2P_RESULT (148) + BType uint32 `order:"big"` // Block type of result + Reserved uint32 `order:"big"` // Reserved for further use + PutPathL uint16 `order:"big"` // size of PUTPATH field + GetPathL uint16 `order:"big"` // size of GETPATH field + Expires util.AbsoluteTime `` // expiration date + Query *crypto.HashCode `` // Query key for block + Origin []byte `size:"(PESize)"` // truncated origin (if TRUNCATED flag set) + PutPath []*PathElementWire `size:"PutPathL"` // PUTPATH + GetPath []*PathElementWire `size:"GetPathL"` // GETPATH + LastSig []byte `size:"(PESize)"` // signature of last hop (if RECORD_ROUTE flag is set) + Block []byte `size:"*"` // block data +} + +// NewDHTP2PResultMsg creates a new empty DHTP2PResultMsg +func NewDHTP2PResultMsg() *DHTP2PResultMsg { + return &DHTP2PResultMsg{ + MsgSize: 88, // size of empty message + MsgType: DHT_P2P_RESULT, // DHT_P2P_RESULT (148) + BType: uint32(enums.BLOCK_TYPE_ANY), // type of returned block + Origin: nil, // no truncated origin + PutPathL: 0, // empty putpath + PutPath: nil, // -"- + GetPathL: 0, // empty getpath + GetPath: nil, // -"- + LastSig: nil, // no recorded route + Block: nil, // empty block + } +} + +// PESize calculates field sizes based on flags and attributes +func (m *DHTP2PResultMsg) PESize(field string) uint { + switch field { + case "Origin": + //if m.Flags&enums.DHT_RO_TRUNCATED != 0 { + return 32 + //} + case "LastSig": + //if m.Flags&enums.DHT_RO_RECORD_ROUTE != 0 { + return 64 + //} + } + return 0 +} + +// String returns a human-readable representation of the message. +func (m *DHTP2PResultMsg) String() string { + return fmt.Sprintf("DHTP2PResultMsg{btype=%s,putl=%d,getl=%d}", + enums.BlockType(m.BType).String(), m.PutPathL, m.GetPathL) +} + +// Header returns the message header in a separate instance. +func (m *DHTP2PResultMsg) Header() *Header { + return &Header{m.MsgSize, m.MsgType} +} + +//---------------------------------------------------------------------- +// DHT-P2P-HELLO +// +// A DHT-P2P-HELLO message is used to exchange information about transports +// with other DHT nodes. This struct is always followed by the actual +// network addresses of type "HelloAddress" +//---------------------------------------------------------------------- + +// DHTP2PHelloMsg is a message send by peers to announce their presence +type DHTP2PHelloMsg struct { + MsgSize uint16 `order:"big"` // total size of message + MsgType uint16 `order:"big"` // DHT_P2P_HELLO (157) + Reserved uint16 `order:"big"` // Reserved for further use + NumAddr uint16 `order:"big"` // Number of addresses in list + Signature *util.PeerSignature `` // Signature + Expires util.AbsoluteTime `` // expiration time + AddrList []byte `size:"*"` // List of end-point addresses (HelloAddress) +} + +// NewHelloMsgDHT creates an empty DHT_P2P_HELLO message. +func NewDHTP2PHelloMsg() *DHTP2PHelloMsg { + // return empty HelloMessage + exp := time.Now().Add(HelloAddressExpiration) + return &DHTP2PHelloMsg{ + MsgSize: 80, // size without 'AddrList' + MsgType: DHT_P2P_HELLO, // DHT_P2P_HELLO (157) + Reserved: 0, // not used here + NumAddr: 0, // start with empty address list + Signature: util.NewPeerSignature(nil), // signature + Expires: util.NewAbsoluteTime(exp), // default expiration + AddrList: make([]byte, 0), // list of addresses + } +} + +// Addresses returns the list of HelloAddress +func (m *DHTP2PHelloMsg) Addresses() (list []*util.Address, err error) { + var addr *util.Address + var as string + num, pos := 0, 0 + for { + // parse address string from stream + if as, pos = util.ReadCString(m.AddrList, pos); pos == -1 { + break + } + if addr, err = util.ParseAddress(as); err != nil { + return + } + addr.Expires = m.Expires + list = append(list, addr) + num++ + } + // check numbers + if num != int(m.NumAddr) { + logger.Printf(logger.WARN, "[DHTP2PHelloMsg] Number of addresses does not match (got %d, expected %d)", num, m.NumAddr) + } + return +} + +// SetAddresses adds addresses to the HELLO message. +func (m *DHTP2PHelloMsg) SetAddresses(list []*util.Address) { + // write addresses as blob and track earliest expiration + exp := util.NewAbsoluteTime(time.Now().Add(HelloAddressExpiration)) + wrt := new(bytes.Buffer) + for _, addr := range list { + // check if address expires before current expire + if exp.Compare(addr.Expires) > 0 { + exp = addr.Expires + } + n, _ := wrt.Write([]byte(addr.URI())) + wrt.WriteByte(0) + m.MsgSize += uint16(n + 1) + } + m.AddrList = wrt.Bytes() + m.Expires = exp + m.NumAddr = uint16(len(list)) +} + +// String returns a human-readable representation of the message. +func (m *DHTP2PHelloMsg) String() string { + addrs, _ := m.Addresses() + aList := "" + for i, a := range addrs { + if i > 0 { + aList += "," + } + aList += a.URI() + } + return fmt.Sprintf("DHTP2PHelloMsg{expire:%s,addrs=%d:[%s]}", m.Expires, m.NumAddr, aList) +} + +// Header returns the message header in a separate instance. +func (m *DHTP2PHelloMsg) Header() *Header { + return &Header{m.MsgSize, m.MsgType} +} + +// Verify the message signature +func (m *DHTP2PHelloMsg) Verify(peer *util.PeerID) (bool, error) { + // assemble signed data and public key + sd := m.SignedData() + pub := ed25519.NewPublicKeyFromBytes(peer.Data) + sig, err := ed25519.NewEdSignatureFromBytes(m.Signature.Data) + if err != nil { + return false, err + } + return pub.EdVerify(sd, sig) +} + +// SetSignature stores a signature in the the HELLO block +func (m *DHTP2PHelloMsg) SetSignature(sig *util.PeerSignature) error { + m.Signature = sig + return nil +} + +// SignedData assembles a data block for sign and verify operations. +func (m *DHTP2PHelloMsg) SignedData() []byte { + // hash address block + hAddr := sha512.Sum512(m.AddrList) + var size uint32 = 80 + purpose := uint32(enums.SIG_HELLO) + + // assemble signed data + buf := new(bytes.Buffer) + var n int + err := binary.Write(buf, binary.BigEndian, size) + if err == nil { + if err = binary.Write(buf, binary.BigEndian, purpose); err == nil { + if err = binary.Write(buf, binary.BigEndian, m.Expires.Epoch()*1000000); err == nil { + if n, err = buf.Write(hAddr[:]); err == nil { + if n != len(hAddr[:]) { + err = errors.New("write failed") + } + } + } + } + } + if err != nil { + logger.Printf(logger.ERROR, "[DHTP2PHelloMsg.SignedData] failed: %s", err.Error()) + } + return buf.Bytes() +} diff --git a/src/gnunet/message/msg_hello.go b/src/gnunet/message/msg_hello.go @@ -25,6 +25,8 @@ import ( "gnunet/util" "io" "time" + + "github.com/bfix/gospel/logger" ) //---------------------------------------------------------------------- @@ -112,11 +114,19 @@ func (a *HelloAddress) String() string { // Bytes returns the binary representation of a HelloAddress func (a *HelloAddress) Bytes() []byte { buf := new(bytes.Buffer) - buf.Write([]byte(a.transport)) - buf.WriteByte(0) - binary.Write(buf, binary.BigEndian, a.addrSize) - binary.Write(buf, binary.BigEndian, a.expires.Val) - buf.Write(a.address) + _, err := buf.Write([]byte(a.transport)) + if err == nil { + if err = buf.WriteByte(0); err == nil { + if err = binary.Write(buf, binary.BigEndian, a.addrSize); err == nil { + if err = binary.Write(buf, binary.BigEndian, a.expires.Val); err != nil { + _, err = buf.Write(a.address) + } + } + } + } + if err != nil { + logger.Printf(logger.ERROR, "[HelloAddress] failed: %s", err.Error()) + } return buf.Bytes() } diff --git a/src/gnunet/message/msg_hello_dht.go b/src/gnunet/message/msg_hello_dht.go @@ -1,167 +0,0 @@ -// This file is part of gnunet-go, a GNUnet-implementation in Golang. -// Copyright (C) 2019-2022 Bernd Fix >Y< -// -// gnunet-go 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. -// -// gnunet-go 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/>. -// -// SPDX-License-Identifier: AGPL3.0-or-later - -package message - -import ( - "bytes" - "crypto/sha512" - "encoding/binary" - "fmt" - "gnunet/enums" - "gnunet/util" - "time" - - "github.com/bfix/gospel/crypto/ed25519" - "github.com/bfix/gospel/logger" -) - -//---------------------------------------------------------------------- -// HELLO-DHT -// -// A HELLO message is used to exchange information about transports with -// other DHT nodes. This struct is always followed by the actual network -// addresses of type "HelloAddress" -//---------------------------------------------------------------------- - -// HelloDHTMsg is a message send by peers to announce their presence -type HelloDHTMsg struct { - MsgSize uint16 `order:"big"` // total size of message - MsgType uint16 `order:"big"` // DHT_P2P_HELLO (157) - Reserved uint16 `order:"big"` // Reserved for further use - NumAddr uint16 `order:"big"` // Number of addresses in list - Signature []byte `size:"64"` // Signature - Expires util.AbsoluteTime `` // expiration time - AddrList []byte `size:"*"` // List of end-point addresses (HelloAddress) -} - -// NewHelloMsgDHT creates an empty DHT_P2P_HELLO message. -func NewHelloDHTMsg() *HelloDHTMsg { - // return empty HelloMessage - exp := time.Now().Add(HelloAddressExpiration) - return &HelloDHTMsg{ - MsgSize: 80, // size without 'AddrList' - MsgType: DHT_P2P_HELLO, // DHT_P2P_HELLO (157) - Reserved: 0, // not used here - NumAddr: 0, // start with empty address list - Signature: make([]byte, 64), // signature - Expires: util.NewAbsoluteTime(exp), // default expiration - AddrList: make([]byte, 0), // list of addresses - } -} - -// Addresses returns the list of HelloAddress -func (m *HelloDHTMsg) Addresses() (list []*util.Address, err error) { - var addr *util.Address - var as string - num, pos := 0, 0 - for { - // parse address string from stream - if as, pos = util.ReadCString(m.AddrList, pos); pos == -1 { - break - } - if addr, err = util.ParseAddress(as); err != nil { - return - } - addr.Expires = m.Expires - list = append(list, addr) - num++ - } - // check numbers - if num != int(m.NumAddr) { - logger.Printf(logger.WARN, "[HelloDHTMsg] Number of addresses does not match (got %d, expected %d)", num, m.NumAddr) - } - return -} - -// SetAddresses adds addresses to the HELLO message. -func (m *HelloDHTMsg) SetAddresses(list []*util.Address) { - // write addresses as blob and track earliest expiration - exp := util.NewAbsoluteTime(time.Now().Add(HelloAddressExpiration)) - wrt := new(bytes.Buffer) - for _, addr := range list { - // check if address expires before current expire - if exp.Compare(addr.Expires) > 0 { - exp = addr.Expires - } - n, _ := wrt.Write([]byte(addr.URI())) - wrt.WriteByte(0) - m.MsgSize += uint16(n + 1) - } - m.AddrList = wrt.Bytes() - m.Expires = exp - m.NumAddr = uint16(len(list)) -} - -// String returns a human-readable representation of the message. -func (m *HelloDHTMsg) String() string { - addrs, _ := m.Addresses() - aList := "" - for i, a := range addrs { - if i > 0 { - aList += "," - } - aList += a.URI() - } - return fmt.Sprintf("HelloDHTMsg{expire:%s,addrs=%d:[%s]}", m.Expires, m.NumAddr, aList) -} - -// Header returns the message header in a separate instance. -func (m *HelloDHTMsg) Header() *Header { - return &Header{m.MsgSize, m.MsgType} -} - -// Verify the message signature -func (m *HelloDHTMsg) Verify(peer *util.PeerID) (bool, error) { - // assemble signed data and public key - sd := m.signedData() - pub := peer.PublicKey() - sig, err := ed25519.NewEdSignatureFromBytes(m.Signature) - if err != nil { - return false, err - } - return pub.EdVerify(sd, sig) -} - -// Sign the HELLO data with private key -func (m *HelloDHTMsg) Sign(prv *ed25519.PrivateKey) error { - // assemble signed data - sd := m.signedData() - sig, err := prv.EdSign(sd) - if err != nil { - return err - } - m.Signature = sig.Bytes() - return nil -} - -// signedData assembles a data block for sign and verify operations. -func (m *HelloDHTMsg) signedData() []byte { - // hash address block - hAddr := sha512.Sum512(m.AddrList) - var size uint32 = 80 - purpose := uint32(enums.SIG_HELLO) - - // assemble signed data - buf := new(bytes.Buffer) - binary.Write(buf, binary.BigEndian, size) - binary.Write(buf, binary.BigEndian, purpose) - binary.Write(buf, binary.BigEndian, m.Expires.Epoch()*1000000) - buf.Write(hAddr[:]) - return buf.Bytes() -} diff --git a/src/gnunet/message/msg_namecache.go b/src/gnunet/message/msg_namecache.go @@ -149,7 +149,7 @@ func (m *NamecacheCacheMsg) Header() *Header { // NAMECACHE_BLOCK_CACHE_RESPONSE //---------------------------------------------------------------------- -// NamecacheCacheResponseMsg is the reponse message for a put request +// NamecacheCacheResponseMsg is the response message for a put request type NamecacheCacheResponseMsg struct { MsgSize uint16 `order:"big"` // total size of message MsgType uint16 `order:"big"` // NAMECACHE_LOOKUP_BLOCK_RESPONSE (432) diff --git a/src/gnunet/message/msg_transport.go b/src/gnunet/message/msg_transport.go @@ -28,6 +28,7 @@ import ( "github.com/bfix/gospel/crypto/ed25519" "github.com/bfix/gospel/data" + "github.com/bfix/gospel/logger" ) //---------------------------------------------------------------------- @@ -106,7 +107,9 @@ func NewTransportPingMsg(target *util.PeerID, a *util.Address) *TransportPingMsg // String returns a human-readable representation of the message. func (m *TransportPingMsg) String() string { a := new(util.Address) - data.Unmarshal(a, m.Address) + if err := data.Unmarshal(a, m.Address); err != nil { + logger.Printf(logger.ERROR, "[TransportPingMsg.String] failed: %s", err.Error()) + } return fmt.Sprintf("TransportPingMsg{target=%s,addr=%s,challenge=%d}", m.Target, a, m.Challenge) } @@ -155,7 +158,7 @@ func NewSignedAddress(a *util.Address) *SignedAddress { return addr } -// TransportPongMsg is a reponse message for a PING request +// TransportPongMsg is a response message for a PING request type TransportPongMsg struct { MsgSize uint16 `order:"big"` // total size of message MsgType uint16 `order:"big"` // TRANSPORT_PING (372) @@ -164,7 +167,7 @@ type TransportPongMsg struct { SignedBlock *SignedAddress // signed block of data } -// NewTransportPongMsg creates a reponse message with an address the replying +// NewTransportPongMsg creates a response message with an address the replying // peer wants to be reached. func NewTransportPongMsg(challenge uint32, a *util.Address) *TransportPongMsg { m := &TransportPongMsg{ @@ -189,7 +192,7 @@ func (m *TransportPongMsg) String() string { return fmt.Sprintf("TransportPongMsg{addr=%s,challenge=%d}", a, m.Challenge) } - return fmt.Sprintf("TransportPongMsg{addr=<unkown>,%d}", m.Challenge) + return fmt.Sprintf("TransportPongMsg{addr=<unknown>,%d}", m.Challenge) } // Header returns the message header in a separate instance. @@ -228,7 +231,7 @@ func (m *TransportPongMsg) Verify(pub *ed25519.PublicKey) (bool, error) { // TRANSPORT_SESSION_ACK //---------------------------------------------------------------------- -// SessionAckMsg is a message to acknowlege a session request +// SessionAckMsg is a message to acknowledge a session request type SessionAckMsg struct { MsgSize uint16 `order:"big"` // total size of message MsgType uint16 `order:"big"` // TRANSPORT_SESSION_ACK (377) diff --git a/src/gnunet/message/types.go b/src/gnunet/message/types.go @@ -16,6 +16,7 @@ // // SPDX-License-Identifier: AGPL3.0-or-later +//nolint:stylecheck // allow non-camel-case in constants package message // GNUnet message types @@ -370,7 +371,7 @@ const ( NAMESTORE_MONITOR_START = 441 // Client to service: start monitoring (yields sequence of "ZONE_ITERATION_RESPONSES" --- forever). NAMESTORE_MONITOR_SYNC = 442 // Service to client: you're now in sync. NAMESTORE_RECORD_RESULT = 443 // Service to client: here is a (plaintext) record you requested. - NAMESTORE_MONITOR_NEXT = 444 // Client to service: I am now ready for the next (set of) monitor events. Monitoring equivlaent of #NAMESTORE_ZONE_ITERATION_NEXT. + NAMESTORE_MONITOR_NEXT = 444 // Client to service: I am now ready for the next (set of) monitor events. Monitoring equivalent of #NAMESTORE_ZONE_ITERATION_NEXT. NAMESTORE_ZONE_ITERATION_START = 445 // Client to service: please start iteration; receives "NAMESTORE_LOOKUP_NAME_RESPONSE" messages in return. NAMESTORE_ZONE_ITERATION_NEXT = 447 // Client to service: next record(s) in iteration please. NAMESTORE_ZONE_ITERATION_STOP = 448 // Client to service: stop iterating. @@ -445,7 +446,7 @@ const ( CONSENSUS_P2P_ELEMENTS_REQUEST = 544 // Elements, and requests for further elements CONSENSUS_P2P_ELEMENTS_REPORT = 545 // Elements that a peer reports to be missing at the remote peer. CONSENSUS_P2P_HELLO = 546 // Initialization message for consensus p2p communication. - CONSENSUS_P2P_SYNCED = 547 // Report that the peer is synced with the partner after successfuly decoding the invertible bloom filter. + CONSENSUS_P2P_SYNCED = 547 // Report that the peer is synced with the partner after successfully decoding the invertible bloom filter. CONSENSUS_P2P_FIN = 548 // Interaction os over, got synched and reported all elements CONSENSUS_P2P_ABORT = 548 // Abort a round, don't send requested elements anymore CONSENSUS_P2P_ROUND_CONTEXT = 547 // Abort a round, don't send requested elements anymore diff --git a/src/gnunet/service/client.go b/src/gnunet/service/client.go @@ -67,7 +67,6 @@ func RequestResponse( callee string, path string, req message.Message) (message.Message, error) { - // client-connect to the service logger.Printf(logger.DBG, "[%s] Connecting to %s service...\n", caller, callee) cl, err := NewClient(ctx, path) diff --git a/src/gnunet/service/connection.go b/src/gnunet/service/connection.go @@ -23,6 +23,7 @@ import ( "errors" "fmt" "gnunet/message" + "gnunet/util" "net" "os" "strconv" @@ -43,6 +44,7 @@ var ( // based on Unix domain sockets. It is used locally by services and // clients in the standard GNUnet environment. type Connection struct { + id int // connection identifier path string // file name of Unix socket conn net.Conn // associated connection buf []byte // read/write buffer @@ -53,6 +55,7 @@ type Connection struct { func NewConnection(ctx context.Context, path string) (s *Connection, err error) { var d net.Dialer s = new(Connection) + s.id = util.NextID() s.path = path s.buf = make([]byte, 65536) s.conn, err = d.DialContext(ctx, "unix", path) @@ -118,11 +121,11 @@ func (s *Connection) Receive(ctx context.Context) (message.Message, error) { return nil, err } // get rest of message - if err := get(4, int(mh.MsgSize)-4); err != nil { + if err = get(4, int(mh.MsgSize)-4); err != nil { return nil, err } - msg, err := message.NewEmptyMessage(mh.MsgType) - if err != nil { + var msg message.Message + if msg, err = message.NewEmptyMessage(mh.MsgType); err != nil { return nil, err } if msg == nil { @@ -134,6 +137,11 @@ func (s *Connection) Receive(ctx context.Context) (message.Message, error) { return msg, nil } +// Receiver returns the receiving client (string representation) +func (s *Connection) Receiver() string { + return fmt.Sprintf("uds:%d", s.id) +} + //---------------------------------------------------------------------- // internal methods //---------------------------------------------------------------------- @@ -212,7 +220,6 @@ func NewConnectionManager( params map[string]string, // connection parameters hdlr chan *Connection, // handler for incoming connections ) (cs *ConnectionManager, err error) { - // instantiate channel server cs = &ConnectionManager{ listener: nil, @@ -224,24 +231,21 @@ func NewConnectionManager( return } // handle additional parameters - if params != nil { - for key, value := range params { - switch key { - case "perm": // set permissions on 'unix' - if perm, err := strconv.ParseInt(value, 8, 32); err == nil { - if err := os.Chmod(path, os.FileMode(perm)); err != nil { - logger.Printf( - logger.ERROR, - "MsgChannelServer: Failed to set permissions %s on %s: %s\n", - path, value, err.Error()) - - } - } else { + for key, value := range params { + switch key { + case "perm": // set permissions on 'unix' + if perm, err := strconv.ParseInt(value, 8, 32); err == nil { + if err := os.Chmod(path, os.FileMode(perm)); err != nil { logger.Printf( logger.ERROR, - "MsgChannelServer: Invalid permissions '%s'\n", - value) + "MsgChannelServer: Failed to set permissions %s on %s: %s\n", + path, value, err.Error()) } + } else { + logger.Printf( + logger.ERROR, + "MsgChannelServer: Invalid permissions '%s'\n", + value) } } } diff --git a/src/gnunet/service/dht/blocks/filters.go b/src/gnunet/service/dht/blocks/filters.go @@ -0,0 +1,296 @@ +// This file is part of gnunet-go, a GNUnet-implementation in Golang. +// Copyright (C) 2019-2022 Bernd Fix >Y< +// +// gnunet-go 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. +// +// gnunet-go 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/>. +// +// SPDX-License-Identifier: AGPL3.0-or-later + +package blocks + +import ( + "bytes" + "crypto/sha512" + "encoding/binary" + "gnunet/util" + + "github.com/bfix/gospel/logger" +) + +//====================================================================== +// Peer filter +//====================================================================== + +// PeerFilter is a bloom filter without mutator +type PeerFilter struct { + BF *BloomFilter +} + +// NewPeerFilter creates an empty peer filter instance. +func NewPeerFilter() *PeerFilter { + return &PeerFilter{ + BF: NewBloomFilter(128), + } +} + +// Add peer id to the filter +func (pf *PeerFilter) Add(p *util.PeerID) { + pf.BF.Add(p.Data) +} + +// Contains returns true if the peer id is filtered (in the filter) +func (pf *PeerFilter) Contains(p *util.PeerID) bool { + return pf.BF.Contains(p.Data) +} + +// Cloone peer filter instance +func (pf *PeerFilter) Clone() *PeerFilter { + return &PeerFilter{ + BF: pf.BF.Clone(), + } +} + +//====================================================================== +// Result filter +//====================================================================== + +// ResultFilter return values +//nolint:stylecheck // allow non-camel-case in constants +const ( + RF_MORE = iota // Valid result, and there may be more. + RF_LAST // Last possible valid result. + RF_DUPLICATE // Valid result, but duplicate (was filtered by the result filter). + RF_IRRELEVANT // Block does not satisfy the constraints imposed by the XQuery. +) + +// Compare return values +//nolint:stylecheck // allow non-camel-case in constants +const ( + CMP_SAME = iota // the two result filter are the same + CMP_MERGE // the two result filter can be merged + CMP_DIFFER // the two result filter are different + CMP_1 // used as state by derived/complex compare functions + CMP_2 + CMP_3 +) + +//---------------------------------------------------------------------- + +// ResultFilter is used to indicate to other peers which results are not of +// interest when processing a GetMessage. Any peer which is processing +// GetMessages and has a result which matches the query key MUST check the +// result filter and only send a reply message if the result does not test +// positive under the result filter. Before forwarding the GetMessage, the +// result filter MUST be updated to filter out all results already returned +// by the local peer. +type ResultFilter interface { + + // Add entry to filter + Add(Block) + + // Contains returns true if entry is filtered + Contains(Block) bool + + // Bytes returns the binary representation of a result filter + Bytes() []byte + + // Compare two result filters + Compare(ResultFilter) int + + // Merge two result filters + Merge(ResultFilter) bool +} + +//---------------------------------------------------------------------- +// Dummy result filter +// [Additional filters (per block type) are defined in corresponding files] +//---------------------------------------------------------------------- + +// PassResultFilter is a dummy result filter with no state. +type PassResultFilter struct{} + +// Add a block to the result filter. +func (rf *PassResultFilter) Add(Block) { +} + +// Contains returns true if entry (binary representation) is filtered +func (rf *PassResultFilter) Contains(Block) bool { + return false +} + +// Bytes returns the binary representation of a result filter +func (rf *PassResultFilter) Bytes() (buf []byte) { + return +} + +// Merge two result filters +func (rf *PassResultFilter) Merge(ResultFilter) bool { + return true +} + +// Compare two result filters +func (rf *PassResultFilter) Compare(t ResultFilter) int { + if _, ok := t.(*PassResultFilter); ok { + return CMP_SAME + } + return CMP_DIFFER +} + +//====================================================================== +// Generic bllom filter with mutator +//====================================================================== + +// BloomFilter is a space-efficient probabilistic datastructure to test if +// an element is part of a set of elementsis defined as a string of bits +// always initially empty. An optional mutator can be used to additionally +// "randomize" the computation of the bloomfilter while remaining deterministic. +type BloomFilter struct { + Bits []byte // filter bits + + // transient attributes + mInput []byte // mutator input + mData []byte // mutator data +} + +// NewBloomFilter creates a new empty filter of given size (8*n bits). +func NewBloomFilter(n int) *BloomFilter { + return &BloomFilter{ + Bits: make([]byte, n), + mInput: nil, + mData: nil, + } +} + +// SetMutator to define a mutator for randomization. If 'm' is nil, +// the mutator is removed from the filter (use with care!) +func (bf *BloomFilter) SetMutator(m any) { + // handle mutator input + switch v := m.(type) { + case uint32: + buf := new(bytes.Buffer) + if err := binary.Write(buf, binary.BigEndian, v); err != nil { + logger.Printf(logger.ERROR, "[BloomFilter.SetMutator] failed: %s", err.Error()) + } + bf.mInput = buf.Bytes() + case []byte: + bf.mInput = make([]byte, 4) + util.CopyAlignedBlock(bf.mInput, v) + case nil: + bf.mInput = nil + bf.mData = nil + return + } + // generate mutator bytes + h := sha512.New() + if _, err := h.Write(bf.mInput); err != nil { + logger.Printf(logger.ERROR, "[BloomFilter.SetMutator] failed: %s", err.Error()) + } + bf.mData = h.Sum(nil) + + //logger.Printf(logger.DBG, "[filter] Mutator %s -> %s", hex.EncodeToString(bf.mInput), hex.EncodeToString(bf.mData)) +} + +// Mutator returns the mutator input as a 4-byte array +func (bf *BloomFilter) Mutator() []byte { + return bf.mInput +} + +// Bytes returns the binary representation of a bloom filter +func (bf *BloomFilter) Bytes() []byte { + var buf []byte + if bf.mInput != nil { + buf = append(buf, bf.mInput...) + } + buf = append(buf, bf.Bits...) + return buf +} + +// Compare two bloom filters +func (bf *BloomFilter) Compare(a *BloomFilter) int { + if len(bf.Bits) != len(a.Bits) || !bytes.Equal(bf.mInput, a.mInput) { + return CMP_DIFFER + } + if bytes.Equal(bf.Bits, a.Bits) { + return CMP_SAME + } + return CMP_MERGE +} + +// Merge two bloom filters +func (bf *BloomFilter) Merge(a *BloomFilter) bool { + if len(bf.Bits) != len(a.Bits) || !bytes.Equal(bf.mInput, a.mInput) { + return false + } + for i := range bf.Bits { + bf.Bits[i] |= a.Bits[i] + } + return true +} + +// Clone a bloom filter instance +func (bf *BloomFilter) Clone() *BloomFilter { + return &BloomFilter{ + Bits: util.Clone(bf.Bits), + mInput: util.Clone(bf.mInput), + mData: util.Clone(bf.mData), + } +} + +// Add entry (binary representation): +// When adding an element to the Bloom filter bf using BF-SET(bf,e), each +// integer n of the mapping M(e) is interpreted as a bit offset n mod L +// within bf and set to 1. +func (bf *BloomFilter) Add(e []byte) { + for _, idx := range bf.indices(e) { + bf.Bits[idx/8] |= (1 << (idx % 8)) + } +} + +// Contains returns true if the entry is most likely to be included: +// When testing if an element may be in the Bloom filter bf using +// BF-TEST(bf,e), each bit offset n mod L within bf MUST have been set to 1. +// Otherwise, the element is not considered to be in the Bloom filter. +func (bf *BloomFilter) Contains(e []byte) bool { + for _, idx := range bf.indices(e) { + if bf.Bits[idx/8]&(1<<(idx%8)) == 0 { + return false + } + } + return true +} + +// indices returns the list of bit indices for antry e: +// The element e is hashed using SHA-512. If a mutator is present, the +// hash values are XOR-ed. The resulting value is interpreted as a list +// of 16 32-bit integers in network byte order. +func (bf *BloomFilter) indices(e []byte) []uint32 { + // hash the entry + h := sha512.Sum512(e) + // apply mutator if available + if bf.mData != nil { + for i := range h { + h[i] ^= bf.mData[i] + } + } + // compute the indices for the entry + size := uint32(8 * len(bf.Bits)) + idx := make([]uint32, 16) + buf := bytes.NewReader(h[:]) + for i := range idx { + if err := binary.Read(buf, binary.BigEndian, &idx[i]); err != nil { + logger.Printf(logger.ERROR, "[BloomFilter.indices] failed: %s", err.Error()) + } + idx[i] %= size + } + return idx +} diff --git a/src/gnunet/service/dht/blocks/filters_test.go b/src/gnunet/service/dht/blocks/filters_test.go @@ -0,0 +1,110 @@ +// This file is part of gnunet-go, a GNUnet-implementation in Golang. +// Copyright (C) 2019-2022 Bernd Fix >Y< +// +// gnunet-go 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. +// +// gnunet-go 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/>. +// +// SPDX-License-Identifier: AGPL3.0-or-later + +package blocks + +import ( + "bytes" + "crypto/rand" + "sort" + "testing" +) + +type Entry []byte + +type EntryList []Entry + +func (list EntryList) Len() int { return len(list) } +func (list EntryList) Swap(i, j int) { list[i], list[j] = list[j], list[i] } +func (list EntryList) Less(i, j int) bool { return bytes.Compare(list[i], list[j]) < 0 } + +func (list EntryList) Contains(e Entry) bool { + size := len(list) + i := sort.Search(size, func(i int) bool { return bytes.Compare(list[i], e) >= 0 }) + return i != size +} + +func TestBloomfilter(t *testing.T) { + F := 500 // number of expected entries + + // The K-value for the HELLO_BF Bloom filter is always 16. The size S of + // the Bloom filter in bytes depends on the number of elements F known to + // be filtered at the initiator. If F is zero, the size S is just 8 (bytes). + // Otherwise, S is set to the minimum of 2^15 and the lowest power of 2 that + // is strictly larger than K*F/4 (in bytes). The wire format of HELLO_BF is + // the resulting byte array. In particular, K is never transmitted. + S := 1 + for S < 4*F && S < 32768 { + S <<= 1 + } + t.Logf("BloomFilter size in bytes: %d\n", S) + + // generate positives (entries in the set) + positives := make(EntryList, F) + for i := 0; i < F; i++ { + data := make(Entry, 32) + if _, err := rand.Read(data); err != nil { + t.Fatal(err) + } + positives[i] = data + } + sort.Sort(positives) + + // generate negatives (entries outside the set) + negatives := make(EntryList, F) + for i := 0; i < F; { + data := make(Entry, 32) + if _, err := rand.Read(data); err != nil { + t.Fatal(err) + } + if !positives.Contains(data) { + negatives[i] = data + i++ + } + } + + // create BloomFilter + bf := NewBloomFilter(S) + + // add positives to bloomfilter + for _, e := range positives { + bf.Add(e) + } + + // check lookup of positives + count := 0 + for _, e := range positives { + if !bf.Contains(e) { + count++ + } + } + if count > 0 { + t.Logf("FAILED with %d false-negatives", count) + } + + // check lookup of negatives + count = 0 + for _, e := range negatives { + if bf.Contains(e) { + count++ + } + } + if count > 0 { + t.Logf("FAILED with %d false-positives", count) + } +} diff --git a/src/gnunet/service/dht/blocks/generic.go b/src/gnunet/service/dht/blocks/generic.go @@ -19,11 +19,10 @@ package blocks import ( - "bytes" - "encoding/gob" "encoding/hex" "fmt" "gnunet/crypto" + "gnunet/enums" "gnunet/util" "github.com/bfix/gospel/data" @@ -39,13 +38,11 @@ type Query interface { // Key returns the DHT key for a block Key() *crypto.HashCode - // Get retrieves the value of a named query parameter. The value is - // unchanged if the key is not in the map or if the value in the map - // has an incompatible type. - Get(key string, value any) bool + // Type returns the requested block type + Type() uint16 - // Set stores the value of a named query parameter - Set(key string, value any) + // Flags returns the query flags + Flags() uint16 // Verify the integrity of a retrieved block (optional). Override in // custom query types to implement block-specific integrity checks @@ -77,7 +74,7 @@ type Block interface { // types to implement block-specific integrity checks (see GNSBlock for // example). This verification is usually weaker than the verification // method from a Query (see GNSBlock.Verify for explanation). - Verify() error + Verify() (bool, error) // String returns the human-readable representation of a block String() string @@ -97,8 +94,14 @@ type GenericQuery struct { // Key for repository queries (local/remote) key *crypto.HashCode - // query parameters (binary value representation) - params map[string][]byte + // block type requested + btype uint16 + + // query flags + flags uint16 + + // Params holds additional query parameters + Params util.ParameterSet } // Key interface method implementation @@ -106,23 +109,14 @@ func (q *GenericQuery) Key() *crypto.HashCode { return q.key } -// Get retrieves the value of a named query parameter -func (q *GenericQuery) Get(key string, value any) bool { - data, ok := q.params[key] - if !ok { - return false - } - dec := gob.NewDecoder(bytes.NewReader(data)) - return dec.Decode(value) != nil +// Type returns the requested block type +func (q *GenericQuery) Type() uint16 { + return q.btype } -// Set stores the value of a named query parameter -func (q *GenericQuery) Set(key string, value any) { - wrt := new(bytes.Buffer) - enc := gob.NewEncoder(wrt) - if enc.Encode(value) == nil { - q.params[key] = wrt.Bytes() - } +// Flags returns the query flags +func (q *GenericQuery) Flags() uint16 { + return q.flags } // Verify interface method implementation @@ -139,14 +133,16 @@ func (q *GenericQuery) Decrypt(b Block) error { // String returns the human-readable representation of a block func (q *GenericQuery) String() string { - return fmt.Sprintf("GenericQuery{key=%s}", hex.EncodeToString(q.Key().Bits)) + return fmt.Sprintf("GenericQuery{btype=%d,key=%s}", q.btype, hex.EncodeToString(q.Key().Bits)) } // NewGenericQuery creates a simple Query from hash code. -func NewGenericQuery(buf []byte) *GenericQuery { +func NewGenericQuery(key []byte, btype enums.BlockType, flags uint16) *GenericQuery { return &GenericQuery{ - key: crypto.NewHashCode(buf), - params: make(map[string][]byte), + key: crypto.NewHashCode(key), + btype: uint16(btype), + flags: flags, + Params: make(util.ParameterSet), } } @@ -181,16 +177,16 @@ func (b *GenericBlock) String() string { } // Verify interface method implementation -func (b *GenericBlock) Verify() error { +func (b *GenericBlock) Verify() (bool, error) { // no verification, no errors ;) - return nil + return true, nil } // NewGenericBlock creates a Block from binary data. func NewGenericBlock(buf []byte) *GenericBlock { return &GenericBlock{ block: util.Clone(buf), - btype: DHT_BLOCK_ANY, // unknown block type - expire: util.AbsoluteTimeNever(), // never expires + btype: uint16(enums.BLOCK_TYPE_ANY), // unknown block type + expire: util.AbsoluteTimeNever(), // never expires } } diff --git a/src/gnunet/service/dht/blocks/generic_test.go b/src/gnunet/service/dht/blocks/generic_test.go @@ -1,67 +0,0 @@ -// This file is part of gnunet-go, a GNUnet-implementation in Golang. -// Copyright (C) 2019-2022 Bernd Fix >Y< -// -// gnunet-go 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. -// -// gnunet-go 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/>. -// -// SPDX-License-Identifier: AGPL3.0-or-later - -package blocks - -import ( - "bytes" - "testing" -) - -// Test parameter handling for queries -func TestQueryParams(t *testing.T) { - q := NewGenericQuery(nil) - - // set parameters - var ( - btype uint16 = DHT_BLOCK_ANY - flags uint32 = 0 - name string = "Test" - data = make([]byte, 8) - ) - q.Set("btype", btype) - q.Set("flags", flags) - q.Set("name", name) - q.Set("data", data) - - // get parameters - var ( - t_btype uint16 - t_flags uint32 - t_name string - t_data []byte - ) - q.Get("btype", &t_btype) - q.Get("flags", &t_flags) - q.Get("name", &t_name) - q.Get("data", &t_data) - - // check for unchanged data - if btype != t_btype { - t.Fatal("btype mismatch") - } - if flags != t_flags { - t.Fatal("flags mismatch") - } - if name != t_name { - t.Fatal("name mismatch") - } - if !bytes.Equal(data, t_data) { - t.Fatal("data mismatch") - } -} diff --git a/src/gnunet/service/dht/blocks/gns.go b/src/gnunet/service/dht/blocks/gns.go @@ -22,14 +22,19 @@ import ( "errors" "fmt" "gnunet/crypto" + "gnunet/enums" "gnunet/util" "github.com/bfix/gospel/data" + "github.com/bfix/gospel/logger" ) // Error messages var ( - ErrBlockNotDecrypted = fmt.Errorf("GNS block not decrypted") + ErrBlockNotDecrypted = errors.New("GNS block not decrypted") + ErrBlockInvalidSig = errors.New("invalid signature key for GNS Block") + ErrBlockTypeNotVerified = errors.New("can't verify block type") + ErrBlockCantDecrypt = errors.New("can't decrypt block type") ) //---------------------------------------------------------------------- @@ -54,9 +59,13 @@ func (q *GNSQuery) Verify(b Block) (err error) { // verify derived key dkey := blk.DerivedKeySig.ZoneKey - dkey2, _ := q.Zone.Derive(q.Label, "gns") + var dkey2 *crypto.ZoneKey + if dkey2, _, err = q.Zone.Derive(q.Label, "gns"); err != nil { + return + } if !dkey.Equal(dkey2) { - return fmt.Errorf("invalid signature key for GNS Block") + err = ErrBlockInvalidSig + return } // verify signature var buf []byte @@ -66,7 +75,7 @@ func (q *GNSQuery) Verify(b Block) (err error) { blk.verified, err = blk.DerivedKeySig.Verify(buf) default: - err = errors.New("can't verify block type") + err = ErrBlockTypeNotVerified } return } @@ -81,7 +90,7 @@ func (q *GNSQuery) Decrypt(b Block) (err error) { return default: - err = errors.New("can't decrypt block type") + err = ErrBlockCantDecrypt } return } @@ -91,10 +100,13 @@ func NewGNSQuery(zkey *crypto.ZoneKey, label string) *GNSQuery { // derive a public key from (pkey,label) and set the repository // key as the SHA512 hash of the binary key representation. // (key blinding) - pd, _ := zkey.Derive(label, "gns") + pd, _, err := zkey.Derive(label, "gns") + if err != nil { + logger.Printf(logger.ERROR, "[NewGNSQuery] failed: %s", err.Error()) + } gq := crypto.Hash(pd.Bytes()).Bits return &GNSQuery{ - GenericQuery: *NewGenericQuery(gq), + GenericQuery: *NewGenericQuery(gq, enums.BLOCK_TYPE_GNS_NAMERECORD, 0), Zone: zkey, Label: label, derived: pd, @@ -161,12 +173,11 @@ func NewBlock() *GNSBlock { // Only the cryptographic signature is verified; the formal correctness of // the association between the block and a GNS label in a GNS zone can't // be verified. This is only possible in Query.Verify(). -func (b *GNSBlock) Verify() (err error) { +func (b *GNSBlock) Verify() (ok bool, err error) { // verify signature var buf []byte if buf, err = data.Marshal(b.Body); err != nil { return } - _, err = b.DerivedKeySig.Verify(buf) - return + return b.DerivedKeySig.Verify(buf) } diff --git a/src/gnunet/service/dht/blocks/handlers.go b/src/gnunet/service/dht/blocks/handlers.go @@ -0,0 +1,80 @@ +// This file is part of gnunet-go, a GNUnet-implementation in Golang. +// Copyright (C) 2019-2022 Bernd Fix >Y< +// +// gnunet-go 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. +// +// gnunet-go 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/>. +// +// SPDX-License-Identifier: AGPL3.0-or-later + +package blocks + +import ( + "gnunet/crypto" + "gnunet/enums" +) + +// BlockHandler interface defines methods specific to block types. +type BlockHandler interface { + + // Parse a block instance from binary data + ParseBlock(buf []byte) (Block, error) + + // ValidateBlockQuery is used to evaluate the request for a block as part of + // DHT-P2P-GET processing. Here, the block payload is unknown, but if possible + // the XQuery and Key SHOULD be verified. + ValidateBlockQuery(key *crypto.HashCode, xquery []byte) bool + + // ValidateBlockKey returns true if the block key is the same as the + // query key used to access the block. + ValidateBlockKey(b Block, key *crypto.HashCode) bool + + // ValidateBlockStoreRequest is used to evaluate a block payload as part of + // PutMessage and ResultMessage processing. + ValidateBlockStoreRequest(b Block) bool + + // SetupResultFilter is used to setup an empty result filter. The arguments + // are the set of results that must be filtered at the initiator, and a + // MUTATOR value which MAY be used to deterministically re-randomize + // probabilistic data structures. + SetupResultFilter(filterSize int, mutator uint32) ResultFilter + + // ParseResultFilter from binary data + ParseResultFilter(data []byte) ResultFilter + + // FilterResult is used to filter results against specific queries. This + // function does not check the validity of the block itself or that it + // matches the given key, as this must have been checked earlier. Thus, + // locally stored blocks from previously observed ResultMessages and + // PutMessages use this function to perform filtering based on the request + // parameters of a particular GET operation. Possible values for the + // FilterEvaluationResult are defined above. If the main evaluation result + // is RF_MORE, the function also returns and updated result filter where + // the block is added to the set of filtered replies. An implementation is + // not expected to actually differentiate between the RF_DUPLICATE and + // RF_IRRELEVANT return values: in both cases the block is ignored for + // this query. + FilterResult(b Block, key *crypto.HashCode, rf ResultFilter, xQuery []byte) int +} + +// BlockHandlers is a map of block query validation implementations +// for supported block types. +var BlockHandlers map[enums.BlockType]BlockHandler + +// initializer function +func init() { + // create map instance + BlockHandlers = make(map[enums.BlockType]BlockHandler) + + // add validation functions + BlockHandlers[enums.BLOCK_TYPE_DHT_URL_HELLO] = new(HelloBlockHandler) +} diff --git a/src/gnunet/service/dht/blocks/hello.go b/src/gnunet/service/dht/blocks/hello.go @@ -24,6 +24,7 @@ import ( "encoding/binary" "errors" "fmt" + "gnunet/crypto" "gnunet/enums" "gnunet/util" "net/url" @@ -32,6 +33,7 @@ import ( "github.com/bfix/gospel/crypto/ed25519" "github.com/bfix/gospel/data" + "github.com/bfix/gospel/logger" ) // HELLO-related errors @@ -50,12 +52,12 @@ const helloPrefix = "gnunet://hello/" // HelloBlock is the DHT-managed block type for HELLO information. // It is used to create and parse HELLO URLs. // All addresses expire at the same time /this different from HELLO -// messages (see message.HeeloMsg). +// messages (see message.HelloMsg). type HelloBlock struct { - PeerID *util.PeerID `` // peer identifier - Signature []byte `size:"64"` // signature - Expire util.AbsoluteTime `` // Expiration date - AddrBin []byte `size:"*"` // raw address data + PeerID *util.PeerID `` // peer identifier + Signature *util.PeerSignature `` // signature + Expires util.AbsoluteTime `` // Expiration date + AddrBin []byte `size:"*"` // raw address data // transient attributes addrs []*util.Address // cooked address data @@ -64,7 +66,9 @@ type HelloBlock struct { // SetAddresses adds a bulk of addresses for this HELLO block. func (h *HelloBlock) SetAddresses(a []*util.Address) { h.addrs = util.Clone(a) - h.finalize() + if err := h.finalize(); err != nil { + logger.Printf(logger.ERROR, "[HelloBlock.SetAddresses] failed: %s", err.Error()) + } } // Addresses returns the list of addresses @@ -101,9 +105,10 @@ func ParseHelloURL(u string, checkExpiry bool) (h *HelloBlock, err error) { h.PeerID = util.NewPeerID(buf) // (2) parse signature - if h.Signature, err = util.DecodeStringToBinary(p[1], 64); err != nil { + if buf, err = util.DecodeStringToBinary(p[1], 64); err != nil { return } + h.Signature = util.NewPeerSignature(buf) // (3) split last element into parts q := strings.SplitN(p[2], "?", 2) @@ -113,8 +118,8 @@ func ParseHelloURL(u string, checkExpiry bool) (h *HelloBlock, err error) { if exp, err = strconv.ParseUint(q[0], 10, 64); err != nil { return } - h.Expire = util.NewAbsoluteTimeEpoch(exp) - if checkExpiry && h.Expire.Expired() { + h.Expires = util.NewAbsoluteTimeEpoch(exp) + if checkExpiry && h.Expires.Expired() { err = ErrHelloExpired return } @@ -138,7 +143,9 @@ func ParseHelloURL(u string, checkExpiry bool) (h *HelloBlock, err error) { } // (6) generate raw address data so block is complete - h.finalize() + if err = h.finalize(); err != nil { + return + } // check signature var ok bool @@ -190,24 +197,39 @@ func (h *HelloBlock) finalize() (err error) { return } -/* -// Message returns the corresponding HELLO message to be sent to peers. -func (h *HelloBlock) Message() *message.HelloMsg { - msg := message.NewHelloMsg(h.PeerID) - for _, a := range h.addrs { - msg.AddAddress(message.NewHelloAddress(a, h.Expire)) +// Return the block type +func (h *HelloBlock) Type() uint16 { + return uint16(enums.BLOCK_TYPE_DHT_URL_HELLO) +} + +// Data returns the raw block data +func (h *HelloBlock) Data() []byte { + buf, err := data.Marshal(h) + if err != nil { + logger.Println(logger.ERROR, "[hello] Failed to serialize HELLO block: "+err.Error()) + buf = nil } - return msg + return buf +} + +// Expire returns the block expiration +func (h *HelloBlock) Expire() util.AbsoluteTime { + return h.Expires +} + +// String returns the human-readable representation of a block +func (h *HelloBlock) String() string { + return fmt.Sprintf("HelloBlock{peer=%s,expires=%s,addrs=[%d]}", + h.PeerID, h.Expires, len(h.Addresses())) } -*/ // URL returns the HELLO URL for the data. func (h *HelloBlock) URL() string { u := fmt.Sprintf("%s%s/%s/%d?", helloPrefix, h.PeerID.String(), - util.EncodeBinaryToString(h.Signature), - h.Expire.Epoch(), + util.EncodeBinaryToString(h.Signature.Data), + h.Expires.Epoch(), ) for i, a := range h.addrs { if i > 0 { @@ -221,10 +243,10 @@ func (h *HelloBlock) URL() string { } // Equals returns true if two HELLOs are the same. The expiration -// timestamp is ignored in the comparision. +// timestamp is ignored in the comparison. func (h *HelloBlock) Equals(g *HelloBlock) bool { if !h.PeerID.Equals(g.PeerID) || - !util.Equals(h.Signature, g.Signature) || + !util.Equals(h.Signature.Data, g.Signature.Data) || len(h.addrs) != len(g.addrs) { return false } @@ -239,29 +261,23 @@ func (h *HelloBlock) Equals(g *HelloBlock) bool { // Verify the integrity of the HELLO data func (h *HelloBlock) Verify() (bool, error) { // assemble signed data and public key - sd := h.signedData() - pub := h.PeerID.PublicKey() - sig, err := ed25519.NewEdSignatureFromBytes(h.Signature) + sd := h.SignedData() + pub := ed25519.NewPublicKeyFromBytes(h.PeerID.Data) + sig, err := ed25519.NewEdSignatureFromBytes(h.Signature.Data) if err != nil { return false, err } return pub.EdVerify(sd, sig) } -// Sign the HELLO data with private key -func (h *HelloBlock) Sign(prv *ed25519.PrivateKey) error { - // assemble signed data - sd := h.signedData() - sig, err := prv.EdSign(sd) - if err != nil { - return err - } - h.Signature = sig.Bytes() +// SetSignature stores a signature in the the HELLO block +func (h *HelloBlock) SetSignature(sig *util.PeerSignature) error { + h.Signature = sig return nil } -// signedData assembles a data block for sign and verify operations. -func (h *HelloBlock) signedData() []byte { +// SignedData assembles a data block for sign and verify operations. +func (h *HelloBlock) SignedData() []byte { // hash address block hAddr := sha512.Sum512(h.AddrBin) var size uint32 = 80 @@ -269,9 +285,166 @@ func (h *HelloBlock) signedData() []byte { // assemble signed data buf := new(bytes.Buffer) - binary.Write(buf, binary.BigEndian, size) - binary.Write(buf, binary.BigEndian, purpose) - binary.Write(buf, binary.BigEndian, h.Expire.Epoch()*1000000) - buf.Write(hAddr[:]) + var n int + err := binary.Write(buf, binary.BigEndian, size) + if err == nil { + if err = binary.Write(buf, binary.BigEndian, purpose); err == nil { + if err = binary.Write(buf, binary.BigEndian, h.Expires.Epoch()*1000000); err == nil { + if n, err = buf.Write(hAddr[:]); err == nil { + if n != len(hAddr[:]) { + err = errors.New("signed data size mismatch") + } + } + } + } + } + if err != nil { + logger.Printf(logger.ERROR, "[HelloBlock.SignedData] failed: %s", err.Error()) + } return buf.Bytes() } + +//---------------------------------------------------------------------- +// HELLO block handler +//---------------------------------------------------------------------- + +// HelloBlockHandler methods related to HELLO blocks +type HelloBlockHandler struct{} + +// Parse a block instance from binary data +func (bh *HelloBlockHandler) ParseBlock(buf []byte) (Block, error) { + return ParseHelloFromBytes(buf) +} + +// ValidateHelloBlockQuery validates query parameters for a +// DHT-GET request for HELLO blocks. +func (bh *HelloBlockHandler) ValidateBlockQuery(key *crypto.HashCode, xquery []byte) bool { + // no xquery parameters allowed. + return len(xquery) == 0 +} + +// ValidateBlockKey returns true if the block key is the same as the +// query key used to access the block. +func (bh *HelloBlockHandler) ValidateBlockKey(b Block, key *crypto.HashCode) bool { + hb, ok := b.(*HelloBlock) + if !ok { + return false + } + // key must be the hash of the peer id + bkey := crypto.Hash(hb.PeerID.Bytes()) + return key.Equals(bkey) +} + +// ValidateBlockStoreRequest is used to evaluate a block payload as part of +// PutMessage and ResultMessage processing. +func (bh *HelloBlockHandler) ValidateBlockStoreRequest(b Block) bool { + // TODO: verify block payload + return true +} + +// SetupResultFilter is used to setup an empty result filter. The arguments +// are the set of results that must be filtered at the initiator, and a +// MUTATOR value which MAY be used to deterministically re-randomize +// probabilistic data structures. +func (bh *HelloBlockHandler) SetupResultFilter(filterSize int, mutator uint32) ResultFilter { + return NewHelloResultFilter(filterSize, mutator) +} + +// ParseResultFilter from binary data +func (bh *HelloBlockHandler) ParseResultFilter(data []byte) ResultFilter { + return NewHelloResultFilterFromBytes(data) +} + +// FilterResult is used to filter results against specific queries. This +// function does not check the validity of the block itself or that it +// matches the given key, as this must have been checked earlier. Thus, +// locally stored blocks from previously observed ResultMessages and +// PutMessages use this function to perform filtering based on the request +// parameters of a particular GET operation. Possible values for the +// FilterEvaluationResult are defined above. If the main evaluation result +// is RF_MORE, the function also returns and updated result filter where +// the block is added to the set of filtered replies. An implementation is +// not expected to actually differentiate between the RF_DUPLICATE and +// RF_IRRELEVANT return values: in both cases the block is ignored for +// this query. +func (bh *HelloBlockHandler) FilterResult(b Block, key *crypto.HashCode, rf ResultFilter, xQuery []byte) int { + if rf.Contains(b) { + return RF_DUPLICATE + } + rf.Add(b) + return RF_LAST +} + +//---------------------------------------------------------------------- + +// HelloResultFilter is a result filter implementation for HELLO blocks +type HelloResultFilter struct { + bf *BloomFilter +} + +// NewHelloResultFilter initializes an empty resut filter +func NewHelloResultFilter(filterSize int, mutator uint32) *HelloResultFilter { + // HELLO result filters are BloomFilters with a mutator + rf := new(HelloResultFilter) + rf.bf = NewBloomFilter(filterSize) + rf.bf.SetMutator(mutator) + return rf +} + +// NewHelloResultFilterFromBytes creates a new result filter from a binary +// representation: 'data' is the concatenaion 'mutator|bloomfilter'. +// If 'withMutator' is false, no mutator is used. +func NewHelloResultFilterFromBytes(data []byte) *HelloResultFilter { + //logger.Printf(logger.DBG, "[filter] FromBytes = %d:%s (mutator: %v)",len(data), hex.EncodeToString(data), withMutator) + + // handle mutator input + mSize := 4 + rf := new(HelloResultFilter) + rf.bf = &BloomFilter{ + Bits: util.Clone(data[mSize:]), + } + if mSize > 0 { + rf.bf.SetMutator(data[:mSize]) + } + return rf +} + +// Add a HELLO block to th result filter +func (rf *HelloResultFilter) Add(b Block) { + if hb, ok := b.(*HelloBlock); ok { + hAddr := sha512.Sum512(hb.AddrBin) + rf.bf.Add(hAddr[:]) + } +} + +// Contains checks if a block is contained in the result filter +func (rf *HelloResultFilter) Contains(b Block) bool { + if hb, ok := b.(*HelloBlock); ok { + hAddr := sha512.Sum512(hb.AddrBin) + return rf.bf.Contains(hAddr[:]) + } + return false +} + +// Bytes returns a binary representation of a HELLO result filter +func (rf *HelloResultFilter) Bytes() []byte { + return rf.bf.Bytes() +} + +// Compare two HELLO result filters +func (rf *HelloResultFilter) Compare(t ResultFilter) int { + trf, ok := t.(*HelloResultFilter) + if !ok { + return CMP_DIFFER + } + return rf.bf.Compare(trf.bf) +} + +// Merge two HELLO result filters +func (rf *HelloResultFilter) Merge(t ResultFilter) bool { + trf, ok := t.(*HelloResultFilter) + if !ok { + return false + } + return rf.bf.Merge(trf.bf) +} diff --git a/src/gnunet/service/dht/blocks/types.go b/src/gnunet/service/dht/blocks/types.go @@ -1,26 +0,0 @@ -// This file is part of gnunet-go, a GNUnet-implementation in Golang. -// Copyright (C) 2019-2022 Bernd Fix >Y< -// -// gnunet-go 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. -// -// gnunet-go 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/>. -// -// SPDX-License-Identifier: AGPL3.0-or-later - -package blocks - -// DHT Block types -const ( - DHT_BLOCK_ANY = 0 - DHT_BLOCK_HELLO = 7 // Type of a block that contains a HELLO for a peer - DHT_BLOCK_GNS = 11 // Block for storing record data -) diff --git a/src/gnunet/service/dht/bloomfilter.go b/src/gnunet/service/dht/bloomfilter.go @@ -1,123 +0,0 @@ -// This file is part of gnunet-go, a GNUnet-implementation in Golang. -// Copyright (C) 2019-2022 Bernd Fix >Y< -// -// gnunet-go 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. -// -// gnunet-go 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/>. -// -// SPDX-License-Identifier: AGPL3.0-or-later - -package dht - -import ( - "bytes" - "crypto/sha512" - "encoding/binary" -) - -//====================================================================== -// Generic BloomFilter -//====================================================================== - -// BloomFilter parameter -var ( - bfNumBits = 128 - bfHash = sha512.New -) - -// BloomFilter is a space-efficient probabilistic datastructure to test if -// an element is part of a set of elementsis defined as a string of bits -// always initially empty. -type BloomFilter struct { - data []byte // filter bits - salt []byte // salt for hashing -} - -// NewBloomFilter cretes a new filter using the specified salt. An unused -// salt is set to nil. -func NewBloomFilter(salt []byte) *BloomFilter { - return &BloomFilter{ - data: make([]byte, (bfNumBits+7)/8), - salt: salt, - } -} - -// Add entry (binary representation): -// When adding an element to the Bloom filter bf using BF-SET(bf,e), each -// integer n of the mapping M(e) is interpreted as a bit offset n mod L -// within bf and set to 1. -func (bf *BloomFilter) Add(e []byte) { - for _, idx := range bf.indices(e) { - bf.data[idx/8] |= (1 << (idx % 7)) - } -} - -// Contains returns true if the entry is most likely to be included: -// When testing if an element may be in the Bloom filter bf using -// BF-TEST(bf,e), each bit offset n mod L within bf MUST have been set to 1. -// Otherwise, the element is not considered to be in the Bloom filter. -func (bf *BloomFilter) Contains(e []byte) bool { - for _, idx := range bf.indices(e) { - if bf.data[idx/8]&(1<<(idx%7)) == 0 { - return false - } - } - return true -} - -// indices returns the list of bit indices for antry e: -// The element e is prepended with a salt (pütional) and hashed using SHA-512. -// The resulting byte string is interpreted as a list of 16 32-bit integers -// in network byte order. -func (bf *BloomFilter) indices(e []byte) []int { - // hash the entry (with optional salt prepended) - hsh := bfHash() - if bf.salt != nil { - hsh.Write(bf.salt) - } - hsh.Write(e) - h := hsh.Sum(nil) - - // compute the indices for the entry - idx := make([]int, len(h)/2) - buf := bytes.NewReader(h) - for i := range idx { - binary.Read(buf, binary.BigEndian, &idx[i]) - } - return idx -} - -//====================================================================== -// BloomFilter for peer addresses -//====================================================================== - -// PeerBloomFilter implements specific Add/Contains functions. -type PeerBloomFilter struct { - BloomFilter -} - -// NewPeerBloomFilter creates a new filter for peer addresses. -func NewPeerBloomFilter() *PeerBloomFilter { - return &PeerBloomFilter{ - BloomFilter: *NewBloomFilter(nil), - } -} - -// Add peer address to the filter. -func (bf *PeerBloomFilter) Add(p *PeerAddress) { - bf.BloomFilter.Add(p.addr[:]) -} - -// Contains returns true if the peer address is most likely to be included. -func (bf *PeerBloomFilter) Contains(p *PeerAddress) bool { - return bf.BloomFilter.Contains(p.addr[:]) -} diff --git a/src/gnunet/service/dht/dhtstore_test.go b/src/gnunet/service/dht/dhtstore_test.go @@ -1,94 +0,0 @@ -// This file is part of gnunet-go, a GNUnet-implementation in Golang. -// Copyright (C) 2019-2022 Bernd Fix >Y< -// -// gnunet-go 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. -// -// gnunet-go 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/>. -// -// SPDX-License-Identifier: AGPL3.0-or-later - -package dht - -import ( - "encoding/hex" - "gnunet/config" - "gnunet/crypto" - "gnunet/service" - "gnunet/service/dht/blocks" - "math/rand" - "testing" -) - -// test constants -const ( - fsNumBlocks = 5 -) - -// TestDHTFileStore generates 'fsNumBlocks' fully-random blocks -// and stores them under their SHA512 key. It than retrieves -// each block from storage and checks for matching hash. -func TestDHTFilesStore(t *testing.T) { - - // test configuration - cfg := make(config.ParameterConfig) - cfg["mode"] = "file" - cfg["cache"] = false - cfg["path"] = "/var/lib/gnunet/dht/store" - cfg["maxGB"] = 10 - - // create file store - fs, err := service.NewFileStore(cfg) - if err != nil { - t.Fatal(err) - } - // allocate keys - keys := make([]blocks.Query, 0, fsNumBlocks) - - // First round: save blocks - for i := 0; i < fsNumBlocks; i++ { - // generate random block - size := 1024 + rand.Intn(62000) - buf := make([]byte, size) - rand.Read(buf) - val := blocks.NewGenericBlock(buf) - // generate associated key - k := crypto.Hash(buf).Bits - key := blocks.NewGenericQuery(k) - - // store block - if err := fs.Put(key, val); err != nil { - t.Fatal(err) - } - // remember key - keys = append(keys, key) - } - - // Second round: retrieve blocks and check - for _, key := range keys { - // get block - val, err := fs.Get(key) - if err != nil { - t.Fatal(err) - } - buf := val.Data() - - // re-create key - k := crypto.Hash(buf) - - // do the keys match? - if !k.Equals(key.Key()) { - t.Log(hex.EncodeToString(k.Bits)) - t.Log(hex.EncodeToString(key.Key().Bits)) - t.Fatal("key/value mismatch") - } - } -} diff --git a/src/gnunet/service/dht/messages.go b/src/gnunet/service/dht/messages.go @@ -0,0 +1,380 @@ +// This file is part of gnunet-go, a GNUnet-implementation in Golang. +// Copyright (C) 2019-2022 Bernd Fix >Y< +// +// gnunet-go 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. +// +// gnunet-go 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/>. +// +// SPDX-License-Identifier: AGPL3.0-or-later + +package dht + +import ( + "context" + "gnunet/enums" + "gnunet/message" + "gnunet/service/dht/blocks" + "gnunet/transport" + "gnunet/util" + + "github.com/bfix/gospel/logger" + "github.com/bfix/gospel/math" +) + +//---------------------------------------------------------------------- +// Handle DHT messages from the network +//---------------------------------------------------------------------- + +// HandleMessage handles a DHT request/response message. Responses are sent +// to the specified responder. +func (m *Module) HandleMessage(ctx context.Context, sender *util.PeerID, msgIn message.Message, back transport.Responder) bool { + // assemble log label + label := "dht" + if v := ctx.Value("label"); v != nil { + if s, _ := v.(string); len(s) > 0 { + label = "dht-" + s + } + } + logger.Printf(logger.INFO, "[%s] message received from %s", label, sender) + + // process message + switch msg := msgIn.(type) { + + case *message.DHTP2PGetMsg: + //-------------------------------------------------------------- + // DHT-P2P GET + //-------------------------------------------------------------- + logger.Printf(logger.INFO, "[%s] Handling DHT-P2P-GET message", label) + query := blocks.NewGenericQuery(msg.Query.Bits, enums.BlockType(msg.BType), msg.Flags) + + var block blocks.Block + var dist *math.Int + var err error + + //-------------------------------------------------------------- + // validate query (based on block type requested) (9.4.3.1) + btype := enums.BlockType(msg.BType) + blockHdlr, ok := blocks.BlockHandlers[btype] + if ok { + // validate block query + if !blockHdlr.ValidateBlockQuery(msg.Query, msg.XQuery) { + logger.Printf(logger.WARN, "[%s] DHT-P2P-GET invalid query -- discarded", label) + return false + } + } else { + logger.Printf(logger.INFO, "[%s] no handler defined for block type %s", label, btype.String()) + blockHdlr = nil + } + //---------------------------------------------------------- + // check if sender is in peer filter (9.4.3.2) + if !msg.PeerFilter.Contains(sender) { + logger.Printf(logger.WARN, "[%s] sender not in peer filter", label) + } + // parse result filter + var rf blocks.ResultFilter = new(blocks.PassResultFilter) + if msg.ResFilter != nil && len(msg.ResFilter) > 0 { + if blockHdlr != nil { + rf = blockHdlr.ParseResultFilter(msg.ResFilter) + } else { + logger.Printf(logger.WARN, "[%s] unknown result filter implementation -- skipped", label) + } + } + // clone peer filter + pf := msg.PeerFilter.Clone() + + //---------------------------------------------------------- + // check if we need to respond (and how) (9.4.3.3) + addr := NewQueryAddress(msg.Query) + closest := m.rtable.IsClosestPeer(nil, addr, msg.PeerFilter) + demux := int(msg.Flags)&enums.DHT_RO_DEMULTIPLEX_EVERYWHERE != 0 + approx := int(msg.Flags)&enums.DHT_RO_FIND_APPROXIMATE != 0 + // actions + doResult := closest || (demux && approx) + doForward := !closest || (demux && !approx) + logger.Printf(logger.DBG, "[dht] GET message: closest=%v, demux=%v, approx=%v --> result=%v, forward=%v", + closest, demux, approx, doResult, doForward) + + //------------------------------------------------------ + // query for a HELLO? (9.4.3.3a) + if msg.BType == uint32(enums.BLOCK_TYPE_DHT_URL_HELLO) { + logger.Println(logger.DBG, "[dht] GET message for HELLO: check cache") + // find best cached HELLO + block, dist = m.rtable.BestHello(addr, rf) + } + //-------------------------------------------------------------- + // find the closest block that has that is not filtered/ by the result + // filter (in case we did not find an appropriate block in cache). + if doResult { + // save best-match values from cache + blockCache := block + distCache := dist + + // query DHT store for exact match (9.4.3.3c) + if block, err = m.Get(ctx, query); err != nil { + logger.Printf(logger.ERROR, "[%s] Failed to get DHT block from storage: %s", label, err.Error()) + return true + } + // if block is filtered, skip it + if rf.Contains(block) { + logger.Println(logger.DBG, "[dht] GET message for HELLO: matching DHT block is filtered") + block = nil + } + // if we have no exact match, find approximate block if requested + if block == nil || approx { + // no exact match: find approximate (9.4.3.3b) + match := func(b blocks.Block) bool { + return rf.Contains(b) + } + block, dist, err = m.GetApprox(ctx, query, match) + if err != nil { + logger.Printf(logger.ERROR, "[%s] Failed to get (approx.) DHT block from storage: %s", label, err.Error()) + return true + } + } + // if we have a block from cache, check if it is better than the + // block found in the DHT + if blockCache != nil && distCache.Cmp(dist) < 0 { + block = blockCache + } + // if we have a block, send it as response + if block != nil { + logger.Printf(logger.INFO, "[%s] sending DHT result message to caller", label) + if err := m.sendResult(ctx, query, block, back); err != nil { + logger.Printf(logger.ERROR, "[%s] Failed to send DHT result message: %s", label, err.Error()) + } + } + } + // check if we need to forward message based on filter result + if block != nil && blockHdlr != nil { + switch blockHdlr.FilterResult(block, query.Key(), rf, msg.XQuery) { + case blocks.RF_LAST: + // no need for further results + case blocks.RF_MORE: + // possibly more results + doForward = true + case blocks.RF_DUPLICATE, blocks.RF_IRRELEVANT: + // do not forward + } + } + if doForward { + // build updated GET message + pf.Add(m.core.PeerID()) + msgOut := msg.Update(pf, rf, msg.HopCount+1) + + // forward to number of peers + numForward := m.rtable.ComputeOutDegree(msg.ReplLevel, msg.HopCount) + key := NewQueryAddress(query.Key()) + for n := 0; n < numForward; n++ { + if p := m.rtable.SelectClosestPeer(key, pf); p != nil { + // forward message to peer + logger.Printf(logger.INFO, "[%s] forward DHT get message to %s", label, p.String()) + if err := back.Send(ctx, msgOut); err != nil { + logger.Printf(logger.ERROR, "[%s] Failed to forward DHT get message: %s", label, err.Error()) + } + pf.Add(p.Peer) + // create open get-forward result handler + rh := NewForwardResultHandler(msg, rf, back) + logger.Printf(logger.INFO, "[%s] DHT-P2P-GET task #%d started", label, rh.ID()) + m.reshdlrs.Add(rh) + } else { + break + } + } + } + logger.Printf(logger.INFO, "[%s] Handling DHT-P2P-GET message done", label) + + case *message.DHTP2PPutMsg: + //---------------------------------------------------------- + // DHT-P2P PUT + //---------------------------------------------------------- + logger.Printf(logger.INFO, "[%s] Handling DHT-P2P-PUT message", label) + + //-------------------------------------------------------------- + // check if request is expired (9.3.2.1) + if msg.Expiration.Expired() { + logger.Printf(logger.WARN, "[%s] DHT-P2P-PUT message expired (%s)", label, msg.Expiration.String()) + return false + } + btype := enums.BlockType(msg.BType) + blockHdlr, ok := blocks.BlockHandlers[btype] + if ok { // (9.3.2.2) + // reconstruct block instance + if block, err := blockHdlr.ParseBlock(msg.Block); err == nil { + + // validate block key (9.3.2.3) + if !blockHdlr.ValidateBlockKey(block, msg.Key) { + logger.Printf(logger.WARN, "[%s] DHT-P2P-PUT invalid key -- discarded", label) + return false + } + + // validate block payload (9.3.2.4) + if !blockHdlr.ValidateBlockStoreRequest(block) { + logger.Printf(logger.WARN, "[%s] DHT-P2P-PUT invalid payload -- discarded", label) + return false + } + } + } else { + logger.Printf(logger.INFO, "[%s] No validator defined for block type %s", label, btype.String()) + blockHdlr = nil + } + //-------------------------------------------------------------- + // check if sender is in peer filter (9.3.2.5) + if !msg.PeerFilter.Contains(sender) { + logger.Printf(logger.WARN, "[%s] Sender not in peer filter", label) + } + //-------------------------------------------------------------- + // check if route is recorded (9.3.2.6) + /* + withPath := msg.Flags&enums.DHT_RO_RECORD_ROUTE != 0 + if withPath { + spe := message.NewPathElement(msg.Key, sender, p) + m.core.Sign(spe) + msg.AppendPutPath(spe) + } + */ + //-------------------------------------------------------------- + // verify PUT path (9.3.2.7) + if msg.PathL > 0 { + + } + + logger.Printf(logger.INFO, "[%s] Handling DHT-P2P-PUT message done", label) + + case *message.DHTP2PResultMsg: + //---------------------------------------------------------- + // DHT-P2P RESULT + //---------------------------------------------------------- + logger.Printf(logger.INFO, "[%s] Handling DHT-P2P-RESULT message", label) + + // check task list for handler + key := msg.Query.String() + handled := false + if list, ok := m.reshdlrs.Get(key); ok { + for _, rh := range list { + logger.Printf(logger.DBG, "[%s] Task #%d for DHT-P2P-RESULT found", label, rh.ID()) + // handle the message + go rh.Handle(ctx, msg) + } + return true + } + if !handled { + logger.Printf(logger.WARN, "[%s] DHT-P2P-RESULT not processed (no handler)", label) + } + return handled + + case *message.DHTP2PHelloMsg: + //---------------------------------------------------------- + // DHT-P2P HELLO + //---------------------------------------------------------- + logger.Printf(logger.INFO, "[%s] Handling DHT-P2P-HELLO message", label) + + // verify integrity of message + if ok, err := msg.Verify(sender); !ok || err != nil { + logger.Println(logger.WARN, "[dht] Received invalid DHT_P2P_HELLO message") + if err != nil { + logger.Println(logger.ERROR, "[dht] --> "+err.Error()) + } + return false + } + // keep peer addresses in core for transport + aList, err := msg.Addresses() + if err != nil { + logger.Println(logger.ERROR, "[dht] Failed to parse addresses from DHT_P2P_HELLO message") + return false + } + if newPeer := m.core.Learn(ctx, sender, aList); newPeer { + // we added a previously unknown peer: send a HELLO + var msgOut *message.DHTP2PHelloMsg + if msgOut, err = m.getHello(); err != nil { + return false + } + logger.Printf(logger.INFO, "[dht] Sending HELLO to %s: %s", sender, msgOut) + err = m.core.Send(ctx, sender, msgOut) + // no error if the message might have been sent + if err == transport.ErrEndpMaybeSent { + err = nil + } + } + + // cache HELLO block if applicable + k := sender.String() + isNew := true + if hb, ok := m.rtable.GetHello(k); ok { + // cache entry exists: is the HELLO message more recent? + _, isNew = hb.Expires.Diff(msg.Expires) + } + // we need to cache a new(er) HELLO + if isNew { + m.rtable.CacheHello(&blocks.HelloBlock{ + PeerID: sender, + Signature: msg.Signature, + Expires: msg.Expires, + AddrBin: util.Clone(msg.AddrList), + }) + } + + //-------------------------------------------------------------- + // Legacy message types (not implemented) + //-------------------------------------------------------------- + + case *message.DHTClientPutMsg: + //---------------------------------------------------------- + // DHT PUT + //---------------------------------------------------------- + logger.Printf(logger.INFO, "[%s] Handling DHTClientPut message", label) + + case *message.DHTClientGetMsg: + //---------------------------------------------------------- + // DHT GET + //---------------------------------------------------------- + logger.Printf(logger.INFO, "[%s] Handling DHTClientGet message", label) + + case *message.DHTClientGetResultsKnownMsg: + //---------------------------------------------------------- + // DHT GET-RESULTS-KNOWN + //---------------------------------------------------------- + logger.Printf(logger.INFO, "[%s] Handling DHTClientGetResultsKnown message", label) + + case *message.DHTClientGetStopMsg: + //---------------------------------------------------------- + // DHT GET-STOP + //---------------------------------------------------------- + logger.Printf(logger.INFO, "[%s] Handling DHTClientGetStop message", label) + + case *message.DHTClientResultMsg: + //---------------------------------------------------------- + // DHT RESULT + //---------------------------------------------------------- + logger.Printf(logger.INFO, "[%s] Handling DHTClientResult message", label) + + default: + //---------------------------------------------------------- + // UNKNOWN message type received + //---------------------------------------------------------- + logger.Printf(logger.ERROR, "[%s] Unhandled message of type (%d)\n", label, msgIn.Header().MsgType) + return false + } + return true +} + +// send a result back to caller +func (m *Module) sendResult(ctx context.Context, query blocks.Query, blk blocks.Block, back transport.Responder) error { + // assemble result message + out := message.NewDHTP2PResultMsg() + out.BType = uint32(query.Type()) + out.Expires = blk.Expire() + out.Query = query.Key() + out.Block = blk.Data() + out.MsgSize += uint16(len(out.Block)) + // send message + return back.Send(ctx, out) +} diff --git a/src/gnunet/service/dht/module.go b/src/gnunet/service/dht/module.go @@ -20,14 +20,20 @@ package dht import ( "context" + "errors" "gnunet/config" "gnunet/core" "gnunet/message" "gnunet/service" "gnunet/service/dht/blocks" + "gnunet/service/store" + "gnunet/transport" + "gnunet/util" + gmath "math" "time" "github.com/bfix/gospel/logger" + "github.com/bfix/gospel/math" ) //====================================================================== @@ -42,34 +48,36 @@ import ( type Module struct { service.ModuleImpl - store service.DHTStore // reference to the block storage mechanism - cache service.DHTStore // transient block cache - core *core.Core // reference to core services + store store.DHTStore // reference to the block storage mechanism + core *core.Core // reference to core services - rtable *RoutingTable // routing table + rtable *RoutingTable // routing table + lastHello *message.DHTP2PHelloMsg // last own HELLO message used; re-create if expired + reshdlrs *ResultHandlerList // list of open tasks } // NewModule returns a new module instance. It initializes the storage // mechanism for persistence. -func NewModule(ctx context.Context, c *core.Core) (m *Module, err error) { +func NewModule(ctx context.Context, c *core.Core, cfg *config.DHTConfig) (m *Module, err error) { // create permanent storage handler - var store, cache service.DHTStore - if store, err = service.NewDHTStore(config.Cfg.DHT.Storage); err != nil { + var storage store.DHTStore + if storage, err = store.NewDHTStore(cfg.Storage); err != nil { return } // create routing table - rt := NewRoutingTable(NewPeerAddress(c.PeerID())) + rt := NewRoutingTable(NewPeerAddress(c.PeerID()), cfg.Routing) // return module instance m = &Module{ ModuleImpl: *service.NewModuleImpl(), - store: store, - cache: cache, + store: storage, core: c, rtable: rt, + reshdlrs: NewResultHandlerList(), } // register as listener for core events - listener := m.Run(ctx, m.event, m.Filter(), 15*time.Minute, m.heartbeat) + pulse := time.Duration(cfg.Heartbeat) * time.Second + listener := m.Run(ctx, m.event, m.Filter(), pulse, m.heartbeat) c.Register("dht", listener) return } @@ -78,26 +86,20 @@ func NewModule(ctx context.Context, c *core.Core) (m *Module, err error) { // Get a block from the DHT ["dht:get"] func (m *Module) Get(ctx context.Context, query blocks.Query) (block blocks.Block, err error) { + return m.store.Get(query) +} - // check if we have the requested block in cache or permanent storage. - block, err = m.cache.Get(query) - if err == nil { - // yes: we are done - return - } - block, err = m.store.Get(query) - if err == nil { - // yes: we are done - return - } - // retrieve the block from the DHT - - return nil, nil +// GetApprox returns the first block not excluded ["dht:getapprox"] +func (m *Module) GetApprox(ctx context.Context, query blocks.Query, excl func(blocks.Block) bool) (block blocks.Block, dist *math.Int, err error) { + var d any + block, d, err = m.store.GetApprox(query, excl) + dist, _ = d.(*math.Int) + return } // Put a block into the DHT ["dht:put"] func (m *Module) Put(ctx context.Context, key blocks.Query, block blocks.Block) error { - return nil + return m.store.Put(key, block) } //---------------------------------------------------------------------- @@ -110,17 +112,17 @@ func (m *Module) Filter() *core.EventFilter { f.AddEvent(core.EV_DISCONNECT) // messages we are interested in: - // (1) DHT messages + // (1) DHT_P2P messages + f.AddMsgType(message.DHT_P2P_PUT) + f.AddMsgType(message.DHT_P2P_GET) + f.AddMsgType(message.DHT_P2P_RESULT) + f.AddMsgType(message.DHT_P2P_HELLO) + // (2) DHT messages (legacy, not implemented) f.AddMsgType(message.DHT_CLIENT_GET) f.AddMsgType(message.DHT_CLIENT_GET_RESULTS_KNOWN) f.AddMsgType(message.DHT_CLIENT_GET_STOP) f.AddMsgType(message.DHT_CLIENT_PUT) f.AddMsgType(message.DHT_CLIENT_RESULT) - // (2) DHT_P2P messages - f.AddMsgType(message.DHT_P2P_PUT) - f.AddMsgType(message.DHT_P2P_GET) - f.AddMsgType(message.DHT_P2P_RESULT) - f.AddMsgType(message.DHT_P2P_HELLO) return f } @@ -143,32 +145,105 @@ func (m *Module) event(ctx context.Context, ev *core.Event) { // Message received. case core.EV_MESSAGE: logger.Printf(logger.INFO, "[dht] Message received: %s", ev.Msg.String()) - // process message (if applicable) - if m.ProcessFcn != nil { - m.ProcessFcn(ctx, ev.Msg, ev.Resp) + + // check if peer is in routing table (connected peer) + if !m.rtable.Contains(NewPeerAddress(ev.Peer)) { + logger.Printf(logger.WARN, "[dht] message %d from unregistered peer -- discarded", ev.Msg.Header().MsgType) + return + } + // process message + if !m.HandleMessage(ctx, ev.Peer, ev.Msg, ev.Resp) { + logger.Println(logger.WARN, "[dht] Message NOT handled!") } } } // Heartbeat handler for periodic tasks func (m *Module) heartbeat(ctx context.Context) { - // update the estimated network size - m.rtable.l2nse = m.core.L2NSE() - // run heartbeat for routing table m.rtable.heartbeat(ctx) + + // clean-up task list + m.reshdlrs.Cleanup() } +// Send the currently active HELLO to given network address +func (m *Module) SendHello(ctx context.Context, addr *util.Address) (err error) { + // get (buffered) HELLO + var msg *message.DHTP2PHelloMsg + if msg, err = m.getHello(); err != nil { + return + } + logger.Printf(logger.INFO, "[core] Sending HELLO to %s: %s", addr.URI(), msg) + return m.core.SendToAddr(ctx, addr, msg) +} + +// get the recent HELLO if it is defined and not expired; +// create a new HELLO otherwise. +func (m *Module) getHello() (msg *message.DHTP2PHelloMsg, err error) { + if m.lastHello == nil || m.lastHello.Expires.Expired() { + // assemble new (signed) HELLO block + var addrList []*util.Address + if addrList, err = m.core.Addresses(); err != nil { + return + } + // assemble HELLO data + hb := new(blocks.HelloBlock) + hb.PeerID = m.core.PeerID() + hb.Expires = util.NewAbsoluteTime(time.Now().Add(message.HelloAddressExpiration)) + hb.SetAddresses(addrList) + + // sign HELLO block + if err = m.core.Sign(hb); err != nil { + return + } + // assemble HELLO message + msg = message.NewDHTP2PHelloMsg() + msg.Expires = hb.Expires + msg.SetAddresses(hb.Addresses()) + if err = m.core.Sign(msg); err != nil { + return + } + + // save for later use + m.lastHello = msg + + // DEBUG + var ok bool + if ok, err = msg.Verify(m.core.PeerID()); !ok || err != nil { + if !ok { + err = errors.New("failed to verify own HELLO") + } + logger.Println(logger.ERROR, err.Error()) + return + } + logger.Println(logger.DBG, "[dht] New HELLO: "+transport.Dump(msg, "hex")) + return + } + // we have a valid HELLO for re-use. + return m.lastHello, nil +} + +//---------------------------------------------------------------------- +// Inter-module linkage helpers //---------------------------------------------------------------------- // Export functions func (m *Module) Export(fcn map[string]any) { // add exported functions from module fcn["dht:get"] = m.Get + fcn["dht:getapprox"] = m.GetApprox fcn["dht:put"] = m.Put } // Import functions -func (m *Module) Import(fcm map[string]any) { - // nothing to import now. +func (m *Module) Import(fcn map[string]any) { + // nothing to import for now. +} + +//---------------------------------------------------------------------- + +// SetNetworkSize sets a fixed number of peers in the network +func (m *Module) SetNetworkSize(numPeers int) { + m.rtable.l2nse = gmath.Log2(float64(numPeers)) } diff --git a/src/gnunet/service/dht/resulthandler.go b/src/gnunet/service/dht/resulthandler.go @@ -0,0 +1,351 @@ +// This file is part of gnunet-go, a GNUnet-implementation in Golang. +// Copyright (C) 2019-2022 Bernd Fix >Y< +// +// gnunet-go 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. +// +// gnunet-go 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/>. +// +// SPDX-License-Identifier: AGPL3.0-or-later + +package dht + +import ( + "bytes" + "context" + "gnunet/crypto" + "gnunet/message" + "gnunet/service/dht/blocks" + "gnunet/transport" + "gnunet/util" + "time" + + "github.com/bfix/gospel/logger" +) + +//====================================================================== +// DHT GET requests send to neighbours result in DHT RESULT messages +// being returned that need to be handled. The sequence of incoming +// results is undetermined and usually not terminated (that is, there +// is no mechanism to determine the end of results). +// ResultHandlers handle DHT RESULT messages. The appropriate handler +// is selected by the DHT query/store key associated with a GET/RESULT +// message; there can be multiple handlers for the same key (serving +// different GET requests and/or differnent originators). +//====================================================================== + +// ResultHandler interface +type ResultHandler interface { + + // ID returna the handler id + ID() int + + // Done returns true if handler can be removed + Done() bool + + // Key returns the query/store key as string + Key() string + + // Compare two result handlers + Compare(ResultHandler) int + + // Merge two result handlers that are the same except for result filter + Merge(ResultHandler) bool + + // Handle result message + Handle(context.Context, *message.DHTP2PResultMsg) bool +} + +// Compare return values +//nolint:stylecheck // allow non-camel-case in constants +const ( + RHC_SAME = blocks.CMP_SAME // the two result handlers are the same + RHC_MERGE = blocks.CMP_MERGE // the two result handlers can be merged + RHC_DIFFER = blocks.CMP_DIFFER // the two result handlers are different + RHC_SIBL = blocks.CMP_1 // the two result handlers are siblings +) + +//---------------------------------------------------------------------- + +// Generic (shared) result handler data structure +type GenericResultHandler struct { + id int // task identifier + key *crypto.HashCode // GET query key + btype uint32 // content type of the payload + flags uint16 // processing flags + resFilter blocks.ResultFilter // result filter + xQuery []byte // extended query + started util.AbsoluteTime // Timestamp of session start + active bool // is the task active? +} + +// NewGenericResultHandler creates an instance from a DHT-GET message and a +// result filter instance. +func NewGenericResultHandler(msg *message.DHTP2PGetMsg, rf blocks.ResultFilter) *GenericResultHandler { + return &GenericResultHandler{ + id: util.NextID(), + key: msg.Query.Clone(), + btype: msg.BType, + flags: msg.Flags, + resFilter: rf, + xQuery: util.Clone(msg.XQuery), + started: util.AbsoluteTimeNow(), + active: true, + } +} + +// ID returns the result handler identifier +func (t *GenericResultHandler) ID() int { + return t.id +} + +// Key returns the key string +func (t *GenericResultHandler) Key() string { + return t.key.String() +} + +// Done returns true if the result handler is no longer active. +func (t *GenericResultHandler) Done() bool { + return !t.active || t.started.Add(time.Hour).Expired() +} + +// Compare two handlers +func (t *GenericResultHandler) Compare(h *GenericResultHandler) int { + if t.key.Equals(h.key) || + t.btype != h.btype || + t.flags != h.flags || + !bytes.Equal(t.xQuery, h.xQuery) { + return RHC_DIFFER + } + return t.resFilter.Compare(h.resFilter) +} + +// Merge two result handlers that are the same except for result filter +func (t *GenericResultHandler) Merge(a *GenericResultHandler) bool { + return t.resFilter.Merge(a.resFilter) +} + +//---------------------------------------------------------------------- +// Result handler for forwarded GET requests +//---------------------------------------------------------------------- + +// ForwardResultHandler data structure +type ForwardResultHandler struct { + GenericResultHandler + + resp transport.Responder // responder for communicating back to originator +} + +// NewForwardResultHandler derived from DHT-GET message +func NewForwardResultHandler(msgIn message.Message, rf blocks.ResultFilter, back transport.Responder) *ForwardResultHandler { + // check for correct message type and handler function + msg, ok := msgIn.(*message.DHTP2PGetMsg) + if ok { + return &ForwardResultHandler{ + GenericResultHandler: *NewGenericResultHandler(msg, rf), + resp: back, + } + } + return nil +} + +// Handle incoming DHT-P2P-RESULT message +func (t *ForwardResultHandler) Handle(ctx context.Context, msg *message.DHTP2PResultMsg) bool { + // send result message back to originator (result forwarding). + logger.Printf(logger.INFO, "[dht-task-%d] sending result back to originator", t.id) + if err := t.resp.Send(ctx, msg); err != nil && err != transport.ErrEndpMaybeSent { + logger.Printf(logger.ERROR, "[dht-task-%d] sending result back to originator failed: %s", t.id, err.Error()) + return false + } + return true +} + +// Compare two forward result filters +func (t *ForwardResultHandler) Compare(h ResultHandler) int { + // check for correct handler type + ht, ok := h.(*ForwardResultHandler) + if !ok { + return RHC_DIFFER + } + // check for same recipient + if ht.resp.Receiver() != t.resp.Receiver() { + return RHC_DIFFER + } + // check generic handler data + return t.GenericResultHandler.Compare(&ht.GenericResultHandler) +} + +// Merge two forward result handlers +func (t *ForwardResultHandler) Merge(h ResultHandler) bool { + // check for correct handler type + ht, ok := h.(*ForwardResultHandler) + if !ok { + return false + } + return t.GenericResultHandler.Merge(&ht.GenericResultHandler) +} + +//---------------------------------------------------------------------- +// Result handler for locally-initiated GET requests: +// +// Before sending the GET request a handler is added for the request: +// +// rc := make(chan any) +// myRH := NewDirectResultHandler(msg, rf, MyCustomHandler, rc) +// m.reshdlrs.Add(myRH) +// +// If a matching response is received, the custom handler is executed +// in a separate go-routine. A custom handler returns a result (or error) on +// a back channel and should be context-sensitive (termination). +// +// If an asynchronous behaviour is required, use 'ret := <-rc' to wait for +// completion; synchronous execution does not require 'rc' (which can be set +// to nil). +//---------------------------------------------------------------------- + +// ResultHandlerFcn is the function prototype for custom handlers: +type ResultHandlerFcn func(context.Context, *message.DHTP2PResultMsg, chan<- any) bool + +// DirectResultHandler for local DHT-P2P-GET requests +type DirectResultHandler struct { + GenericResultHandler + + hdlr ResultHandlerFcn // Hdlr is a custom message handler + rc chan any // handler result channel +} + +// NewDirectResultHandler create a new GET handler instance +func NewDirectResultHandler(msgIn message.Message, rf blocks.ResultFilter, hdlr ResultHandlerFcn, rc chan any) *DirectResultHandler { + // check for correct message type and handler function + msg, ok := msgIn.(*message.DHTP2PGetMsg) + if ok { + return &DirectResultHandler{ + GenericResultHandler: *NewGenericResultHandler(msg, rf), + hdlr: hdlr, + rc: rc, + } + } + return nil +} + +// Handle incoming DHT-P2P-RESULT message +func (t *DirectResultHandler) Handle(ctx context.Context, msg *message.DHTP2PResultMsg) bool { + // check for correct message type and handler function + if t.hdlr != nil { + logger.Printf(logger.INFO, "[dht-task-%d] handling result message", t.id) + return t.hdlr(ctx, msg, t.rc) + } + return false +} + +// Compare two direct result handlers +func (t *DirectResultHandler) Compare(h ResultHandler) int { + // check for correct handler type + ht, ok := h.(*DirectResultHandler) + if !ok { + return RHC_DIFFER + } + // check generic handler data + return t.GenericResultHandler.Compare(&ht.GenericResultHandler) +} + +// Merge two direct result handlers +func (t *DirectResultHandler) Merge(h ResultHandler) bool { + // check for correct handler type + ht, ok := h.(*DirectResultHandler) + if !ok { + return false + } + // check generic handler data + return t.GenericResultHandler.Merge(&ht.GenericResultHandler) +} + +//---------------------------------------------------------------------- +// Handler list for book-keeping: +// * For each query/store key there can be multiple result handlers. +//---------------------------------------------------------------------- + +// ResultHandlerList holds the currently active tasks +type ResultHandlerList struct { + list *util.Map[string, []ResultHandler] // map of handlers +} + +// NewResultHandlerList creates a new task list +func NewResultHandlerList() *ResultHandlerList { + return &ResultHandlerList{ + list: util.NewMap[string, []ResultHandler](), + } +} + +// Add handler to list +func (t *ResultHandlerList) Add(hdlr ResultHandler) bool { + // get current list of handlers for key + key := hdlr.Key() + list, ok := t.list.Get(key) + modified := false + if !ok { + list = make([]ResultHandler, 0) + } else { + // check if handler is already available + loop: + for i, h := range list { + switch h.Compare(hdlr) { + case RHC_SAME: + // already in list; no need to add again + return false + case RHC_MERGE: + // merge the two result handlers + modified = h.Merge(hdlr) || modified + break loop + case RHC_SIBL: + // replace the old handler with the new one + list[i] = hdlr + modified = true + break loop + case RHC_DIFFER: + // try next + } + } + } + if !modified { + // append new handler to list + list = append(list, hdlr) + } + t.list.Put(key, list) + return true +} + +// Get handler list for given key +func (t *ResultHandlerList) Get(key string) ([]ResultHandler, bool) { + return t.list.Get(key) +} + +// Cleanup removes expired tasks from list +func (t *ResultHandlerList) Cleanup() { + err := t.list.ProcessRange(func(key string, list []ResultHandler) error { + var newList []ResultHandler + changed := false + for _, rh := range list { + if !rh.Done() { + newList = append(newList, rh) + } else { + changed = true + } + } + if changed { + t.list.Put(key, newList) + } + return nil + }, false) + if err != nil { + logger.Printf(logger.ERROR, "[ResultHandlerList] clean-up error: %s", err.Error()) + } +} diff --git a/src/gnunet/service/dht/routingtable.go b/src/gnunet/service/dht/routingtable.go @@ -21,10 +21,11 @@ package dht import ( "bytes" "context" - "crypto/sha512" "encoding/hex" + "gnunet/config" + "gnunet/crypto" + "gnunet/service/dht/blocks" "gnunet/util" - "math/rand" "sync" "time" @@ -32,61 +33,63 @@ import ( "github.com/bfix/gospel/math" ) -var ( - // routing table hash function: defines number of - // buckets and size of peer addresses - rtHash = sha512.New -) - -// Routing table contants (adjust with changing hash function) +// Routing table constants const ( - numBuckets = 512 // number of bits of hash function result - numK = 20 // number of entries per k-bucket - sizeAddr = 64 // size of peer address in bytes + numK = 20 // number of entries per k-bucket ) //====================================================================== +// Peer address //====================================================================== // PeerAddress is the identifier for a peer in the DHT network. // It is the SHA-512 hash of the PeerID (public Ed25519 key). type PeerAddress struct { - addr [sizeAddr]byte // hash value as bytes - connected bool // is peer connected? - lastSeen util.AbsoluteTime // time the peer was last seen - lastUsed util.AbsoluteTime // time the peer was last used + Peer *util.PeerID // peer identifier + Key *crypto.HashCode // address key is a sha512 hash + lastSeen util.AbsoluteTime // time the peer was last seen + lastUsed util.AbsoluteTime // time the peer was last used } // NewPeerAddress returns the DHT address of a peer. func NewPeerAddress(peer *util.PeerID) *PeerAddress { - r := new(PeerAddress) - h := rtHash() - h.Write(peer.Key) - copy(r.addr[:], h.Sum(nil)) - r.lastSeen = util.AbsoluteTimeNow() - r.lastUsed = util.AbsoluteTimeNow() - return r + return &PeerAddress{ + Peer: peer, + Key: crypto.Hash(peer.Data), + lastSeen: util.AbsoluteTimeNow(), + lastUsed: util.AbsoluteTimeNow(), + } +} + +// NewQueryAddress returns a wrapped peer address for a query key +func NewQueryAddress(key *crypto.HashCode) *PeerAddress { + return &PeerAddress{ + Peer: nil, + Key: crypto.NewHashCode(key.Bits), + lastSeen: util.AbsoluteTimeNow(), + lastUsed: util.AbsoluteTimeNow(), + } } // String returns a human-readble representation of an address. func (addr *PeerAddress) String() string { - return hex.EncodeToString(addr.addr[:]) + return hex.EncodeToString(addr.Key.Bits) } // Equals returns true if two peer addresses are the same. func (addr *PeerAddress) Equals(p *PeerAddress) bool { - return bytes.Equal(addr.addr[:], p.addr[:]) + return bytes.Equal(addr.Key.Bits, p.Key.Bits) } // Distance between two addresses: returns a distance value and a // bucket index (smaller index = less distant). func (addr *PeerAddress) Distance(p *PeerAddress) (*math.Int, int) { - var d PeerAddress - for i := range d.addr { - d.addr[i] = addr.addr[i] ^ p.addr[i] + d := make([]byte, 64) + for i := range d { + d[i] = addr.Key.Bits[i] ^ p.Key.Bits[i] } - r := math.NewIntFromBytes(d.addr[:]) - return r, numBuckets - r.BitLen() + r := math.NewIntFromBytes(d) + return r, 512 - r.BitLen() } //====================================================================== @@ -98,23 +101,28 @@ func (addr *PeerAddress) Distance(p *PeerAddress) (*math.Int, int) { // distance to the reference address, so smaller index means // "nearer" to the reference address. type RoutingTable struct { - ref *PeerAddress // reference address for distance - buckets []*Bucket // list of buckets - list map[*PeerAddress]struct{} // keep list of peers - mtx sync.RWMutex // lock for write operations - l2nse float64 // log2 of estimated network size - inProcess bool // flag if Process() is running + sync.RWMutex + + ref *PeerAddress // reference address for distance + buckets []*Bucket // list of buckets + list *util.Map[string, *PeerAddress] // keep list of peers + l2nse float64 // log2 of estimated network size + inProcess bool // flag if Process() is running + cfg *config.RoutingConfig // routing parameters + helloCache *util.Map[string, *blocks.HelloBlock] // HELLO block cache } // NewRoutingTable creates a new routing table for the reference address. -func NewRoutingTable(ref *PeerAddress) *RoutingTable { +func NewRoutingTable(ref *PeerAddress, cfg *config.RoutingConfig) *RoutingTable { // create routing table rt := &RoutingTable{ - ref: ref, - list: make(map[*PeerAddress]struct{}), - buckets: make([]*Bucket, numBuckets), - l2nse: 0., - inProcess: false, + ref: ref, + list: util.NewMap[string, *PeerAddress](), + buckets: make([]*Bucket, 512), + l2nse: -1, + inProcess: false, + cfg: cfg, + helloCache: util.NewMap[string, *blocks.HelloBlock](), } // fill buckets for i := range rt.buckets { @@ -130,41 +138,69 @@ func NewRoutingTable(ref *PeerAddress) *RoutingTable { // Add new peer address to routing table. // Returns true if the entry was added, false otherwise. func (rt *RoutingTable) Add(p *PeerAddress) bool { - // ensure one write and no readers - rt.lock(false) - defer rt.unlock(false) + k := p.String() + logger.Printf(logger.DBG, "[RT] Add(%s)", k) // check if peer is already known - if _, ok := rt.list[p]; ok { + if px, ok := rt.list.Get(k); ok { + logger.Println(logger.DBG, "[RT] --> already known") + px.lastSeen = util.AbsoluteTimeNow() return false } // compute distance (bucket index) and insert address. _, idx := p.Distance(rt.ref) if rt.buckets[idx].Add(p) { - rt.list[p] = struct{}{} + logger.Println(logger.DBG, "[RT] --> entry added") + p.lastUsed = util.AbsoluteTimeNow() + rt.list.Put(k, p) return true } // Full bucket: we did not add the address to the routing table. + logger.Println(logger.DBG, "[RT] --> bucket full -- discarded") return false } // Remove peer address from routing table. // Returns true if the entry was removed, false otherwise. func (rt *RoutingTable) Remove(p *PeerAddress) bool { - // ensure one write and no readers - rt.lock(false) - defer rt.unlock(false) + k := p.String() + logger.Printf(logger.DBG, "[RT] Remove(%s)", k) // compute distance (bucket index) and remove entry from bucket + rc := false _, idx := p.Distance(rt.ref) if rt.buckets[idx].Remove(p) { - delete(rt.list, p) - return true + logger.Println(logger.DBG, "[RT] --> entry removed from bucket and internal lists") + rc = true + } else { + // remove from internal list + logger.Println(logger.DBG, "[RT] --> entry removed from internal lists only") } - // remove from internal list - delete(rt.list, p) - return false + rt.list.Delete(k) + // delete from HELLO cache + rt.helloCache.Delete(p.Peer.String()) + return rc +} + +// Contains checks if a peer is available in the routing table +func (rt *RoutingTable) Contains(p *PeerAddress) bool { + k := p.String() + logger.Printf(logger.DBG, "[RT] Contains(%s)?", k) + + // check for peer in internal list + px, ok := rt.list.Get(k) + if !ok { + logger.Println(logger.DBG, "[RT] --> NOT found in current list:") + _ = rt.list.ProcessRange(func(key string, val *PeerAddress) error { + logger.Printf(logger.DBG, "[RT] * %s", val) + return nil + }, true) + } else { + logger.Println(logger.DBG, "[RT] --> found in current list") + px.lastSeen = util.AbsoluteTimeNow() + } + return ok } //---------------------------------------------------------------------- @@ -186,51 +222,53 @@ func (rt *RoutingTable) Process(f func() error, readonly bool) error { // Routing functions //---------------------------------------------------------------------- -// SelectClosestPeer for a given peer address and bloomfilter. -func (rt *RoutingTable) SelectClosestPeer(p *PeerAddress, bf *PeerBloomFilter) (n *PeerAddress) { +// SelectClosestPeer for a given peer address and peer filter. +func (rt *RoutingTable) SelectClosestPeer(p *PeerAddress, pf *blocks.PeerFilter) (n *PeerAddress) { // no writer allowed - rt.mtx.RLock() - defer rt.mtx.RUnlock() + rt.RLock() + defer rt.RUnlock() - // find closest address + // find closest peer in routing table var dist *math.Int for _, b := range rt.buckets { - if k, d := b.SelectClosestPeer(p, bf); n == nil || (d != nil && d.Cmp(dist) < 0) { + if k, d := b.SelectClosestPeer(p, pf); n == nil || (d != nil && d.Cmp(dist) < 0) { dist = d n = k } } // mark peer as used - n.lastUsed = util.AbsoluteTimeNow() + if n != nil { + n.lastUsed = util.AbsoluteTimeNow() + } return } // SelectRandomPeer returns a random address from table (that is not // included in the bloomfilter) -func (rt *RoutingTable) SelectRandomPeer(bf *PeerBloomFilter) *PeerAddress { +func (rt *RoutingTable) SelectRandomPeer(pf *blocks.PeerFilter) (p *PeerAddress) { // no writer allowed - rt.mtx.RLock() - defer rt.mtx.RUnlock() + rt.RLock() + defer rt.RUnlock() // select random entry from list - if size := len(rt.list); size > 0 { - idx := rand.Intn(size) - for k := range rt.list { - if idx == 0 { - // mark peer as used - k.lastUsed = util.AbsoluteTimeNow() - return k - } - idx-- + var ok bool + for { + if _, p, ok = rt.list.GetRandom(); !ok { + return nil + } + if !pf.Contains(p.Peer) { + break } } - return nil + // mark peer as used + p.lastUsed = util.AbsoluteTimeNow() + return } // SelectPeer selects a neighbor depending on the number of hops parameter. // If hops < NSE this function MUST return SelectRandomPeer() and // SelectClosestpeer() otherwise. -func (rt *RoutingTable) SelectPeer(p *PeerAddress, hops int, bf *PeerBloomFilter) *PeerAddress { +func (rt *RoutingTable) SelectPeer(p *PeerAddress, hops int, bf *blocks.PeerFilter) *PeerAddress { if float64(hops) < rt.l2nse { return rt.SelectRandomPeer(bf) } @@ -238,9 +276,24 @@ func (rt *RoutingTable) SelectPeer(p *PeerAddress, hops int, bf *PeerBloomFilter } // IsClosestPeer returns true if p is the closest peer for k. Peers with a -// positive test in the Bloom filter are not considered. -func (rt *RoutingTable) IsClosestPeer(p, k *PeerAddress, bf *PeerBloomFilter) bool { - n := rt.SelectClosestPeer(k, bf) +// positive test in the Bloom filter are not considered. If p is nil, our +// reference address is used. +func (rt *RoutingTable) IsClosestPeer(p, k *PeerAddress, pf *blocks.PeerFilter) bool { + // get closest peer in routing table + n := rt.SelectClosestPeer(k, pf) + // check SELF? + if p == nil { + // if no peer in routing table found + if n == nil { + // local peer is closest + return true + } + // check if local distance is smaller than for best peer in routing table + d0, _ := n.Distance(k) + d1, _ := rt.ref.Distance(k) + return d1.Cmp(d0) < 0 + } + // check if p is closest peer return n.Equals(p) } @@ -248,7 +301,7 @@ func (rt *RoutingTable) IsClosestPeer(p, k *PeerAddress, bf *PeerBloomFilter) bo // The arguments are the desired replication level, the hop count of the message so far, // and the base-2 logarithm of the current network size estimate (L2NSE) as provided by the // underlay. The result is the non-negative number of next hops to select. -func (rt *RoutingTable) ComputeOutDegree(repl, hop int) int { +func (rt *RoutingTable) ComputeOutDegree(repl, hop uint16) int { hf := float64(hop) if hf > 4*rt.l2nse { return 0 @@ -271,22 +324,62 @@ func (rt *RoutingTable) ComputeOutDegree(repl, hop int) int { func (rt *RoutingTable) heartbeat(ctx context.Context) { // check for dead or expired peers - timeout := util.NewRelativeTime(3 * time.Hour) + logger.Println(logger.DBG, "[dht] RT heartbeat...") + timeout := util.NewRelativeTime(time.Duration(rt.cfg.PeerTTL) * time.Second) if err := rt.Process(func() error { - for addr := range rt.list { - if addr.connected { - continue - } + return rt.list.ProcessRange(func(k string, p *PeerAddress) error { // check if we can/need to drop a peer - drop := timeout.Compare(addr.lastSeen.Elapsed()) < 0 - if drop || timeout.Compare(addr.lastUsed.Elapsed()) < 0 { - rt.Remove(addr) + drop := timeout.Compare(p.lastSeen.Elapsed()) < 0 + if drop || timeout.Compare(p.lastUsed.Elapsed()) < 0 { + logger.Printf(logger.DBG, "[RT] removing %v: %v, %v", p, p.lastSeen.Elapsed(), p.lastUsed.Elapsed()) + rt.Remove(p) } - } - return nil + return nil + }, false) }, false); err != nil { - logger.Println(logger.ERROR, "[dht] RT heartbeat: "+err.Error()) + logger.Println(logger.ERROR, "[dht] RT heartbeat failed: "+err.Error()) } + + // drop expired entries from the HELLO cache + _ = rt.helloCache.ProcessRange(func(key string, val *blocks.HelloBlock) error { + if val.Expires.Expired() { + rt.helloCache.Delete(key) + } + return nil + }, false) + + // update the estimated network size + // rt.l2nse = ... +} + +//---------------------------------------------------------------------- + +func (rt *RoutingTable) BestHello(addr *PeerAddress, rf blocks.ResultFilter) (hb *blocks.HelloBlock, dist *math.Int) { + // iterate over cached HELLOs to find (best) match first + _ = rt.helloCache.ProcessRange(func(key string, val *blocks.HelloBlock) error { + // check if block is excluded by result filter + if !rf.Contains(val) { + // check for better match + p := NewPeerAddress(val.PeerID) + d, _ := addr.Distance(p) + if hb == nil || d.Cmp(dist) < 0 { + hb = val + dist = d + } + } + return nil + }, true) + return +} + +// CacheHello adds a HELLO block to the list of cached entries. +func (rt *RoutingTable) CacheHello(hb *blocks.HelloBlock) { + rt.helloCache.Put(hb.PeerID.String(), hb) +} + +// GetHello returns a HELLO block for key k (if available) +func (rt *RoutingTable) GetHello(k string) (*blocks.HelloBlock, bool) { + return rt.helloCache.Get(k) } //---------------------------------------------------------------------- @@ -295,9 +388,9 @@ func (rt *RoutingTable) heartbeat(ctx context.Context) { func (rt *RoutingTable) lock(readonly bool) { if !rt.inProcess { if readonly { - rt.mtx.RLock() + rt.RLock() } else { - rt.mtx.Lock() + rt.Lock() } } } @@ -306,9 +399,9 @@ func (rt *RoutingTable) lock(readonly bool) { func (rt *RoutingTable) unlock(readonly bool) { if !rt.inProcess { if readonly { - rt.mtx.RUnlock() + rt.RUnlock() } else { - rt.mtx.Unlock() + rt.Unlock() } } } @@ -319,8 +412,9 @@ func (rt *RoutingTable) unlock(readonly bool) { // Bucket holds peer entries with approx. same distance from node type Bucket struct { - list []*PeerAddress - rwlock sync.RWMutex + sync.RWMutex + + list []*PeerAddress // list of peer addresses in bucket. } // NewBucket creates a new entry list of given size @@ -334,8 +428,8 @@ func NewBucket(n int) *Bucket { // Returns true if entry is added, false otherwise. func (b *Bucket) Add(p *PeerAddress) bool { // only one writer and no readers - b.rwlock.Lock() - defer b.rwlock.Unlock() + b.Lock() + defer b.Unlock() // check for free space in bucket if len(b.list) < numK { @@ -343,6 +437,7 @@ func (b *Bucket) Add(p *PeerAddress) bool { b.list = append(b.list, p) return true } + // full bucket: no further additions return false } @@ -350,8 +445,8 @@ func (b *Bucket) Add(p *PeerAddress) bool { // Returns true if entry is removed (found), false otherwise. func (b *Bucket) Remove(p *PeerAddress) bool { // only one writer and no readers - b.rwlock.Lock() - defer b.rwlock.Unlock() + b.Lock() + defer b.Unlock() for i, pe := range b.list { if pe.Equals(p) { @@ -365,14 +460,14 @@ func (b *Bucket) Remove(p *PeerAddress) bool { // SelectClosestPeer returns the entry with minimal distance to the given // peer address; entries included in the bloom flter are ignored. -func (b *Bucket) SelectClosestPeer(p *PeerAddress, bf *PeerBloomFilter) (n *PeerAddress, dist *math.Int) { +func (b *Bucket) SelectClosestPeer(p *PeerAddress, pf *blocks.PeerFilter) (n *PeerAddress, dist *math.Int) { // no writer allowed - b.rwlock.RLock() - defer b.rwlock.RUnlock() + b.RLock() + defer b.RUnlock() for _, addr := range b.list { // skip addresses in bloomfilter - if bf.Contains(addr) { + if pf.Contains(addr.Peer) { continue } // check for shorter distance diff --git a/src/gnunet/service/dht/routingtable_test.go b/src/gnunet/service/dht/routingtable_test.go @@ -19,8 +19,11 @@ package dht import ( + "crypto/sha512" + "encoding/hex" "gnunet/config" "gnunet/core" + "gnunet/service/dht/blocks" "gnunet/util" "math/rand" "testing" @@ -43,7 +46,7 @@ type Entry struct { // test data var ( - cfg = &config.NodeConfig{ + nodeCfg = &config.NodeConfig{ PrivateSeed: "YGoe6XFH3XdvFRl+agx9gIzPTvxA229WFdkazEMdcOs=", Endpoints: []*config.EndpointConfig{ { @@ -53,6 +56,9 @@ var ( }, }, } + rtCfg = &config.RoutingConfig{ + PeerTTL: 10800, + } ) // TestRT connects and disconnects random peers to test the base @@ -64,18 +70,16 @@ func TestRT(t *testing.T) { // helper functions genRemotePeer := func() *PeerAddress { d := make([]byte, 32) - if _, err := rand.Read(d); err != nil { - panic(err) - } + _, _ = rand.Read(d) return NewPeerAddress(util.NewPeerID(d)) } // create routing table and start command handler - local, err := core.NewLocalPeer(cfg) + local, err := core.NewLocalPeer(nodeCfg) if err != nil { t.Fatal(err) } - rt := NewRoutingTable(NewPeerAddress(local.GetID())) + rt := NewRoutingTable(NewPeerAddress(local.GetID()), rtCfg) // create a task list tasks := make([]*Entry, NUMP) @@ -91,7 +95,6 @@ func TestRT(t *testing.T) { // actions: connected := func(task *Entry, e int64, msg string) { - task.addr.connected = true rt.Add(task.addr) task.online = true task.last = e @@ -136,10 +139,29 @@ func TestRT(t *testing.T) { // execute some routing functions on remaining table k := genRemotePeer() - bf := NewPeerBloomFilter() - n := rt.SelectClosestPeer(k, bf) + pf := blocks.NewPeerFilter() + n := rt.SelectClosestPeer(k, pf) t.Logf("Closest: %s -> %s\n", k, n) - n = rt.SelectRandomPeer(bf) + n = rt.SelectRandomPeer(pf) t.Logf("Random: %s\n", n) } + +func TestDistance(t *testing.T) { + pid1 := "4ER9C0GV4QC25GGQMXBBGXYFEB3ZVAYMXZVSRKDVEGCDTAS34E30" + pid2 := "V61ESQ96AFXZWDSA509HP11K5HJXXJ9ECM4NAMCQRX5YW4KN8XPG" + + p1, _ := util.DecodeStringToBinary(pid1, 32) + p2, _ := util.DecodeStringToBinary(pid2, 32) + + h1 := sha512.Sum512(p1) + h2 := sha512.Sum512(p2) + t.Logf("h1=%s\n", hex.EncodeToString(h1[:])) + t.Logf("h2=%s\n", hex.EncodeToString(h2[:])) + + pa1 := NewPeerAddress(util.NewPeerID(p1)) + pa2 := NewPeerAddress(util.NewPeerID(p2)) + + dist, idx := pa1.Distance(pa2) + t.Logf("dist=%v, idx=%d\n", dist, idx) +} diff --git a/src/gnunet/service/dht/rpc.go b/src/gnunet/service/dht/rpc.go @@ -19,21 +19,49 @@ package dht import ( - "net/rpc" - "time" + "gnunet/service" + "net/http" + + "github.com/bfix/gospel/logger" ) //---------------------------------------------------------------------- -type DHTCommand struct{} +// RPCService is a type for DHT-related JSON-RPC requests +type RPCService struct{} + +// local instance of service +var dhtRPC = &RPCService{} + +//---------------------------------------------------------------------- +// Command "DHT.Status" +//---------------------------------------------------------------------- -type DHTStats struct { - Started time.Time +// StatusRequest is a status request for specific information addressed +// by topic(s) +type StatusRequest struct { + Topics []string `json:"topics"` } -func (c *DHTCommand) Status(mode int, stats *DHTStats) error { - *stats = DHTStats{ - Started: time.Now(), +// StatusResponse is a response to a status request. It returns information +// on each topic requested. +type StatusResponse struct { + Messages map[string]string `json:"messages"` +} + +// Status requests information by topic(s). +func (s *RPCService) Status(r *http.Request, req *StatusRequest, reply *StatusResponse) error { + // assemble information on topic(s) + out := make(map[string]string) + for _, topic := range req.Topics { + switch topic { + case "echo": + out[topic] = "echo test" + } + } + // set reply + *reply = StatusResponse{ + Messages: out, } return nil } @@ -41,6 +69,8 @@ func (c *DHTCommand) Status(mode int, stats *DHTStats) error { //---------------------------------------------------------------------- // InitRPC registers RPC commands for the module -func (m *Module) InitRPC(srv *rpc.Server) { - srv.Register(new(DHTCommand)) +func (m *Module) InitRPC(srv *service.JRPCServer) { + if err := srv.RegisterService(dhtRPC, "DHT"); err != nil { + logger.Printf(logger.ERROR, "[dht] Failed to init RPC: %s", err.Error()) + } } diff --git a/src/gnunet/service/dht/service.go b/src/gnunet/service/dht/service.go @@ -23,10 +23,9 @@ import ( "fmt" "io" + "gnunet/config" "gnunet/core" - "gnunet/message" "gnunet/service" - "gnunet/transport" "github.com/bfix/gospel/logger" ) @@ -48,15 +47,14 @@ type Service struct { } // NewService creates a new DHT service instance -func NewService(ctx context.Context, c *core.Core) (service.Service, error) { - mod, err := NewModule(ctx, c) +func NewService(ctx context.Context, c *core.Core, cfg *config.DHTConfig) (*Service, error) { + mod, err := NewModule(ctx, c, cfg) if err != nil { return nil, err } srv := &Service{ Module: *mod, } - srv.ProcessFcn = srv.HandleMessage return srv, nil } @@ -85,7 +83,8 @@ loop: logger.Printf(logger.INFO, "[dht:%d:%d] Received request: %v\n", id, reqID, msg) // handle message - s.HandleMessage(context.WithValue(ctx, "label", fmt.Sprintf(":%d:%d", id, reqID)), msg, mc) + valueCtx := context.WithValue(ctx, service.CtxKey("label"), fmt.Sprintf(":%d:%d", id, reqID)) + s.HandleMessage(valueCtx, nil, msg, mc) } // close client connection mc.Close() @@ -94,48 +93,3 @@ loop: logger.Printf(logger.INFO, "[dht:%d] Start closing session...\n", id) cancel() } - -// HandleMessage handles a DHT request/response message. If the transport channel -// is nil, responses are send directly via the transport layer. -func (s *Service) HandleMessage(ctx context.Context, msg message.Message, back transport.Responder) bool { - // assemble log label - label := "" - if v := ctx.Value("label"); v != nil { - label = v.(string) - } - // process message - switch msg.(type) { - case *message.DHTClientPutMsg: - //---------------------------------------------------------- - // DHT PUT - //---------------------------------------------------------- - - case *message.DHTClientGetMsg: - //---------------------------------------------------------- - // DHT GET - //---------------------------------------------------------- - - case *message.DHTClientGetResultsKnownMsg: - //---------------------------------------------------------- - // DHT GET-RESULTS-KNOWN - //---------------------------------------------------------- - - case *message.DHTClientGetStopMsg: - //---------------------------------------------------------- - // DHT GET-STOP - //---------------------------------------------------------- - - case *message.DHTClientResultMsg: - //---------------------------------------------------------- - // DHT RESULT - //---------------------------------------------------------- - - default: - //---------------------------------------------------------- - // UNKNOWN message type received - //---------------------------------------------------------- - logger.Printf(logger.ERROR, "[dht-%s] Unhandled message of type (%d)\n", label, msg.Header().MsgType) - return false - } - return true -} diff --git a/src/gnunet/service/gns/block_handler.go b/src/gnunet/service/gns/block_handler.go @@ -30,7 +30,7 @@ import ( "github.com/bfix/gospel/logger" ) -// HdlrInst is the type for functions that instanciate custom block handlers. +// HdlrInst is the type for functions that instantiate custom block handlers. type HdlrInst func(*message.ResourceRecord, []string) (BlockHandler, error) // Error codes @@ -157,7 +157,7 @@ func NewBlockHandlerList(records []*message.ResourceRecord, labels []string) (*B hl.counts.Add(rrType) // check for custom handler type - if creat, ok := customHandler[enums.GNSType(rrType)]; ok { + if creat, ok := customHandler[rrType]; ok { // check if a handler for given type already exists var ( hdlr BlockHandler @@ -192,7 +192,7 @@ func NewBlockHandlerList(records []*message.ResourceRecord, labels []string) (*B } // GetHandler returns a BlockHandler for the given GNS block type. -// If more than one type is given, the first matching hanlder is +// If more than one type is given, the first matching handler is // returned. func (hl *BlockHandlerList) GetHandler(types ...enums.GNSType) BlockHandler { for _, t := range types { diff --git a/src/gnunet/service/gns/dns.go b/src/gnunet/service/gns/dns.go @@ -204,7 +204,7 @@ func QueryDNS(id int, name string, server net.IP, kind RRTypeList) *message.Reco // ResolveDNS resolves a name in DNS. Multiple DNS servers are queried in // parallel; the first result delivered by any of the servers is returned // as the result list of matching resource records. -func (gns *Module) ResolveDNS( +func (m *Module) ResolveDNS( ctx context.Context, name string, servers []string, @@ -223,7 +223,7 @@ func (gns *Module) ResolveDNS( if addr == nil { // no, it is a name... try to resolve an IP address from the name query := NewRRTypeList(enums.GNS_TYPE_DNS_A, enums.GNS_TYPE_DNS_AAAA) - if set, err = gns.ResolveUnknown(ctx, srv, nil, zkey, query, depth+1); err != nil { + if set, err = m.ResolveUnknown(ctx, srv, nil, zkey, query, depth+1); err != nil { logger.Printf(logger.ERROR, "[dns] Can't resolve NS server '%s': %s\n", srv, err.Error()) continue } diff --git a/src/gnunet/service/gns/module.go b/src/gnunet/service/gns/module.go @@ -103,7 +103,7 @@ func NewModule(ctx context.Context, c *core.Core) (m *Module) { ModuleImpl: *service.NewModuleImpl(), } // register as listener for core events - listener := m.Run(ctx, m.event, m.Filter(), 0, nil) + listener := m.ModuleImpl.Run(ctx, m.event, m.Filter(), 0, nil) c.Register("gns", listener) return } @@ -135,11 +135,11 @@ func (m *Module) Export(fcn map[string]any) { // Import functions func (m *Module) Import(fcn map[string]any) { // resolve imports from other modules - m.LookupLocal = fcn["namecache:get"].(func(ctx context.Context, query *blocks.GNSQuery) (*blocks.GNSBlock, error)) - m.StoreLocal = fcn["namecache:put"].(func(ctx context.Context, query *blocks.GNSQuery, block *blocks.GNSBlock) error) - m.LookupRemote = fcn["dht:get"].(func(ctx context.Context, query blocks.Query) (blocks.Block, error)) - m.RevocationQuery = fcn["rev:query"].(func(ctx context.Context, zkey *crypto.ZoneKey) (valid bool, err error)) - m.RevocationRevoke = fcn["rev:revoke"].(func(ctx context.Context, rd *revocation.RevData) (success bool, err error)) + m.LookupLocal, _ = fcn["namecache:get"].(func(ctx context.Context, query *blocks.GNSQuery) (*blocks.GNSBlock, error)) + m.StoreLocal, _ = fcn["namecache:put"].(func(ctx context.Context, query *blocks.GNSQuery, block *blocks.GNSBlock) error) + m.LookupRemote, _ = fcn["dht:get"].(func(ctx context.Context, query blocks.Query) (blocks.Block, error)) + m.RevocationQuery, _ = fcn["rev:query"].(func(ctx context.Context, zkey *crypto.ZoneKey) (valid bool, err error)) + m.RevocationRevoke, _ = fcn["rev:revoke"].(func(ctx context.Context, rd *revocation.RevData) (success bool, err error)) } //---------------------------------------------------------------------- @@ -257,7 +257,7 @@ func (m *Module) ResolveRelative( if hdlr := hdlrs.GetHandler(crypto.ZoneTypes...); hdlr != nil { // (1) zone key record: - inst := hdlr.(*ZoneKeyHandler) + inst, _ := hdlr.(*ZoneKeyHandler) // if labels are pending, set new zone and continue resolution; // otherwise resolve "@" label for the zone if no zone key record // was requested. @@ -265,14 +265,15 @@ func (m *Module) ResolveRelative( labels = append(labels, "@") } // check if zone key has been revoked - if valid, err := m.RevocationQuery(ctx, inst.zkey); err != nil || !valid { + var valid bool + if valid, err = m.RevocationQuery(ctx, inst.zkey); err != nil || !valid { // revoked key -> no results! records = make([]*message.ResourceRecord, 0) break } } else if hdlr := hdlrs.GetHandler(enums.GNS_TYPE_GNS2DNS); hdlr != nil { // (2) GNS2DNS records - inst := hdlr.(*Gns2DnsHandler) + inst, _ := hdlr.(*Gns2DnsHandler) // if we are at the end of the path and the requested type // includes GNS_TYPE_GNS2DNS, the GNS2DNS records are returned... if len(labels) == 1 && kind.HasType(enums.GNS_TYPE_GNS2DNS) && !kind.IsAny() { @@ -308,7 +309,7 @@ func (m *Module) ResolveRelative( break } else if hdlr := hdlrs.GetHandler(enums.GNS_TYPE_BOX); hdlr != nil { // (3) BOX records: - inst := hdlr.(*BoxHandler) + inst, _ := hdlr.(*BoxHandler) newRecords := inst.Records(kind).Records if len(newRecords) > 0 { records = newRecords @@ -316,7 +317,7 @@ func (m *Module) ResolveRelative( } } else if hdlr := hdlrs.GetHandler(enums.GNS_TYPE_DNS_CNAME); hdlr != nil { // (4) CNAME records: - inst := hdlr.(*CnameHandler) + inst, _ := hdlr.(*CnameHandler) // if we are at the end of the path and the requested type // includes GNS_TYPE_DNS_CNAME, the records are returned... if len(labels) == 1 && kind.HasType(enums.GNS_TYPE_DNS_CNAME) && !kind.IsAny() { @@ -352,7 +353,7 @@ func (m *Module) ResolveRelative( // check for VPN record if hdlr := hdlrs.GetHandler(enums.GNS_TYPE_VPN); hdlr != nil { // add VPN record to result set - inst := hdlr.(*VpnHandler) + inst, _ := hdlr.(*VpnHandler) set.AddRecord(inst.rec) } } @@ -458,7 +459,10 @@ func (m *Module) Lookup( return } // store RRs from remote locally. - m.StoreLocal(ctx, query, block) + if err = m.StoreLocal(ctx, query, block); err != nil { + logger.Printf(logger.DBG, "[gns] store local failed: %s", err.Error()) + return + } } } return diff --git a/src/gnunet/service/gns/rpc.go b/src/gnunet/service/gns/rpc.go @@ -18,10 +18,10 @@ package gns -import "net/rpc" +import "gnunet/service" //---------------------------------------------------------------------- // InitRPC registers RPC commands for the module -func (m *Module) InitRPC(srv *rpc.Server) { +func (m *Module) InitRPC(srv *service.JRPCServer) { } diff --git a/src/gnunet/service/gns/service.go b/src/gnunet/service/gns/service.go @@ -62,8 +62,6 @@ func NewService(ctx context.Context, c *core.Core) service.Service { srv := &Service{ Module: *mod, } - srv.ProcessFcn = srv.HandleMessage - // set external function references (external services) srv.LookupLocal = srv.LookupNamecache srv.StoreLocal = srv.StoreNamecache @@ -98,7 +96,8 @@ func (s *Service) ServeClient(ctx context.Context, id int, mc *service.Connectio logger.Printf(logger.INFO, "[gns:%d:%d] Received request: %v\n", id, reqID, msg) // handle message - s.HandleMessage(context.WithValue(ctx, "label", fmt.Sprintf(":%d:%d", id, reqID)), msg, mc) + valueCtx := context.WithValue(ctx, service.CtxKey("label"), fmt.Sprintf(":%d:%d", id, reqID)) + s.HandleMessage(valueCtx, nil, msg, mc) } // close client connection mc.Close() @@ -109,11 +108,11 @@ func (s *Service) ServeClient(ctx context.Context, id int, mc *service.Connectio } // Handle a single incoming message -func (s *Service) HandleMessage(ctx context.Context, msg message.Message, back transport.Responder) bool { +func (s *Service) HandleMessage(ctx context.Context, sender *util.PeerID, msg message.Message, back transport.Responder) bool { // assemble log label label := "" if v := ctx.Value("label"); v != nil { - label = v.(string) + label, _ = v.(string) } // perform lookup switch m := msg.(type) { @@ -123,7 +122,7 @@ func (s *Service) HandleMessage(ctx context.Context, msg message.Message, back t //---------------------------------------------------------- // perform lookup on block (locally and remote) - go func(m *message.LookupMsg) { + go func(m *message.LookupMsg, label string) { logger.Printf(logger.INFO, "[gns%s] Lookup request received.\n", label) resp := message.NewGNSLookupResultMsg(m.ID) defer func() { @@ -137,7 +136,6 @@ func (s *Service) HandleMessage(ctx context.Context, msg message.Message, back t logger.Printf(logger.DBG, "[gns%s] Lookup request finished.\n", label) }() - label := m.GetName() kind := NewRRTypeList(enums.GNSType(m.Type)) recset, err := s.Resolve(ctx, label, m.Zone, kind, int(m.Options), 0) if err != nil { @@ -163,11 +161,13 @@ func (s *Service) HandleMessage(ctx context.Context, msg message.Message, back t // is this the record type we are looking for? if rec.Type == m.Type || enums.GNSType(m.Type) == enums.GNS_TYPE_ANY { // add it to the response message - resp.AddRecord(rec) + if err := resp.AddRecord(rec); err != nil { + logger.Printf(logger.ERROR, "[gns%s] failed: %sv", label, err.Error()) + } } } } - }(m) + }(m, label) default: //---------------------------------------------------------- @@ -374,7 +374,7 @@ func (s *Service) LookupDHT(ctx context.Context, query blocks.Query) (block bloc // send DHT GET request and wait for response reqGet := message.NewDHTClientGetMsg(query.Key()) reqGet.ID = uint64(util.NextID()) - reqGet.ReplLevel = uint32(enums.DHT_GNS_REPLICATION_LEVEL) + reqGet.ReplLevel = uint32(enums.GNS_REPLICATION_LEVEL) reqGet.Type = uint32(enums.BLOCK_TYPE_GNS_NAMERECORD) reqGet.Options = uint32(enums.DHT_RO_DEMULTIPLEX_EVERYWHERE) @@ -419,7 +419,7 @@ func (s *Service) LookupDHT(ctx context.Context, query blocks.Query) (block bloc } // get GNSBlock from message - qGNS := query.(*blocks.GNSQuery) + qGNS, _ := query.(*blocks.GNSQuery) block = new(blocks.GNSBlock) if err = data.Unmarshal(block, m.Data); err != nil { logger.Printf(logger.ERROR, "[gns] can't read GNS block: %s\n", err.Error()) diff --git a/src/gnunet/service/module.go b/src/gnunet/service/module.go @@ -21,9 +21,6 @@ package service import ( "context" "gnunet/core" - "gnunet/message" - "gnunet/transport" - "net/rpc" "time" ) @@ -66,7 +63,7 @@ type Module interface { Import(map[string]any) // InitRPC registers RPC commands for the module - InitRPC(*rpc.Server) + InitRPC(*JRPCServer) // Filter returns the event filter for the module Filter() *core.EventFilter @@ -78,20 +75,19 @@ type EventHandler func(context.Context, *core.Event) // Heartbeat is a function prototype for periodic tasks type Heartbeat func(context.Context) +// CtxKey is a value-context key +type CtxKey string + // ModuleImpl is an event-handling type used by Module implementations. type ModuleImpl struct { // channel for core events. ch chan *core.Event - - // ProcessFcn message: function reference (implemented by service) - ProcessFcn func(ctx context.Context, msg message.Message, back transport.Responder) bool } // NewModuleImplementation returns a new base module and starts func NewModuleImpl() (m *ModuleImpl) { return &ModuleImpl{ - ch: make(chan *core.Event), - ProcessFcn: nil, + ch: make(chan *core.Event), } } @@ -108,14 +104,14 @@ func (m *ModuleImpl) Run( if heartbeat == nil { pulse = 365 * 24 * time.Hour // once a year } - tick := time.Tick(pulse) + tick := time.NewTicker(pulse) // run event loop go func() { for { select { // Handle events case event := <-m.ch: - hCtx := context.WithValue(ctx, "label", event.Label) + hCtx := context.WithValue(ctx, CtxKey("label"), event.Label) hdlr(hCtx, event) // wait for terminate signal @@ -123,7 +119,7 @@ func (m *ModuleImpl) Run( return // handle heartbeat - case <-tick: + case <-tick.C: // check for defined heartbeat handler if heartbeat != nil { heartbeat(ctx) diff --git a/src/gnunet/service/namecache/module.go b/src/gnunet/service/namecache/module.go @@ -24,6 +24,7 @@ import ( "gnunet/core" "gnunet/service" "gnunet/service/dht/blocks" + "gnunet/service/store" ) //====================================================================== @@ -38,7 +39,7 @@ import ( type Module struct { service.ModuleImpl - cache service.DHTStore // transient block cache + cache store.DHTStore // transient block cache } // NewModule creates a new module instance. @@ -46,7 +47,7 @@ func NewModule(ctx context.Context, c *core.Core) (m *Module) { m = &Module{ ModuleImpl: *service.NewModuleImpl(), } - m.cache, _ = service.NewDHTStore(config.Cfg.Namecache.Storage) + m.cache, _ = store.NewDHTStore(config.Cfg.Namecache.Storage) return } @@ -69,7 +70,9 @@ func (m *Module) Import(fcm map[string]any) { // Get an entry from the cache if available. func (m *Module) Get(ctx context.Context, query *blocks.GNSQuery) (block *blocks.GNSBlock, err error) { var b blocks.Block - b, err = m.cache.Get(query) + if b, err = m.cache.Get(query); err != nil { + return + } err = blocks.Unwrap(b, block) return } diff --git a/src/gnunet/service/revocation/module.go b/src/gnunet/service/revocation/module.go @@ -25,6 +25,7 @@ import ( "gnunet/crypto" "gnunet/message" "gnunet/service" + "gnunet/service/store" "gnunet/util" "net/http" @@ -44,7 +45,7 @@ type Module struct { service.ModuleImpl bloomf *data.BloomFilter // bloomfilter for fast revocation check - kvs service.KVStore // storage for known revocations + kvs store.KVStore // storage for known revocations } // NewModule returns an initialized revocation module @@ -55,7 +56,7 @@ func NewModule(ctx context.Context, c *core.Core) (m *Module) { } init := func() (err error) { // Initialize access to revocation data storage - if m.kvs, err = service.NewKVStore(config.Cfg.Revocation.Storage); err != nil { + if m.kvs, err = store.NewKVStore(config.Cfg.Revocation.Storage); err != nil { return } // traverse the storage and build bloomfilter for all keys @@ -170,6 +171,6 @@ func (m *Module) Revoke(ctx context.Context, rd *RevData) (success bool, err err // RPC returns the route and handler function for a JSON-RPC request func (m *Module) RPC() (string, func(http.ResponseWriter, *http.Request)) { return "/revocation/", func(wrt http.ResponseWriter, req *http.Request) { - wrt.Write([]byte(`{"msg": "This is REVOCATION" }`)) + _, _ = wrt.Write([]byte(`{"msg": "This is REVOCATION" }`)) } } diff --git a/src/gnunet/service/revocation/pow.go b/src/gnunet/service/revocation/pow.go @@ -22,7 +22,6 @@ import ( "bytes" "context" "encoding/binary" - "fmt" "sort" "time" @@ -62,27 +61,21 @@ func NewPoWData(pow uint64, ts util.AbsoluteTime, zoneKey *crypto.ZoneKey) *PoWD Timestamp: ts, ZoneKey: zoneKey, } - if rd.SetPoW(pow) != nil { - return nil - } + rd.SetPoW(pow) return rd } // SetPoW sets a new PoW value in the data structure -func (p *PoWData) SetPoW(pow uint64) error { +func (p *PoWData) SetPoW(pow uint64) { p.PoW = pow p.blob = p.Blob() - if p.blob == nil { - return fmt.Errorf("invalid PoW work unit") - } - return nil } // GetPoW returns the last checked PoW value func (p *PoWData) GetPoW() uint64 { if p.blob != nil { var val uint64 - binary.Read(bytes.NewReader(p.blob[:8]), binary.BigEndian, &val) + _ = binary.Read(bytes.NewReader(p.blob[:8]), binary.BigEndian, &val) p.PoW = val } return p.PoW @@ -138,14 +131,11 @@ type SignedRevData struct { // NewRevDataFromMsg initializes a new RevData instance from a GNUnet message func NewRevDataFromMsg(m *message.RevocationRevokeMsg) *RevData { - rd := &RevData{ + return &RevData{ Timestamp: m.Timestamp, ZoneKeySig: m.ZoneKeySig, + PoWs: util.Clone(m.PoWs), } - for i, pow := range m.PoWs { - rd.PoWs[i] = pow - } - return rd } // Size of a serialized RevData object. @@ -174,7 +164,6 @@ func (rd *RevData) Sign(skey *crypto.ZonePrivate) (err error) { // in this revocation and a verification status (-1=failed signature, -2= // expired revocation, -3="out-of-order" PoW sequence). func (rd *RevData) Verify(withSig bool) (zbits float64, rc int) { - // (1) check signature if withSig { sigBlock := &SignedRevData{ @@ -196,7 +185,7 @@ func (rd *RevData) Verify(withSig bool) (zbits float64, rc int) { } // (2) check PoWs - var last uint64 = 0 + var last uint64 for _, pow := range rd.PoWs { // check sequence order if pow <= last { @@ -252,7 +241,7 @@ func (rdc *RevDataCalc) Size() int { // Average number of leading zero-bits in current list func (rdc *RevDataCalc) Average() float64 { - var sum uint16 = 0 + var sum uint16 for _, num := range rdc.Bits { sum += num } @@ -290,7 +279,7 @@ func (rdc *RevDataCalc) sortBits() { func (rdc *RevDataCalc) Compute(ctx context.Context, bits int, last uint64, cb func(float64, uint64)) (float64, uint64) { // find the largest PoW value in current work unit work := NewPoWData(0, rdc.Timestamp, &rdc.ZoneKeySig.ZoneKey) - var max uint64 = 0 + var max uint64 for i, pow := range rdc.PoWs { if pow == 0 { max++ diff --git a/src/gnunet/service/revocation/pow_test.go b/src/gnunet/service/revocation/pow_test.go @@ -12,7 +12,6 @@ import ( // Test revocation with test vector defined in the RFC draft. func TestRevocationRFC(t *testing.T) { - var ( D = "6fea32c05af58bfa979553d188605fd57d8bf9cc263b78d5f7478c07b998ed70" ZKEY = "000100002ca223e879ecc4bbdeb5da17319281d63b2e3b6955f1c3775c804a98d5f8ddaa" diff --git a/src/gnunet/service/revocation/rpc.go b/src/gnunet/service/revocation/rpc.go @@ -18,10 +18,10 @@ package revocation -import "net/rpc" +import "gnunet/service" //---------------------------------------------------------------------- // InitRPC registers RPC commands for the module -func (m *Module) InitRPC(srv *rpc.Server) { +func (m *Module) InitRPC(srv *service.JRPCServer) { } diff --git a/src/gnunet/service/revocation/service.go b/src/gnunet/service/revocation/service.go @@ -27,6 +27,7 @@ import ( "gnunet/message" "gnunet/service" "gnunet/transport" + "gnunet/util" "github.com/bfix/gospel/logger" ) @@ -47,7 +48,6 @@ func NewService(ctx context.Context, c *core.Core) service.Service { srv := &Service{ Module: *mod, } - srv.ProcessFcn = srv.HandleMessage return srv } @@ -75,7 +75,8 @@ func (s *Service) ServeClient(ctx context.Context, id int, mc *service.Connectio logger.Printf(logger.INFO, "[revocation:%d:%d] Received request: %v\n", id, reqID, msg) // handle message - s.HandleMessage(context.WithValue(ctx, "label", fmt.Sprintf(":%d:%d", id, reqID)), msg, mc) + valueCtx := context.WithValue(ctx, service.CtxKey("label"), fmt.Sprintf(":%d:%d", id, reqID)) + s.HandleMessage(valueCtx, nil, msg, mc) } // close client connection mc.Close() @@ -86,11 +87,11 @@ func (s *Service) ServeClient(ctx context.Context, id int, mc *service.Connectio } // Handle a single incoming message -func (s *Service) HandleMessage(ctx context.Context, msg message.Message, back transport.Responder) bool { +func (s *Service) HandleMessage(ctx context.Context, sender *util.PeerID, msg message.Message, back transport.Responder) bool { // assemble log label label := "" if v := ctx.Value("label"); v != nil { - label = v.(string) + label, _ = v.(string) } switch m := msg.(type) { case *message.RevocationQueryMsg: diff --git a/src/gnunet/service/rpc.go b/src/gnunet/service/rpc.go @@ -21,24 +21,35 @@ package service import ( "context" "net/http" - "net/rpc" "time" "github.com/bfix/gospel/logger" "github.com/gorilla/mux" + "github.com/gorilla/rpc/v2" + "github.com/gorilla/rpc/v2/json2" ) //---------------------------------------------------------------------- +//---------------------------------------------------------------------- + +// JRPCServer for JSON-RPC handling (wrapper to keep type in our package) +type JRPCServer struct { + *rpc.Server +} + +//---------------------------------------------------------------------- // JSON-RPC interface for services to be used as the primary client API // for perform, manage and monitor GNUnet activities. //---------------------------------------------------------------------- -// StartRPC the JSON-RPC server. It can be terminated by context -func StartRPC(ctx context.Context, endpoint string) (srvRPC *rpc.Server, err error) { +// RunRPCServer runs the JSON-RPC server. It can be terminated by context only. +func RunRPCServer(ctx context.Context, endpoint string) (srvRPC *JRPCServer, err error) { + // instantiate RPC service + srvRPC = &JRPCServer{rpc.NewServer()} + srvRPC.RegisterCodec(json2.NewCodec(), "application/json") // setup RPC request handler router := mux.NewRouter() - srvRPC = rpc.NewServer() router.HandleFunc("/", srvRPC.ServeHTTP) // instantiate a server and run it @@ -51,16 +62,14 @@ func StartRPC(ctx context.Context, endpoint string) (srvRPC *rpc.Server, err err // start listening go func() { if err := srv.ListenAndServe(); err != http.ErrServerClosed { - logger.Printf(logger.WARN, "[RPC] Server listen failed: %s", err.Error()) + logger.Printf(logger.WARN, "[rpc] server listen failed: %s", err.Error()) } }() // wait for shutdown go func() { - select { - case <-ctx.Done(): - if err := srv.Shutdown(context.Background()); err != nil { - logger.Printf(logger.WARN, "[RPC] Server shutdownn failed: %s", err.Error()) - } + <-ctx.Done() + if err := srv.Shutdown(context.Background()); err != nil { + logger.Printf(logger.WARN, "[rpc] server shutdownn failed: %s", err.Error()) } }() return diff --git a/src/gnunet/service/service.go b/src/gnunet/service/service.go @@ -42,7 +42,7 @@ type Service interface { // Handle a single incoming message (either locally from a socket // connection or from Transport). Response messages can be send // via a Responder. Returns true if message was processed. - HandleMessage(ctx context.Context, msg message.Message, resp transport.Responder) bool + HandleMessage(ctx context.Context, sender *util.PeerID, msg message.Message, resp transport.Responder) bool } // SocketHandler handles incoming connections on the local service socket. @@ -85,7 +85,6 @@ func (h *SocketHandler) Start(ctx context.Context, path string, params map[strin loop: for { select { - // handle incoming connection case conn := <-h.hdlr: // run a new session with context diff --git a/src/gnunet/service/store.go b/src/gnunet/service/store.go @@ -1,502 +0,0 @@ -// This file is part of gnunet-go, a GNUnet-implementation in Golang. -// Copyright (C) 2019-2022 Bernd Fix >Y< -// -// gnunet-go 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. -// -// gnunet-go 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/>. -// -// SPDX-License-Identifier: AGPL3.0-or-later - -package service - -import ( - "context" - "database/sql" - "encoding/binary" - "encoding/gob" - "encoding/hex" - "errors" - "fmt" - "gnunet/config" - "gnunet/crypto" - "gnunet/service/dht/blocks" - "gnunet/util" - "io" - "io/ioutil" - "os" - "sort" - "sync" - - "github.com/bfix/gospel/logger" - redis "github.com/go-redis/redis/v8" -) - -// Error messages related to the key/value-store implementations -var ( - ErrStoreInvalidSpec = fmt.Errorf("Invalid Store specification") - ErrStoreUnknown = fmt.Errorf("Unknown Store type") - ErrStoreNotAvailable = fmt.Errorf("Store not available") -) - -//------------------------------------------------------------ -// Generic storage interface. Can be used for persistent or -// transient (caching) storage of key/value data. -//------------------------------------------------------------ - -// Store is a key/value storage where the type of the key is either -// a SHA512 hash value or a string and the value is either a DHT -// block or a string. It is possiblle to mix any key/value types, -// but not used in this implementation. -type Store[K, V any] interface { - // Put value into storage under given key - Put(key K, val V) error - - // Get value with given key from storage - Get(key K) (V, error) - - // List all store keys - List() ([]K, error) -} - -//------------------------------------------------------------ -// Types for custom store requirements -//------------------------------------------------------------ - -// DHTStore for DHT queries and blocks -type DHTStore Store[blocks.Query, blocks.Block] - -// KVStore for key/value string pairs -type KVStore Store[string, string] - -//------------------------------------------------------------ -// NewDHTStore creates a new storage handler with given spec -// for use with DHT queries and blocks -func NewDHTStore(spec config.ParameterConfig) (DHTStore, error) { - // get the mode parameter - mode, ok := config.GetParam[string](spec, "mode") - if !ok { - return nil, ErrStoreInvalidSpec - } - switch mode { - //------------------------------------------------------------------ - // File-base storage - //------------------------------------------------------------------ - case "file": - return NewFileStore(spec) - } - return nil, ErrStoreUnknown -} - -//------------------------------------------------------------ -// NewKVStore creates a new storage handler with given spec -// for use with key/value string pairs. -func NewKVStore(spec config.ParameterConfig) (KVStore, error) { - // get the mode parameter - mode, ok := config.GetParam[string](spec, "mode") - if !ok { - return nil, ErrStoreInvalidSpec - } - switch mode { - //-------------------------------------------------------------- - // Redis service - //-------------------------------------------------------------- - case "redis": - return NewRedisStore(spec) - - //-------------------------------------------------------------- - // SQL database service - //-------------------------------------------------------------- - case "sql": - return NewSQLStore(spec) - } - return nil, errors.New("unknown storage mechanism") -} - -//------------------------------------------------------------ -// Filesystem-based storage -//------------------------------------------------------------ - -// FileHeader is the layout of a file managed by the storage handler. -// On start-up the file store recreates the list of file entries from -// traversing the actual filesystem. This is done in the background. -type FileHeader struct { - key string // storage key - size uint64 // size of file - btype uint16 // block type - stored util.AbsoluteTime // time added to store - expires util.AbsoluteTime // expiration time - lastUsed util.AbsoluteTime // time last used - usedCount uint64 // usage count -} - -// FileStore implements a filesystem-based storage mechanism for -// DHT queries and blocks. -type FileStore struct { - path string // storage path - cache bool // storage works as cache - args config.ParameterConfig // arguments / settings - - totalSize uint64 // total storage size (logical, not physical) - files map[string]*FileHeader // list of file headers - wrPos int // write position in cyclic list - mtx sync.Mutex // serialize operations (prune) -} - -// NewFileStore instantiates a new file storage. -func NewFileStore(spec config.ParameterConfig) (DHTStore, error) { - // get path parameter - path, ok := config.GetParam[string](spec, "path") - if !ok { - return nil, ErrStoreInvalidSpec - } - isCache, ok := config.GetParam[bool](spec, "cache") - if !ok { - isCache = false - } - // remove old cache content - if isCache { - os.RemoveAll(path) - } - // create file store handler - fs := &FileStore{ - path: path, - args: spec, - cache: isCache, - files: make(map[string]*FileHeader), - } - // load file header list - if !isCache { - if fp, err := os.Open(path + "/files.db"); err == nil { - dec := gob.NewDecoder(fp) - for { - hdr := new(FileHeader) - if dec.Decode(hdr) != nil { - if err != io.EOF { - return nil, err - } - break - } - fs.files[hdr.key] = hdr - fs.totalSize += hdr.size - } - fp.Close() - } - } - return fs, nil -} - -// Close file storage. write metadata to file -func (s *FileStore) Close() (err error) { - if !s.cache { - if fp, err := os.Create(s.path + "/files.db"); err == nil { - defer fp.Close() - enc := gob.NewEncoder(fp) - for _, hdr := range s.files { - if err = enc.Encode(hdr); err != nil { - break - } - } - } - } - return -} - -// Put block into storage under given key -func (s *FileStore) Put(query blocks.Query, block blocks.Block) (err error) { - // check for free space - if s.cache { - // caching is limited by explicit number of files - num, ok := config.GetParam[int](s.args, "num") - if !ok { - num = 100 - } - if len(s.files) >= num { - // make space for at least one new entry - s.prune(1) - } - } else { - // normal storage is limited by quota (default: 10GB) - max, ok := config.GetParam[int](s.args, "maxGB") - if !ok { - max = 10 - } - if int(s.totalSize>>30) > max { - // drop a significant number of blocks - s.prune(20) - } - } - // get query parameters for entry - var btype uint16 // block type - query.Get("blkType", &btype) - var expire util.AbsoluteTime // block expiration - query.Get("expire", &expire) - - // get path and filename from key - path, fname := s.expandPath(query.Key()) - // make sure the path exists - if err = os.MkdirAll(path, 0755); err != nil { - return - } - // write to file for storage - var fp *os.File - var fpSize int - if fp, err = os.Create(path + "/" + fname); err == nil { - defer fp.Close() - // write block data - if err = binary.Write(fp, binary.BigEndian, btype); err == nil { - if err = binary.Write(fp, binary.BigEndian, expire); err == nil { - _, err = fp.Write(block.Data()) - } - } - } - // add header to internal list - now := util.AbsoluteTimeNow() - hdr := &FileHeader{ - key: hex.EncodeToString(query.Key().Bits), - size: uint64(fpSize), - btype: btype, - expires: expire, - stored: now, - lastUsed: now, - usedCount: 1, - } - s.files[hdr.key] = hdr - return -} - -// Get block with given key from storage -func (s *FileStore) Get(query blocks.Query) (block blocks.Block, err error) { - // get requested block type - var ( - btype uint16 = blocks.DHT_BLOCK_ANY - blkt uint16 // actual block type - expire util.AbsoluteTime // expiration date - data []byte // block data - ) - query.Get("blkType", &btype) - - // get path and filename from key - path, fname := s.expandPath(query.Key()) - // read file content (block data) - var file *os.File - if file, err = os.Open(path + "/" + fname); err != nil { - return - } - // read block data - if err = binary.Read(file, binary.BigEndian, &blkt); err == nil { - if btype != blocks.DHT_BLOCK_ANY && btype != blkt { - // block types not matching - return - } - if err = binary.Read(file, binary.BigEndian, &expire); err == nil { - if data, err = ioutil.ReadAll(file); err == nil { - block = blocks.NewGenericBlock(data) - } - } - } - return -} - -// Get a list of all stored block keys (generic query). -func (s *FileStore) List() ([]blocks.Query, error) { - return make([]blocks.Query, 0), nil -} - -// expandPath returns the full path to the file for given key. -func (s *FileStore) expandPath(key *crypto.HashCode) (string, string) { - h := hex.EncodeToString(key.Bits) - return fmt.Sprintf("%s/%s/%s", s.path, h[:2], h[2:4]), h[4:] -} - -// Prune list of file headers so we drop at least n entries. -// returns number of removed entries. -func (s *FileStore) prune(n int) (del int) { - // get list of headers; remove expired entries on the fly - list := make([]*FileHeader, 0) - for key, hdr := range s.files { - // remove expired entry - if hdr.expires.Expired() { - s.dropFile(key) - del++ - } - // append to list - list = append(list, hdr) - } - // check if we are already done. - if del >= n { - return - } - // sort list by decending rate "(lifetime * size) / usedCount" - sort.Slice(list, func(i, j int) bool { - ri := (list[i].stored.Elapsed().Val * list[i].size) / list[i].usedCount - rj := (list[j].stored.Elapsed().Val * list[j].size) / list[j].usedCount - return ri > rj - }) - // remove from start of list until prune limit is reached - for _, hdr := range list { - s.dropFile(hdr.key) - del++ - if del == n { - break - } - } - return -} - -// drop file removes a file from the internal list and the physical storage. -func (s *FileStore) dropFile(key string) { - // remove for internal list - delete(s.files, key) - // remove from filesystem - path := fmt.Sprintf("%s/%s/%s/%s", s.path, key[:2], key[2:4], key[4:]) - if err := os.Remove(path); err != nil { - logger.Printf(logger.ERROR, "[store] can't remove file %s: %s", path, err.Error()) - return - } -} - -//------------------------------------------------------------ -// Redis: only use for caching purposes on key/value strings -//------------------------------------------------------------ - -// RedisStore uses a (local) Redis server for key/value storage -type RedisStore struct { - client *redis.Client // client connection - db int // index to database -} - -// NewRedisStore creates a Redis service client instance. -func NewRedisStore(spec config.ParameterConfig) (s KVStore, err error) { - // get connection parameters - addr, ok := config.GetParam[string](spec, "addr") - if !ok { - return nil, ErrStoreInvalidSpec - } - passwd, ok := config.GetParam[string](spec, "passwd") - if !ok { - passwd = "" - } - db, ok := config.GetParam[int](spec, "db") - if !ok { - return nil, ErrStoreInvalidSpec - } - - // create new Redis store - kvs := new(RedisStore) - kvs.db = db - kvs.client = redis.NewClient(&redis.Options{ - Addr: addr, - Password: passwd, - DB: db, - }) - if kvs.client == nil { - err = ErrStoreNotAvailable - } - s = kvs - return -} - -// Put block into storage under given key -func (s *RedisStore) Put(key string, value string) (err error) { - return s.client.Set(context.TODO(), key, value, 0).Err() -} - -// Get block with given key from storage -func (s *RedisStore) Get(key string) (value string, err error) { - return s.client.Get(context.TODO(), key).Result() -} - -// List all keys in store -func (s *RedisStore) List() (keys []string, err error) { - var ( - crs uint64 - segm []string - ctx = context.TODO() - ) - keys = make([]string, 0) - for { - segm, crs, err = s.client.Scan(ctx, crs, "*", 10).Result() - if err != nil { - return - } - if crs == 0 { - break - } - keys = append(keys, segm...) - } - return -} - -//------------------------------------------------------------ -// SQL-based key-value-store -//------------------------------------------------------------ - -// SQLStore for generic SQL database handling -type SQLStore struct { - db *util.DbConn -} - -// NewSQLStore creates a new SQL-based key/value store. -func NewSQLStore(spec config.ParameterConfig) (s KVStore, err error) { - // get connection parameters - connect, ok := config.GetParam[string](spec, "connect") - if !ok { - return nil, ErrStoreInvalidSpec - } - // create SQL store - kvs := new(SQLStore) - - // connect to SQL database - kvs.db, err = util.DbPool.Connect(connect) - if err != nil { - return nil, err - } - // get number of key/value pairs (as a check for existing table) - row := kvs.db.QueryRow("select count(*) from store") - var num int - if row.Scan(&num) != nil { - return nil, ErrStoreNotAvailable - } - return kvs, nil -} - -// Put a key/value pair into the store -func (s *SQLStore) Put(key string, value string) error { - _, err := s.db.Exec("insert into store(key,value) values(?,?)", key, value) - return err -} - -// Get a value for a given key from store -func (s *SQLStore) Get(key string) (value string, err error) { - row := s.db.QueryRow("select value from store where key=?", key) - err = row.Scan(&value) - return -} - -// List all keys in store -func (s *SQLStore) List() (keys []string, err error) { - var ( - rows *sql.Rows - key string - ) - keys = make([]string, 0) - rows, err = s.db.Query("select key from store") - if err == nil { - for rows.Next() { - if err = rows.Scan(&key); err != nil { - break - } - keys = append(keys, key) - } - } - return -} diff --git a/src/gnunet/service/store/database.go b/src/gnunet/service/store/database.go @@ -0,0 +1,186 @@ +// This file is part of gnunet-go, a GNUnet-implementation in Golang. +// Copyright (C) 2019-2022 Bernd Fix >Y< +// +// gnunet-go 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. +// +// gnunet-go 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/>. +// +// SPDX-License-Identifier: AGPL3.0-or-later + +package store + +import ( + "context" + "database/sql" + "fmt" + "gnunet/util" + "os" + "strings" + + _ "github.com/go-sql-driver/mysql" // init MySQL driver + _ "github.com/mattn/go-sqlite3" // init SQLite3 driver +) + +// Error messages related to databases +var ( + ErrSQLInvalidDatabaseSpec = fmt.Errorf("invalid database specification") + ErrSQLNoDatabase = fmt.Errorf("database not found") +) + +//---------------------------------------------------------------------- +// Connection to a database instance. There can be multiple connections +// on the same instance, managed by the database pool. +//---------------------------------------------------------------------- + +// DBConn is a database connection suitable for executing SQL commands. +type DBConn struct { + conn *sql.Conn // connection to database instance + key string // database connect string (identifier for pool) + engine string // database engine +} + +// Close database connection. +func (db *DBConn) Close() (err error) { + if err = db.conn.Close(); err != nil { + return + } + err = DBPool.remove(db.key) + return +} + +// QueryRow returns a single record for a query +func (db *DBConn) QueryRow(query string, args ...any) *sql.Row { + return db.conn.QueryRowContext(DBPool.ctx, query, args...) +} + +// Query returns all matching records for a query +func (db *DBConn) Query(query string, args ...any) (*sql.Rows, error) { + return db.conn.QueryContext(DBPool.ctx, query, args...) +} + +// Exec a SQL statement +func (db *DBConn) Exec(query string, args ...any) (sql.Result, error) { + return db.conn.ExecContext(DBPool.ctx, query, args...) +} + +// TODO: add more SQL methods + +//---------------------------------------------------------------------- +// DbPool holds all database instances used: Connecting with the same +// connect string returns the same instance. +//---------------------------------------------------------------------- + +// global instance for the database pool (singleton) +var ( + DBPool *dbPool +) + +// DBPoolEntry holds information about a database instance. +type DBPoolEntry struct { + db *sql.DB // reference to the database engine + refs int // number of open connections (reference count) + connect string // SQL connect string +} + +// package initialization +func init() { + // construct database pool + DBPool = new(dbPool) + DBPool.insts = util.NewMap[string, *DBPoolEntry]() + DBPool.ctx, DBPool.cancel = context.WithCancel(context.Background()) +} + +// dbPool keeps a mapping between connect string and database instance +type dbPool struct { + ctx context.Context // connection context + cancel context.CancelFunc // cancel function + insts *util.Map[string, *DBPoolEntry] // map of database instances +} + +// remove a database instance from the pool based on its connect string. +func (p *dbPool) remove(key string) error { + return p.insts.Process(func() (err error) { + // get pool entry + pe, ok := p.insts.Get(key) + if !ok { + return nil + } + // decrement ref count + pe.refs-- + if pe.refs == 0 { + // no more refs: close database + err = pe.db.Close() + p.insts.Delete(key) + } + return + }, false) +} + +// Connect to a SQL database (various types and flavors): +// The 'spec' option defines the arguments required to connect to a database; +// the meaning and format of the arguments depends on the specific SQL database. +// The arguments are separated by the '+' character; the first (and mandatory) +// argument defines the SQL database type. Other arguments depend on the value +// of this first argument. +// The following SQL types are implemented: +// * 'sqlite3': SQLite3-compatible database; the second argument specifies the +// file that holds the data (e.g. "sqlite3+/home/user/store.db") +// * 'mysql': A MySQL-compatible database; the second argument specifies the +// information required to log into the database (e.g. +// "[user[:passwd]@][proto[(addr)]]/dbname[?param1=value1&...]"). +func (p *dbPool) Connect(spec string) (db *DBConn, err error) { + err = p.insts.Process(func() error { + // check if we have a connection to this database. + db = new(DBConn) + inst, ok := p.insts.Get(spec) + if !ok { + inst = new(DBPoolEntry) + inst.refs = 0 + inst.connect = spec + + // No: create new database instance. + // split spec string into segments + specs := strings.Split(spec, ":") + if len(specs) < 2 { + return ErrSQLInvalidDatabaseSpec + } + // create database object + db.engine = specs[0] + switch db.engine { + case "sqlite3": + // check if the database file exists + var fi os.FileInfo + if fi, err = os.Stat(specs[1]); err != nil { + return ErrSQLNoDatabase + } + if fi.IsDir() { + return ErrSQLNoDatabase + } + // open the database file + inst.db, err = sql.Open("sqlite3", specs[1]) + case "mysql": + // just connect to the database + inst.db, err = sql.Open("mysql", specs[1]) + default: + return ErrSQLInvalidDatabaseSpec + } + // save database in pool + p.insts.Put(spec, inst) + } + // increment reference count + inst.refs++ + // return a new connection to the database. + db.conn, err = inst.db.Conn(p.ctx) + return err + }, false) + return +} diff --git a/src/gnunet/service/store/dhtstore_test.go b/src/gnunet/service/store/dhtstore_test.go @@ -0,0 +1,93 @@ +// This file is part of gnunet-go, a GNUnet-implementation in Golang. +// Copyright (C) 2019-2022 Bernd Fix >Y< +// +// gnunet-go 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. +// +// gnunet-go 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/>. +// +// SPDX-License-Identifier: AGPL3.0-or-later + +package store + +import ( + "encoding/hex" + "gnunet/crypto" + "gnunet/enums" + "gnunet/service/dht/blocks" + "gnunet/util" + "math/rand" + "testing" +) + +// test constants +const ( + fsNumBlocks = 10 +) + +// TestDHTFileStore generates 'fsNumBlocks' fully-random blocks +// and stores them under their SHA512 key. It than retrieves +// each block from storage and checks for matching hash. +func TestDHTFilesStore(t *testing.T) { + // test configuration + cfg := make(util.ParameterSet) + cfg["mode"] = "file" + cfg["cache"] = false + cfg["path"] = "/var/lib/gnunet/dht/store" + cfg["maxGB"] = 10 + + // create file store + fs, err := NewFileStore(cfg) + if err != nil { + t.Fatal(err) + } + // allocate keys + keys := make([]blocks.Query, 0, fsNumBlocks) + + // First round: save blocks + for i := 0; i < fsNumBlocks; i++ { + // generate random block + size := 1024 + rand.Intn(62000) + buf := make([]byte, size) + rand.Read(buf) + val := blocks.NewGenericBlock(buf) + // generate associated key + k := crypto.Hash(buf).Bits + key := blocks.NewGenericQuery(k, enums.BLOCK_TYPE_ANY, 0) + + // store block + if err := fs.Put(key, val); err != nil { + t.Fatal(err) + } + // remember key + keys = append(keys, key) + } + + // Second round: retrieve blocks and check + for _, key := range keys { + // get block + val, err := fs.Get(key) + if err != nil { + t.Fatal(err) + } + buf := val.Data() + + // re-create key + k := crypto.Hash(buf) + + // do the keys match? + if !k.Equals(key.Key()) { + t.Log(hex.EncodeToString(k.Bits)) + t.Log(hex.EncodeToString(key.Key().Bits)) + t.Fatal("key/value mismatch") + } + } +} diff --git a/src/gnunet/service/store/store.go b/src/gnunet/service/store/store.go @@ -0,0 +1,278 @@ +// This file is part of gnunet-go, a GNUnet-implementation in Golang. +// Copyright (C) 2019-2022 Bernd Fix >Y< +// +// gnunet-go 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. +// +// gnunet-go 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/>. +// +// SPDX-License-Identifier: AGPL3.0-or-later + +package store + +import ( + "context" + "database/sql" + _ "embed" // use embedded filesystem + "errors" + "fmt" + "gnunet/service/dht/blocks" + "gnunet/util" + + redis "github.com/go-redis/redis/v8" +) + +// Error messages related to the key/value-store implementations +var ( + ErrStoreInvalidSpec = fmt.Errorf("invalid Store specification") + ErrStoreUnknown = fmt.Errorf("unknown Store type") + ErrStoreNotAvailable = fmt.Errorf("store not available") + ErrStoreNoApprox = fmt.Errorf("no approx search for store defined") + ErrStoreNoList = fmt.Errorf("no key listing for store defined") +) + +//------------------------------------------------------------ +// Generic storage interface. Can be used for persistent or +// transient (caching) storage of key/value data. +//------------------------------------------------------------ + +// Store is a key/value storage where the type of the key is either +// a SHA512 hash value or a string and the value is either a DHT +// block or a string. It is possiblle to mix any key/value types, +// but not used in this implementation. +type Store[K, V any] interface { + // Put value into storage under given key + Put(key K, val V) error + + // Get value with given key from storage + Get(key K) (V, error) + + // GetApprox returns the best-matching value with given key from storage + // that is not excluded. + GetApprox(key K, excl func(V) bool) (V, any, error) + + // List all store keys + List() ([]K, error) + + // Close store + Close() error +} + +//------------------------------------------------------------ +// Types for custom store requirements +//------------------------------------------------------------ + +// DHTStore for DHT queries and blocks +type DHTStore Store[blocks.Query, blocks.Block] + +// KVStore for key/value string pairs +type KVStore Store[string, string] + +//------------------------------------------------------------ +// NewDHTStore creates a new storage handler with given spec +// for use with DHT queries and blocks +func NewDHTStore(spec util.ParameterSet) (DHTStore, error) { + // get the mode parameter + mode, ok := util.GetParam[string](spec, "mode") + if !ok { + return nil, ErrStoreInvalidSpec + } + switch mode { + //------------------------------------------------------------------ + // File-base storage + //------------------------------------------------------------------ + case "file": + return NewFileStore(spec) + } + return nil, ErrStoreUnknown +} + +//------------------------------------------------------------ +// NewKVStore creates a new storage handler with given spec +// for use with key/value string pairs. +func NewKVStore(spec util.ParameterSet) (KVStore, error) { + // get the mode parameter + mode, ok := util.GetParam[string](spec, "mode") + if !ok { + return nil, ErrStoreInvalidSpec + } + switch mode { + //-------------------------------------------------------------- + // Redis service + //-------------------------------------------------------------- + case "redis": + return NewRedisStore(spec) + + //-------------------------------------------------------------- + // SQL database service + //-------------------------------------------------------------- + case "sql": + return NewSQLStore(spec) + } + return nil, errors.New("unknown storage mechanism") +} + +//------------------------------------------------------------ +// Redis: only use for caching purposes on key/value strings +//------------------------------------------------------------ + +// RedisStore uses a (local) Redis server for key/value storage +type RedisStore struct { + client *redis.Client // client connection + db int // index to database +} + +// NewRedisStore creates a Redis service client instance. +func NewRedisStore(spec util.ParameterSet) (s KVStore, err error) { + // get connection parameters + addr, ok := util.GetParam[string](spec, "addr") + if !ok { + return nil, ErrStoreInvalidSpec + } + passwd, ok := util.GetParam[string](spec, "passwd") + if !ok { + passwd = "" + } + db, ok := util.GetParam[int](spec, "db") + if !ok { + return nil, ErrStoreInvalidSpec + } + + // create new Redis store + kvs := new(RedisStore) + kvs.db = db + kvs.client = redis.NewClient(&redis.Options{ + Addr: addr, + Password: passwd, + DB: db, + }) + if kvs.client == nil { + err = ErrStoreNotAvailable + } + s = kvs + return +} + +// Put value into storage under given key +func (s *RedisStore) Put(key string, value string) (err error) { + return s.client.Set(context.TODO(), key, value, 0).Err() +} + +// Get value with given key from storage +func (s *RedisStore) Get(key string) (value string, err error) { + return s.client.Get(context.TODO(), key).Result() +} + +// GetApprox returns the best-matching value for given key from storage +func (s *RedisStore) GetApprox(key string, crit func(string) bool) (value string, vkey any, err error) { + return "", "", ErrStoreNoApprox +} + +// List all keys in store +func (s *RedisStore) List() (keys []string, err error) { + var ( + crs uint64 + segm []string + ctx = context.TODO() + ) + keys = make([]string, 0) + for { + segm, crs, err = s.client.Scan(ctx, crs, "*", 10).Result() + if err != nil { + return + } + if crs == 0 { + break + } + keys = append(keys, segm...) + } + return +} + +// Close redis connection +func (s *RedisStore) Close() error { + return s.client.Close() +} + +//------------------------------------------------------------ +// SQL-based key-value-store +//------------------------------------------------------------ + +// SQLStore for generic SQL database handling +type SQLStore struct { + db *DBConn +} + +// NewSQLStore creates a new SQL-based key/value store. +func NewSQLStore(spec util.ParameterSet) (s KVStore, err error) { + // get connection parameters + connect, ok := util.GetParam[string](spec, "connect") + if !ok { + return nil, ErrStoreInvalidSpec + } + // create SQL store + kvs := new(SQLStore) + + // connect to SQL database + kvs.db, err = DBPool.Connect(connect) + if err != nil { + return nil, err + } + // get number of key/value pairs (as a check for existing table) + row := kvs.db.QueryRow("select count(*) from store") + var num int + if row.Scan(&num) != nil { + return nil, ErrStoreNotAvailable + } + return kvs, nil +} + +// Put a key/value pair into the store +func (s *SQLStore) Put(key string, value string) error { + _, err := s.db.Exec("insert into store(key,value) values(?,?)", key, value) + return err +} + +// Get a value for a given key from store +func (s *SQLStore) Get(key string) (value string, err error) { + row := s.db.QueryRow("select value from store where key=?", key) + err = row.Scan(&value) + return +} + +// GetApprox returns the best-matching value for given key from storage +func (s *SQLStore) GetApprox(key string, crit func(string) bool) (value string, vkey any, err error) { + return "", "", ErrStoreNoApprox +} + +// List all keys in store +func (s *SQLStore) List() (keys []string, err error) { + var ( + rows *sql.Rows + key string + ) + keys = make([]string, 0) + rows, err = s.db.Query("select key from store") + if err == nil { + for rows.Next() { + if err = rows.Scan(&key); err != nil { + break + } + keys = append(keys, key) + } + } + return +} + +// Close redis connection +func (s *SQLStore) Close() error { + return s.db.Close() +} diff --git a/src/gnunet/service/store/store_fs.go b/src/gnunet/service/store/store_fs.go @@ -0,0 +1,287 @@ +// This file is part of gnunet-go, a GNUnet-implementation in Golang. +// Copyright (C) 2019-2022 Bernd Fix >Y< +// +// gnunet-go 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. +// +// gnunet-go 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/>. +// +// SPDX-License-Identifier: AGPL3.0-or-later + +package store + +import ( + "encoding/hex" + "fmt" + "gnunet/service/dht/blocks" + "gnunet/util" + "io/ioutil" + "os" + + "github.com/bfix/gospel/logger" + "github.com/bfix/gospel/math" +) + +//============================================================ +// Filesystem-based storage +//============================================================ + +// FileStore implements a filesystem-based storage mechanism for +// DHT queries and blocks. +type FileStore struct { + path string // storage path + cache bool // storage works as cache + args util.ParameterSet // arguments / settings + totalSize uint64 // total storage size (logical, not physical) + + // storage-mode metadata + meta *FileMetaDB // database for metadata + maxSpace int // max. storage space in GB + + // cache-mode metadata + cacheMeta []*FileMetadata // cached metadata + wrPos int // write position in cyclic list + size int // size of cache (number of entries) +} + +// NewFileStore instantiates a new file storage. +func NewFileStore(spec util.ParameterSet) (DHTStore, error) { + // create file store handler + fs := new(FileStore) + fs.args = spec + + // get parameter + var ok bool + if fs.path, ok = util.GetParam[string](spec, "path"); !ok { + return nil, ErrStoreInvalidSpec + } + if fs.cache, ok = util.GetParam[bool](spec, "cache"); !ok { + fs.cache = false + } + + // setup file store depending on mode (storage/cache) + if fs.cache { + // remove old cache content + os.RemoveAll(fs.path) + // get number of cache entries + if fs.size, ok = util.GetParam[int](spec, "num"); !ok { + // defaults to 1000 entries + fs.size = 1000 + } + fs.cacheMeta = make([]*FileMetadata, fs.size) + } else { + // connect to metadata database + var err error + if fs.meta, err = OpenMetaDB(fs.path); err != nil { + return nil, err + } + // normal storage is limited by quota (default: 10GB) + if fs.maxSpace, ok = util.GetParam[int](spec, "maxGB"); !ok { + fs.maxSpace = 10 + } + } + return fs, nil +} + +// Close file storage. +func (s *FileStore) Close() (err error) { + if !s.cache { + // close database connection + err = s.meta.Close() + } + return +} + +// Put block into storage under given key +func (s *FileStore) Put(query blocks.Query, block blocks.Block) (err error) { + // check for free space + if !s.cache { + if int(s.totalSize>>30) > s.maxSpace { + // drop a significant number of blocks + s.prune(20) + } + } + // get parameters + btype := query.Type() + expire := block.Expire() + + // get path and filename from key + path, fname := s.expandPath(query.Key().Bits) + // make sure the path exists + if err = os.MkdirAll(path, 0755); err != nil { + return + } + // write to file for storage + var fp *os.File + bd := block.Data() + if fp, err = os.Create(path + "/" + fname); err == nil { + defer fp.Close() + // write block data + if _, err = fp.Write(bd); err != nil { + return + } + } + // compile metadata + now := util.AbsoluteTimeNow() + meta := &FileMetadata{ + key: query.Key().Bits, + size: uint64(len(bd)), + btype: btype, + expires: expire, + stored: now, + lastUsed: now, + usedCount: 1, + } + if s.cache { + // store in cyclic list + s.cacheMeta[s.wrPos] = meta + s.wrPos = (s.wrPos + 1) % s.size + } else { + // store metadata in database + if err = s.meta.Store(meta); err != nil { + return + } + // add to total storage size + s.totalSize += meta.size + } + return +} + +// Get block with given key from storage +func (s *FileStore) Get(query blocks.Query) (block blocks.Block, err error) { + // check if we have metadata for the query + key := query.Key().Bits + btype := query.Type() + var md *FileMetadata + if md, err = s.meta.Get(key, btype); err != nil || md == nil { + return + } + // check for expired entry + if md.expires.Expired() { + err = s.dropFile(md) + return + } + // mark the block as newly used + if err = s.meta.Used(key, btype); err != nil { + return + } + return s.readBlock(query.Key().Bits) +} + +// GetApprox returns the best-matching value with given key from storage +// that is not excluded +func (s *FileStore) GetApprox(query blocks.Query, excl func(blocks.Block) bool) (block blocks.Block, key any, err error) { + var bestKey []byte + var bestBlk blocks.Block + var bestDist *math.Int + // distance function + dist := func(a, b []byte) *math.Int { + c := make([]byte, len(a)) + for i := range a { + c[i] = a[i] ^ b[i] + } + return math.NewIntFromBytes(c) + } + // iterate over all keys + check := func(md *FileMetadata) { + // check for better match + d := dist(md.key, query.Key().Bits) + if bestKey == nil || d.Cmp(bestDist) < 0 { + // we might have a match. check block for exclusion + block, err = s.readBlock(md.key) + if err != nil { + logger.Printf(logger.ERROR, "[dhtstore] failed to retrieve blok for %s", hex.EncodeToString(md.key)) + return + } + if excl(block) { + return + } + // remember best match + bestKey = md.key + bestBlk = block + bestDist = d + } + } + if err = s.meta.Traverse(check); err != nil { + return + } + if bestBlk != nil { + // mark the block as newly used + if err = s.meta.Used(bestKey, bestBlk.Type()); err != nil { + return + } + } + return bestBlk, bestDist, nil +} + +// Get a list of all stored block keys (generic query). +func (s *FileStore) List() ([]blocks.Query, error) { + return nil, ErrStoreNoList +} + +// read block from storage for given key +func (s *FileStore) readBlock(key []byte) (block blocks.Block, err error) { + // get path and filename from key + path, fname := s.expandPath(key) + // read file content (block data) + var file *os.File + if file, err = os.Open(path + "/" + fname); err != nil { + return + } + defer file.Close() + // read block data + var data []byte + if data, err = ioutil.ReadAll(file); err == nil { + block = blocks.NewGenericBlock(data) + } + return +} + +// expandPath returns the full path to the file for given key. +func (s *FileStore) expandPath(key []byte) (string, string) { + h := hex.EncodeToString(key) + return fmt.Sprintf("%s/%s/%s", s.path, h[:2], h[2:4]), h[4:] +} + +// Prune list of file headers so we drop at least n entries. +// returns number of removed entries. +func (s *FileStore) prune(n int) (del int) { + // collect obsolete records + obsolete, err := s.meta.Obsolete(n) + if err != nil { + logger.Println(logger.ERROR, "[FileStore] failed to collect obsolete records: "+err.Error()) + return + } + for _, md := range obsolete { + if err := s.dropFile(md); err != nil { + return + } + del++ + } + return +} + +// drop file removes a file from metadatabase and the physical storage. +func (s *FileStore) dropFile(md *FileMetadata) (err error) { + // adjust total size + s.totalSize -= md.size + // remove from database + if err = s.meta.Drop(md.key, md.btype); err != nil { + logger.Printf(logger.ERROR, "[store] can't remove metadata (%s,%d): %s", md.key, md.btype, err.Error()) + return + } + // remove from filesystem + path := fmt.Sprintf("%s/%s/%s/%s", s.path, md.key[:2], md.key[2:4], md.key[4:]) + if err = os.Remove(path); err != nil { + logger.Printf(logger.ERROR, "[store] can't remove file %s: %s", path, err.Error()) + } + return +} diff --git a/src/gnunet/service/store/store_fs_meta.go b/src/gnunet/service/store/store_fs_meta.go @@ -0,0 +1,174 @@ +// This file is part of gnunet-go, a GNUnet-implementation in Golang. +// Copyright (C) 2019-2022 Bernd Fix >Y< +// +// gnunet-go 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. +// +// gnunet-go 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/>. +// +// SPDX-License-Identifier: AGPL3.0-or-later + +package store + +import ( + "database/sql" + _ "embed" + "gnunet/util" + "os" +) + +//============================================================ +// Metadata handling for file storage +//============================================================ + +// FileMetadata holds information about a file (raw block data) +// and is stored in a SQL database for faster access. +type FileMetadata struct { + key []byte // storage key + size uint64 // size of file + btype uint16 // block type + stored util.AbsoluteTime // time added to store + expires util.AbsoluteTime // expiration time + lastUsed util.AbsoluteTime // time last used + usedCount uint64 // usage count +} + +//------------------------------------------------------------ +// Metadata database: A SQLite3 database to hold metadata about +// blocks in file storage +//------------------------------------------------------------ + +//go:embed store_fs_meta.sql +var initScript []byte + +// FileMetaDB is a SQLite3 database for block metadata +type FileMetaDB struct { + conn *DBConn // database connection +} + +// OpenMetaDB opens a metadata database in the given path. The name of the +// database is "access.db". +func OpenMetaDB(path string) (db *FileMetaDB, err error) { + // connect to database + dbFile := path + "/acccess.db" + if _, err = os.Stat(path + "/acccess.db"); err != nil { + var file *os.File + if file, err = os.Create(dbFile); err != nil { + return + } + file.Close() + } + db = new(FileMetaDB) + if db.conn, err = DBPool.Connect("sqlite3:" + dbFile); err != nil { + return + } + // check for initialized database + res := db.conn.QueryRow("select name from sqlite_master where type='table' and name='meta'") + var s string + if res.Scan(&s) != nil { + // initialize database + if _, err = db.conn.Exec(string(initScript)); err != nil { + return + } + } + return +} + +// Store metadata in database: creates or updates a record for the metadata +// in the database; primary key is the query key +func (db *FileMetaDB) Store(md *FileMetadata) (err error) { + sql := "replace into meta(qkey,btype,size,stored,expires,lastUsed,usedCount) values(?,?,?,?,?,?,?)" + _, err = db.conn.Exec(sql, md.key, md.btype, md.size, md.stored.Epoch(), md.expires.Epoch(), md.lastUsed.Epoch(), md.usedCount) + return +} + +// Get block metadata from database +func (db *FileMetaDB) Get(key []byte, btype uint16) (md *FileMetadata, err error) { + md = new(FileMetadata) + md.key = util.Clone(key) + md.btype = btype + stmt := "select size,stored,expires,lastUsed,usedCount from meta where qkey=? and btype=?" + row := db.conn.QueryRow(stmt, key, btype) + var st, exp, lu uint64 + if err = row.Scan(&md.size, &st, &exp, &lu, &md.usedCount); err == sql.ErrNoRows { + md = nil + err = nil + } else { + md.stored.Val = st * 1000000 + md.expires.Val = exp * 1000000 + md.lastUsed.Val = lu * 1000000 + } + return +} + +// Drop metadata for block from database +func (db *FileMetaDB) Drop(key []byte, btype uint16) error { + _, err := db.conn.Exec("delete from meta where qkey=? and btype=?", key, btype) + return err +} + +// Used a block from store: increment usage count and lastUsed time. +func (db *FileMetaDB) Used(key []byte, btype uint16) error { + _, err := db.conn.Exec("update meta set usedCount=usedCount+1,lastUsed=unixepoch() where qkey=? and btype=?", key, btype) + return err +} + +// Obsolete collects records from the meta database that are considered +// "removable". Entries are rated by the value of "(lifetime * size) / usedCount" +func (db *FileMetaDB) Obsolete(n int) (removable []*FileMetadata, err error) { + // get obsolete records from database + rate := "(unixepoch()-unixepoch(stored))*size/usedCount" + stmt := "select qkey,btype from meta order by " + rate + " limit ?" + var rows *sql.Rows + if rows, err = db.conn.Query(stmt, n); err != nil { + return + } + var md *FileMetadata + for rows.Next() { + var st, exp, lu uint64 + if err = rows.Scan(&md.key, &md.btype, &md.size, &st, &exp, &lu, &md.usedCount); err != nil { + return + } + md.stored.Val = st * 1000000 + md.expires.Val = exp * 1000000 + md.lastUsed.Val = lu * 1000000 + removable = append(removable, md) + } + return +} + +// Traverse metadata records and call function on each record +func (db *FileMetaDB) Traverse(f func(*FileMetadata)) error { + sql := "select qkey,btype,size,stored,expires,lastUsed,usedCount from meta" + rows, err := db.conn.Query(sql) + if err != nil { + return err + } + md := new(FileMetadata) + for rows.Next() { + var st, exp, lu uint64 + err = rows.Scan(&md.key, &md.btype, &md.size, &st, &exp, &lu, &md.usedCount) + if err != nil { + return err + } + md.stored.Val = st * 1000000 + md.expires.Val = exp * 1000000 + md.lastUsed.Val = lu * 1000000 + // call process function + f(md) + } + return nil +} + +// Close metadata database +func (db *FileMetaDB) Close() error { + return db.conn.Close() +} diff --git a/src/gnunet/service/store/store_fs_meta.sql b/src/gnunet/service/store/store_fs_meta.sql @@ -0,0 +1,29 @@ +-- This file is part of gnunet-go, a GNUnet-implementation in Golang. +-- Copyright (C) 2019-2022 Bernd Fix >Y< +-- +-- gnunet-go 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. +-- +-- gnunet-go 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/>. +-- +-- SPDX-License-Identifier: AGPL3.0-or-later + +create table meta ( + qkey blob, -- key (SHA512 hash) + btype integer, -- block type + size integer, -- size of file + stored integer, -- time added to store + expires integer, -- expiration time + lastUsed integer, -- time last used + usedCount integer, -- usage count + + unique(qkey,btype) -- unique key in database +); diff --git a/src/gnunet/test/gnunet-dhtu/main.go b/src/gnunet/test/gnunet-dhtu/main.go @@ -1,206 +0,0 @@ -// This file is part of gnunet-go, a GNUnet-implementation in Golang. -// Copyright (C) 2022 Bernd Fix >Y< -// -// gnunet-go 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. -// -// gnunet-go 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/>. -// -// SPDX-License-Identifier: AGPL3.0-or-later - -package main - -import ( - "context" - "flag" - "fmt" - "gnunet/config" - "gnunet/core" - "gnunet/message" - "gnunet/service" - "gnunet/service/dht" - "gnunet/transport" - "gnunet/util" - "log" - "net/rpc" - "time" - - "github.com/bfix/gospel/logger" -) - -//---------------------------------------------------------------------- -// Test Go node with DHTU GNUnet nodes -// -// N.B.: THIS TEST ONLY COVERS THE BASIC MESSAGE EXCHANGE LEVEL; NO -// MESSAGE PROCESSING EXCEPT FOR HELLO MESSAGES WILL TAKE PLACE. -//---------------------------------------------------------------------- - -func main() { - // handle command-line arguments - var remoteAddr string - var cfgFile string - flag.StringVar(&cfgFile, "c", "gnunet-config.json", "configuration file") - flag.StringVar(&remoteAddr, "a", "", "address of remote node") - flag.Parse() - - // read configuration file and set missing arguments. - if err := config.ParseConfig(cfgFile); err != nil { - logger.Printf(logger.ERROR, "[node] Invalid configuration file: %s\n", err.Error()) - return - } - - // convert arguments - var rAddr *util.Address - var err error - if rAddr, err = util.ParseAddress(remoteAddr); err != nil { - logger.Println(logger.ERROR, err.Error()) - return - } - - // setup execution context - ctx, cancel := context.WithCancel(context.Background()) - defer func() { - cancel() - time.Sleep(time.Second) - }() - - // create and run node - node, err := NewTestNode(ctx) - if err != nil { - logger.Println(logger.ERROR, err.Error()) - return - } - defer node.Shutdown() - - // show our HELLO URL - ep := config.Cfg.Local.Endpoints[0] - as := fmt.Sprintf("%s://%s:%d", ep.Network, ep.Address, ep.Port) - listen, err := util.ParseAddress(as) - if err != nil { - logger.Println(logger.ERROR, err.Error()) - return - } - aList := []*util.Address{listen} - logger.Println(logger.INFO, "[node] --> "+node.HelloURL(aList)) - - // send HELLO to bootstrap address - if err = node.SendHello(ctx, rAddr); err != nil && err != transport.ErrEndpMaybeSent { - logger.Println(logger.ERROR, "[node] failed to send HELLO: "+err.Error()) - return - } - - // run forever - var ch chan struct{} - <-ch -} - -//---------------------------------------------------------------------- -// create and run a node with given spec -//---------------------------------------------------------------------- - -type TestNode struct { - id int - peer *core.Peer - core *core.Core - addr *util.Address -} - -func (n *TestNode) Shutdown() { - n.core.Shutdown() -} -func (n *TestNode) HelloURL(a []*util.Address) string { - hd, err := n.peer.HelloData(message.HelloAddressExpiration, a) - if err != nil { - return "" - } - return hd.URL() -} - -func (n *TestNode) SendHello(ctx context.Context, addr *util.Address) error { - return n.core.SendHello(ctx, addr) -} - -func NewTestNode(ctx context.Context) (node *TestNode, err error) { - - // create test node - node = new(TestNode) - node.id = util.NextID() - - // create core service - if node.core, err = core.NewCore(ctx, config.Cfg.Local); err != nil { - return - } - node.peer = node.core.Peer() - logger.Printf(logger.INFO, "[node] Node %s starting", node.peer.GetID()) - - // start a new DHT service - dht, err := dht.NewService(ctx, node.core) - if err != nil { - log.Fatal(err) - } - - // start JSON-RPC server on request - var rpc *rpc.Server - if rpc, err = service.StartRPC(ctx, config.Cfg.RPC.Endpoint); err != nil { - logger.Printf(logger.ERROR, "[node] RPC failed to start: %s", err.Error()) - return - } - dht.InitRPC(rpc) - - // start listening on the network - list, err := node.core.Addresses() - if err != nil { - log.Fatal(err) - } - for _, addr := range list { - s := addr.Network() + "://" + addr.String() - if node.addr, err = util.ParseAddress(s); err != nil { - continue - } - logger.Printf(logger.INFO, "[node] Listening on %s", s) - } - - // register as event listener - incoming := make(chan *core.Event) - node.core.Register(config.Cfg.Local.Name, core.NewListener(incoming, nil)) - - // heart beat - tick := time.NewTicker(5 * time.Minute) - - // run event handler - go func() { - for { - select { - // show incoming event - case ev := <-incoming: - switch ev.ID { - case core.EV_CONNECT: - logger.Printf(logger.INFO, "[node] <<< Peer %s connected", ev.Peer) - case core.EV_DISCONNECT: - logger.Printf(logger.INFO, "[node] <<< Peer %s diconnected", ev.Peer) - case core.EV_MESSAGE: - logger.Printf(logger.INFO, "[node] <<< Msg from %s of type %d", ev.Peer, ev.Msg.Header().MsgType) - logger.Printf(logger.INFO, "[node] <<< --> %s", ev.Msg.String()) - } - - // handle termination signal - case <-ctx.Done(): - logger.Println(logger.INFO, "[node] Shutting down node") - return - - // handle heart beat - case now := <-tick.C: - logger.Printf(logger.INFO, "[node] Heart beat at %s", now.String()) - } - } - }() - return -} diff --git a/src/gnunet/transport/endpoint.go b/src/gnunet/transport/endpoint.go @@ -39,7 +39,7 @@ var ( ErrEndpExists = errors.New("endpoint exists") ErrEndpNoAddress = errors.New("no address for endpoint") ErrEndpNoConnection = errors.New("no connection on endpoint") - ErrEndpMaybeSent = errors.New("message may have been sent - cant know") + ErrEndpMaybeSent = errors.New("message may have been sent - can't know") ErrEndpWriteShort = errors.New("write too short") ) @@ -48,10 +48,10 @@ var ( // remote endpoints for TCP and UDP traffic. type Endpoint interface { // Run the endpoint and send received messages to channel - Run(context.Context, chan *TransportMessage) error + Run(context.Context, chan *Message) error // Send message on endpoint - Send(context.Context, net.Addr, *TransportMessage) error + Send(context.Context, net.Addr, *Message) error // Address returns the listening address for the endpoint Address() net.Addr @@ -84,16 +84,17 @@ func NewEndpoint(addr net.Addr) (ep Endpoint, err error) { // PacketEndpoint for packet-oriented network protocols type PaketEndpoint struct { + sync.Mutex + id int // endpoint identifier netw string // network identifier ("udp", "udp4", "udp6", ...) addr net.Addr // endpoint address conn net.PacketConn // packet connection buf []byte // buffer for read/write operations - mtx sync.Mutex // mutex for send operations } // Run packet endpoint: send incoming messages to the handler. -func (ep *PaketEndpoint) Run(ctx context.Context, hdlr chan *TransportMessage) (err error) { +func (ep *PaketEndpoint) Run(ctx context.Context, hdlr chan *Message) (err error) { // create listener var lc net.ListenConfig xproto := ep.addr.Network() @@ -144,7 +145,7 @@ func (ep *PaketEndpoint) Run(ctx context.Context, hdlr chan *TransportMessage) ( } // Read a transport message from endpoint based on extended protocol -func (ep *PaketEndpoint) read() (tm *TransportMessage, err error) { +func (ep *PaketEndpoint) read() (tm *Message, err error) { // read next packet (assuming that it contains one complete message) var n int if n, _, err = ep.conn.ReadFrom(ep.buf); err != nil { @@ -167,7 +168,7 @@ func (ep *PaketEndpoint) read() (tm *TransportMessage, err error) { panic(ErrEndpProtocolUnknown) } // return transport message - return &TransportMessage{ + return &Message{ Peer: peer, Msg: msg, Resp: nil, @@ -176,10 +177,10 @@ func (ep *PaketEndpoint) read() (tm *TransportMessage, err error) { } // Send message to address from endpoint -func (ep *PaketEndpoint) Send(ctx context.Context, addr net.Addr, msg *TransportMessage) (err error) { +func (ep *PaketEndpoint) Send(ctx context.Context, addr net.Addr, msg *Message) (err error) { // only one sender at a time - ep.mtx.Lock() - defer ep.mtx.Unlock() + ep.Lock() + defer ep.Unlock() // check for valid connection if ep.conn == nil { @@ -284,7 +285,7 @@ type StreamEndpoint struct { } // Run packet endpoint: send incoming messages to the handler. -func (ep *StreamEndpoint) Run(ctx context.Context, hdlr chan *TransportMessage) (err error) { +func (ep *StreamEndpoint) Run(ctx context.Context, hdlr chan *Message) (err error) { // create listener var lc net.ListenConfig xproto := ep.addr.Network() @@ -331,7 +332,7 @@ func (ep *StreamEndpoint) Run(ctx context.Context, hdlr chan *TransportMessage) } // Read a transport message from endpoint based on extended protocol -func (ep *StreamEndpoint) read(ctx context.Context, conn net.Conn) (tm *TransportMessage, err error) { +func (ep *StreamEndpoint) read(ctx context.Context, conn net.Conn) (tm *Message, err error) { // parse transport message based on extended protocol var ( peer *util.PeerID @@ -341,7 +342,7 @@ func (ep *StreamEndpoint) read(ctx context.Context, conn net.Conn) (tm *Transpor case "ip+udp": // parse peer id peer = util.NewPeerID(nil) - if _, err = conn.Read(peer.Key); err != nil { + if _, err = conn.Read(peer.Data); err != nil { return } // read next message from connection @@ -352,14 +353,14 @@ func (ep *StreamEndpoint) read(ctx context.Context, conn net.Conn) (tm *Transpor panic(ErrEndpProtocolUnknown) } // return transport message - return &TransportMessage{ + return &Message{ Peer: peer, Msg: msg, }, nil } // Send message to address from endpoint -func (ep *StreamEndpoint) Send(ctx context.Context, addr net.Addr, msg *TransportMessage) error { +func (ep *StreamEndpoint) Send(ctx context.Context, addr net.Addr, msg *Message) error { return nil } diff --git a/src/gnunet/transport/reader_writer.go b/src/gnunet/transport/reader_writer.go @@ -54,12 +54,8 @@ func WriteMessage(ctx context.Context, wrt io.WriteCloser, msg message.Message) } // watch dog for write operation go func() { - for { - select { - case <-ctx.Done(): - wrt.Close() - } - } + <-ctx.Done() + wrt.Close() }() // perform write operation var n int @@ -86,46 +82,41 @@ func ReadMessageDirect(rdr io.Reader, buf []byte) (msg message.Message, err erro func ReadMessage(ctx context.Context, rdr io.ReadCloser, buf []byte) (msg message.Message, err error) { // watch dog for write operation go func() { - for { - select { - case <-ctx.Done(): - rdr.Close() - } - } + <-ctx.Done() + rdr.Close() }() // get bytes from reader if buf == nil { buf = make([]byte, 65536) } - get := func(pos, count int) error { - n, err := rdr.Read(buf[pos : pos+count]) - if err == nil && n != count { + get := func(pos, count int) (err error) { + var n int + if n, err = rdr.Read(buf[pos : pos+count]); err == nil && n != count { err = fmt.Errorf("not enough bytes on reader (%d of %d)", n, count) } return err } // read header first - if err := get(0, 4); err != nil { - return nil, err + if err = get(0, 4); err != nil { + return } var mh *message.Header if mh, err = message.GetMsgHeader(buf[:4]); err != nil { - return nil, err + return } // get rest of message if err = get(4, int(mh.MsgSize)-4); err != nil { - return nil, err + return } if msg, err = message.NewEmptyMessage(mh.MsgType); err != nil { - return nil, err + return } if msg == nil { - return nil, fmt.Errorf("message{%d} is nil", mh.MsgType) - } - if err = data.Unmarshal(msg, buf[:mh.MsgSize]); err != nil { - return nil, err + err = fmt.Errorf("message{%d} is nil", mh.MsgType) + return } - return msg, nil + err = data.Unmarshal(msg, buf[:mh.MsgSize]) + return } //---------------------------------------------------------------------- diff --git a/src/gnunet/transport/responder.go b/src/gnunet/transport/responder.go @@ -32,6 +32,9 @@ import ( type Responder interface { // Handle outgoing message Send(ctx context.Context, msg message.Message) error + + // Receiver returns the receiving peer (string representation) + Receiver() string } //---------------------------------------------------------------------- @@ -50,3 +53,8 @@ func (r *TransportResponder) Send(ctx context.Context, msg message.Message) erro } return r.SendFcn(ctx, r.Peer, msg) } + +// Receiver returns the receiving peer id +func (r *TransportResponder) Receiver() string { + return r.Peer.String() +} diff --git a/src/gnunet/transport/transport.go b/src/gnunet/transport/transport.go @@ -32,17 +32,18 @@ import ( // Trnsport layer error codes var ( ErrTransNoEndpoint = errors.New("no matching endpoint found") + ErrTransNoUPNP = errors.New("no UPnP available") ) //====================================================================== // Network-oriented transport implementation //====================================================================== -// TransportMessage is the unit processed by the transport mechanism. +// Message is the unit processed by the transport mechanism. // Peer refers to the remote endpoint (sender/receiver) and // Msg is the exchanged GNUnet message. The packet itself satisfies the // message.Message interface. -type TransportMessage struct { +type Message struct { // Peer is a identifier for a remote peer Peer *util.PeerID @@ -62,10 +63,10 @@ type TransportMessage struct { } // Bytes returns the binary representation of a transport message -func (msg *TransportMessage) Bytes() ([]byte, error) { +func (msg *Message) Bytes() ([]byte, error) { buf := new(bytes.Buffer) // serialize peer id - if _, err := buf.Write(msg.Peer.Key); err != nil { + if _, err := buf.Write(msg.Peer.Bytes()); err != nil { return nil, err } // serialize message @@ -74,16 +75,16 @@ func (msg *TransportMessage) Bytes() ([]byte, error) { } // String returns the message in human-readable form -func (msg *TransportMessage) String() string { +func (msg *Message) String() string { return "TransportMessage{...}" } // NewTransportMessage creates a message suitable for transfer -func NewTransportMessage(peer *util.PeerID, msg message.Message) (tm *TransportMessage) { +func NewTransportMessage(peer *util.PeerID, msg message.Message) (tm *Message) { if peer == nil { peer = util.NewPeerID(nil) } - tm = &TransportMessage{ + tm = &Message{ Peer: peer, Msg: msg, Resp: nil, @@ -97,13 +98,13 @@ func NewTransportMessage(peer *util.PeerID, msg message.Message) (tm *TransportM // Transport enables network-oriented (like IP, UDP, TCP or UDS) // message exchange on multiple endpoints. type Transport struct { - incoming chan *TransportMessage // messages as received from the network + incoming chan *Message // messages as received from the network endpoints *util.Map[int, Endpoint] // list of available endpoints upnp *network.PortMapper // UPnP mapper (optional) } // NewTransport creates and runs a new transport layer implementation. -func NewTransport(ctx context.Context, tag string, ch chan *TransportMessage) (t *Transport) { +func NewTransport(ctx context.Context, tag string, ch chan *Message) (t *Transport) { // create transport instance mngr, err := network.NewPortMapper(tag) if err != nil { @@ -124,7 +125,7 @@ func (t *Transport) Shutdown() { } // Send a message over suitable endpoint -func (t *Transport) Send(ctx context.Context, addr net.Addr, msg *TransportMessage) (err error) { +func (t *Transport) Send(ctx context.Context, addr net.Addr, msg *Message) (err error) { // select best endpoint able to handle address var bestEp Endpoint err = t.endpoints.ProcessRange(func(_ int, ep Endpoint) error { @@ -174,7 +175,7 @@ func (t *Transport) AddEndpoint(ctx context.Context, addr *util.Address) (ep End } // add endpoint to list and run it t.endpoints.Put(ep.ID(), ep) - ep.Run(ctx, t.incoming) + err = ep.Run(ctx, t.incoming) return } @@ -185,11 +186,20 @@ func (t *Transport) AddEndpoint(ctx context.Context, addr *util.Address) (ep End // ForwardOpen returns a local address for listening that will receive traffic // from a port forward handled by UPnP on the router. func (t *Transport) ForwardOpen(protocol, param string, port int) (id, local, remote string, err error) { + // check for available UPnP + if t.upnp == nil { + err = ErrTransNoUPNP + return + } // no parameters currently defined, so just do the assignment. return t.upnp.Assign(protocol, port) } // ForwardClose closes a specific port forwarding func (t *Transport) ForwardClose(id string) error { + // check for available UPnP + if t.upnp == nil { + return ErrTransNoUPNP + } return t.upnp.Unassign(id) } diff --git a/src/gnunet/util/address.go b/src/gnunet/util/address.go @@ -27,10 +27,10 @@ import ( // Address specifies how a peer is reachable on the network. type Address struct { - Netw string `` // network protocol - Options uint32 `order:"big"` // address options - Expires AbsoluteTime `` // expiration date for address - Address []byte `size:"*"` // address data (protocol-dependent) + Netw string // network protocol + Options uint32 // address options + Expires AbsoluteTime // expiration date for address + Address []byte // address data (protocol-dependent) } // NewAddress returns a new Address for the given transport and specs @@ -43,6 +43,8 @@ func NewAddress(transport string, addr string) *Address { } } +// NewAddressWrap returns new address from net.Addr with no options +// or expiry date. func NewAddressWrap(addr net.Addr) *Address { return &Address{ Netw: addr.Network(), @@ -53,7 +55,7 @@ func NewAddressWrap(addr net.Addr) *Address { } // ParseAddress translates a GNUnet address string like -// "r5n+ip+udp://1.2.3.4:6789" or "gnunet+tcp://12.3.4.5/". +// "ip+udp://1.2.3.4:6789" or "gnunet+tcp://12.3.4.5/". // It can also handle standard strings like "udp:127.0.0.1:6735". func ParseAddress(s string) (addr *Address, err error) { p := strings.SplitN(s, ":", 2) @@ -72,11 +74,6 @@ func (a *Address) Equals(b *Address) bool { bytes.Equal(a.Address, b.Address) } -// StringAll returns a human-readable representation of an address. -func (a *Address) StringAll() string { - return a.Netw + "://" + string(a.Address) -} - // implement net.Addr interface methods: // String returns a human-readable representation of an address. @@ -91,7 +88,7 @@ func (a *Address) Network() string { //---------------------------------------------------------------------- -// URI returns a string representaion of an address. +// URI returns a string representation of an address. func (a *Address) URI() string { return URI(a.Netw, a.Address) } @@ -101,24 +98,6 @@ func URI(network string, addr []byte) string { //---------------------------------------------------------------------- -// IPAddress (can be IPv4 or IPv6 or a DNS name) -type IPAddress struct { - Host []byte `size:"*-2"` - Port uint16 `order:"big"` -} - -// NewIPAddress creates a new instance for a given host and port. -func NewIPAddress(host []byte, port uint16) *IPAddress { - ip := &IPAddress{ - Host: make([]byte, len(host)), - Port: port, - } - copy(ip.Host, host) - return ip -} - -//---------------------------------------------------------------------- - // PeerAddrList is a list of addresses per peer ID. type PeerAddrList struct { list *Map[string, []*Address] @@ -133,12 +112,13 @@ func NewPeerAddrList() *PeerAddrList { // Add address for peer. The returned mode is 0=not added, 1=new peer, // 2=new address -func (a *PeerAddrList) Add(id string, addr *Address) (mode int) { +func (a *PeerAddrList) Add(peer *PeerID, addr *Address) (mode int) { // check for expired address. mode = 0 if !addr.Expires.Expired() { // run add operation - a.list.Process(func() error { + _ = a.list.Process(func() error { + id := peer.String() list, ok := a.list.Get(id) if !ok { list = make([]*Address, 0) @@ -160,7 +140,8 @@ func (a *PeerAddrList) Add(id string, addr *Address) (mode int) { } // Get address for peer -func (a *PeerAddrList) Get(id string, transport string) (res []*Address) { +func (a *PeerAddrList) Get(peer *PeerID, transport string) (res []*Address) { + id := peer.String() list, ok := a.list.Get(id) if ok { for _, addr := range list { @@ -181,6 +162,13 @@ func (a *PeerAddrList) Get(id string, transport string) (res []*Address) { } // Delete a list entry by key. -func (a *PeerAddrList) Delete(id string) { - a.list.Delete(id) +func (a *PeerAddrList) Delete(peer *PeerID) { + a.list.Delete(peer.String()) +} + +// Contains checks if a peer is contained in the list. Does not check +// for expired entries. +func (a *PeerAddrList) Contains(peer *PeerID) (ok bool) { + _, ok = a.list.Get(peer.String()) + return } diff --git a/src/gnunet/util/address_test.go b/src/gnunet/util/address_test.go @@ -37,17 +37,24 @@ func TestAddrList(t *testing.T) { t.Fatal(err) } } + // test peer + peer := NewPeerID(nil) // allocate AddrList addrL := NewPeerAddrList() for _, addr := range addrA { - rc := addrL.Add("2BHV4BN8736W5W3CJNXY2S9WABWTGH35QMFG4BPCWBH7DNBCFC60", addr) + rc := addrL.Add(peer, addr) t.Logf("added %s (%d)", addr.URI(), rc) } // check list t.Log("checking list...") - list := addrL.Get("2BHV4BN8736W5W3CJNXY2S9WABWTGH35QMFG4BPCWBH7DNBCFC60", "ip+udp") - t.Logf("got: %v", list) + list := addrL.Get(peer, "ip+udp") + for i, addr := range list { + t.Logf("got: %s", addr.URI()) + if addr != addrA[i] { + t.Errorf("address mismatch at index %d", i) + } + } if len(list) != len(addrS) { t.Fatal("list size not matching") } diff --git a/src/gnunet/util/array.go b/src/gnunet/util/array.go @@ -33,6 +33,11 @@ var ( // Clone creates a new array of same content as the argument. func Clone[T []E, E any](d T) T { + // handle nil slices + if d == nil { + return nil + } + // create copy r := make(T, len(d)) copy(r, d) return r diff --git a/src/gnunet/util/base32.go b/src/gnunet/util/base32.go @@ -46,9 +46,9 @@ const xlate = "0123456789ABCDEFGHJKMNPQRSTVWXYZ" var ( // ErrInvalidEncoding signals an invalid encoding - ErrInvalidEncoding = errors.New("Invalid encoding") + ErrInvalidEncoding = errors.New("invalid encoding") // ErrBufferTooSmall signalsa too small buffer for decoding - ErrBufferTooSmall = errors.New("Buffer to small") + ErrBufferTooSmall = errors.New("buffer to small") ) // EncodeBinaryToString encodes a byte array into a string. diff --git a/src/gnunet/util/base32_test.go b/src/gnunet/util/base32_test.go @@ -73,7 +73,7 @@ func TestBase32Preset(t *testing.T) { if err != nil { t.Fatal(err) } - if bytes.Compare(x.bin, e) != 0 { + if !bytes.Equal(x.bin, e) { t.Fatalf("Decoding mismatch: '%s' != '%s' for '%s'\n", hex.EncodeToString(e), hex.EncodeToString(x.bin), x.str) } } diff --git a/src/gnunet/util/database.go b/src/gnunet/util/database.go @@ -1,185 +0,0 @@ -// This file is part of gnunet-go, a GNUnet-implementation in Golang. -// Copyright (C) 2019-2022 Bernd Fix >Y< -// -// gnunet-go 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. -// -// gnunet-go 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/>. -// -// SPDX-License-Identifier: AGPL3.0-or-later - -package util - -import ( - "context" - "database/sql" - "fmt" - "os" - "strings" - - _ "github.com/go-sql-driver/mysql" // init MySQL driver - _ "github.com/mattn/go-sqlite3" // init SQLite3 driver -) - -// Error messages related to databases -var ( - ErrSQLInvalidDatabaseSpec = fmt.Errorf("Invalid database specification") - ErrSQLNoDatabase = fmt.Errorf("Database not found") -) - -//---------------------------------------------------------------------- -// Connection to a database instance. There can be multiple connections -// on the same instance, managed by the database pool. -//---------------------------------------------------------------------- - -// DbConn is a database connection suitable for executing SQL commands. -type DbConn struct { - conn *sql.Conn // connection to database instance - pool *dbPool // reference to managng pool - key string // database identifier (connect string) -} - -// Close database connection. -func (db *DbConn) Close() (err error) { - if err = db.conn.Close(); err != nil { - return - } - err = db.pool.remove(db.key) - return -} - -// QueryRow returns a single record for a query -func (db *DbConn) QueryRow(query string, args ...any) *sql.Row { - return db.conn.QueryRowContext(db.pool.ctx, query, args...) -} - -// Query returns all matching records for a query -func (db *DbConn) Query(query string, args ...any) (*sql.Rows, error) { - return db.conn.QueryContext(db.pool.ctx, query, args...) -} - -// Exec a SQL statement -func (db *DbConn) Exec(query string, args ...any) (sql.Result, error) { - return db.conn.ExecContext(db.pool.ctx, query, args...) -} - -// TODO: add more SQL methods - -//---------------------------------------------------------------------- -// DbPool holds all database instances used: Connecting with the same -// connect string returns the same instance. -//---------------------------------------------------------------------- - -// global instance for the database pool (singleton) -var ( - DbPool *dbPool -) - -// DbPoolEntry holds information about a database instance. -type DbPoolEntry struct { - db *sql.DB // reference to the database engine - refs int // number of open connections (reference count) - connect string // SQL connect string -} - -// package initialization -func init() { - // construct database pool - DbPool = new(dbPool) - DbPool.insts = NewMap[string, *DbPoolEntry]() - DbPool.ctx, DbPool.cancel = context.WithCancel(context.Background()) -} - -// dbPool keeps a mapping between connect string and database instance -type dbPool struct { - ctx context.Context // connection context - cancel context.CancelFunc // cancel function - insts *Map[string, *DbPoolEntry] // map of database instances -} - -// remove a database instance from the pool based on its connect string. -func (p *dbPool) remove(key string) error { - return p.insts.Process(func() (err error) { - // get pool entry - pe, ok := p.insts.Get(key) - if !ok { - return nil - } - // decrement ref count - pe.refs-- - if pe.refs == 0 { - // no more refs: close database - err = pe.db.Close() - p.insts.Delete(key) - } - return - }, false) -} - -// Connect to a SQL database (various types and flavors): -// The 'spec' option defines the arguments required to connect to a database; -// the meaning and format of the arguments depends on the specific SQL database. -// The arguments are seperated by the '+' character; the first (and mandatory) -// argument defines the SQL database type. Other arguments depend on the value -// of this first argument. -// The following SQL types are implemented: -// * 'sqlite3': SQLite3-compatible database; the second argument specifies the -// file that holds the data (e.g. "sqlite3+/home/user/store.db") -// * 'mysql': A MySQL-compatible database; the second argument specifies the -// information required to log into the database (e.g. -// "[user[:passwd]@][proto[(addr)]]/dbname[?param1=value1&...]"). -func (p *dbPool) Connect(spec string) (db *DbConn, err error) { - err = p.insts.Process(func() error { - // check if we have a connection to this database. - inst, ok := p.insts.Get(spec) - if !ok { - inst = new(DbPoolEntry) - inst.refs = 0 - inst.connect = spec - - // No: create new database instance. - // split spec string into segments - specs := strings.Split(spec, ":") - if len(specs) < 2 { - return ErrSQLInvalidDatabaseSpec - } - // create database object - switch specs[0] { - case "sqlite3": - // check if the database file exists - var fi os.FileInfo - if fi, err = os.Stat(specs[1]); err != nil { - return ErrSQLNoDatabase - } - if fi.IsDir() { - return ErrSQLNoDatabase - } - // open the database file - inst.db, err = sql.Open("sqlite3", specs[1]) - case "mysql": - // just connect to the database - inst.db, err = sql.Open("mysql", specs[1]) - default: - return ErrSQLInvalidDatabaseSpec - } - // save database in pool - p.insts.Put(spec, inst) - ok = true - } - // increment reference count - inst.refs++ - // return a new connection to the database. - db = new(DbConn) - db.conn, err = inst.db.Conn(p.ctx) - return err - }, false) - return -} diff --git a/src/gnunet/util/fs.go b/src/gnunet/util/fs.go @@ -37,7 +37,7 @@ func EnforceDirExists(path string) error { return err } if !fi.IsDir() { - return fmt.Errorf("Not a directory (%s)", path) + return fmt.Errorf("not a directory (%s)", path) } return nil } diff --git a/src/gnunet/util/map.go b/src/gnunet/util/map.go @@ -0,0 +1,151 @@ +// This file is part of gnunet-go, a GNUnet-implementation in Golang. +// Copyright (C) 2019-2022 Bernd Fix >Y< +// +// gnunet-go 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. +// +// gnunet-go 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/>. +// +// SPDX-License-Identifier: AGPL3.0-or-later + +package util + +import ( + "math/rand" + "sync" +) + +//---------------------------------------------------------------------- +// Thread-safe map implementation +//---------------------------------------------------------------------- + +// Map keys to values +type Map[K comparable, V any] struct { + sync.RWMutex + + list map[K]V + inProcess bool +} + +// NewMap allocates a new mapping. +func NewMap[K comparable, V any]() *Map[K, V] { + return &Map[K, V]{ + list: make(map[K]V), + inProcess: false, + } +} + +//---------------------------------------------------------------------- + +// Process a function in the locked map context. Calls +// to other map functions in 'f' will skip their locks. +func (m *Map[K, V]) Process(f func() error, readonly bool) error { + // handle locking + m.lock(readonly) + m.inProcess = true + defer func() { + m.inProcess = false + m.unlock(readonly) + }() + // function call in unlocked environment + return f() +} + +// Process a ranged function in the locked map context. Calls +// to other map functions in 'f' will skip their locks. +func (m *Map[K, V]) ProcessRange(f func(key K, value V) error, readonly bool) error { + // handle locking + m.lock(readonly) + m.inProcess = true + defer func() { + m.inProcess = false + m.unlock(readonly) + }() + // range over map and call function. + for key, value := range m.list { + if err := f(key, value); err != nil { + return err + } + } + return nil +} + +//---------------------------------------------------------------------- + +// Size returns the number of entries in the map. +func (m *Map[K, V]) Size() int { + return len(m.list) +} + +// Put value into map under given key. +func (m *Map[K, V]) Put(key K, value V) { + m.lock(false) + defer m.unlock(false) + m.list[key] = value +} + +// Get value with iven key from map. +func (m *Map[K, V]) Get(key K) (value V, ok bool) { + m.lock(true) + defer m.unlock(true) + value, ok = m.list[key] + return +} + +// GetRandom returns a random map entry. +func (m *Map[K, V]) GetRandom() (key K, value V, ok bool) { + m.lock(true) + defer m.unlock(true) + + ok = false + if size := m.Size(); size > 0 { + idx := rand.Intn(size) + for key, value = range m.list { + if idx == 0 { + ok = true + return + } + idx-- + } + } + return +} + +// Delete key/value pair from map. +func (m *Map[K, V]) Delete(key K) { + m.lock(false) + defer m.unlock(false) + delete(m.list, key) +} + +//---------------------------------------------------------------------- + +// lock with given mode (if not in processing function) +func (m *Map[K, V]) lock(readonly bool) { + if !m.inProcess { + if readonly { + m.RLock() + } else { + m.Lock() + } + } +} + +// lock with given mode (if not in processing function) +func (m *Map[K, V]) unlock(readonly bool) { + if !m.inProcess { + if readonly { + m.RUnlock() + } else { + m.Unlock() + } + } +} diff --git a/src/gnunet/util/misc.go b/src/gnunet/util/misc.go @@ -20,11 +20,10 @@ package util import ( "strings" - "sync" ) //---------------------------------------------------------------------- -// Count occurence of multiple instance at the same time. +// Count occurrence of multiple instance at the same time. //---------------------------------------------------------------------- // Counter is a metric with single key @@ -52,107 +51,23 @@ func (cm Counter[T]) Num(i T) int { } //---------------------------------------------------------------------- -// Thread-safe map implementation +// Parameter set with string keys and variable value types //---------------------------------------------------------------------- -// Map keys to values -type Map[K comparable, V any] struct { - list map[K]V - mtx sync.RWMutex - inProcess bool -} - -// NewMap allocates a new mapping. -func NewMap[K comparable, V any]() *Map[K, V] { - return &Map[K, V]{ - list: make(map[K]V), - inProcess: false, - } -} +// ParameterSet with string keys and variable value types +type ParameterSet map[string]any -//---------------------------------------------------------------------- - -// Process a function in the locked map context. Calls -// to other map functions in 'f' will skip their locks. -func (m *Map[K, V]) Process(f func() error, readonly bool) error { - // handle locking - m.lock(readonly) - m.inProcess = true - defer func() { - m.inProcess = false - m.unlock(readonly) - }() - // function call in unlocked environment - return f() -} - -// Process a ranged function in the locked map context. Calls -// to other map functions in 'f' will skip their locks. -func (m *Map[K, V]) ProcessRange(f func(key K, value V) error, readonly bool) error { - // handle locking - m.lock(readonly) - m.inProcess = true - defer func() { - m.inProcess = false - m.unlock(readonly) - }() - // range over map and call function. - for key, value := range m.list { - if err := f(key, value); err != nil { - return err +// Get a parameter value with given type 'V' +func GetParam[V any](params ParameterSet, key string) (i V, ok bool) { + var v any + if v, ok = params[key]; ok { + if i, ok = v.(V); ok { + return } } - return nil -} - -//---------------------------------------------------------------------- - -// Put value into map under given key. -func (m *Map[K, V]) Put(key K, value V) { - m.lock(false) - defer m.unlock(false) - m.list[key] = value -} - -// Get value with iven key from map. -func (m *Map[K, V]) Get(key K) (value V, ok bool) { - m.lock(true) - defer m.unlock(true) - value, ok = m.list[key] return } -// Delete key/value pair from map. -func (m *Map[K, V]) Delete(key K) { - m.lock(false) - defer m.unlock(false) - delete(m.list, key) -} - -//---------------------------------------------------------------------- - -// lock with given mode (if not in processing function) -func (m *Map[K, V]) lock(readonly bool) { - if !m.inProcess { - if readonly { - m.mtx.RLock() - } else { - m.mtx.Lock() - } - } -} - -// lock with given mode (if not in processing function) -func (m *Map[K, V]) unlock(readonly bool) { - if !m.inProcess { - if readonly { - m.mtx.RUnlock() - } else { - m.mtx.Unlock() - } - } -} - //---------------------------------------------------------------------- // additional helpers //---------------------------------------------------------------------- diff --git a/src/gnunet/util/peer.go b/src/gnunet/util/peer.go @@ -0,0 +1,94 @@ +// This file is part of gnunet-go, a GNUnet-implementation in Golang. +// Copyright (C) 2019-2022 Bernd Fix >Y< +// +// gnunet-go 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. +// +// gnunet-go 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/>. +// +// SPDX-License-Identifier: AGPL3.0-or-later + +package util + +import ( + "bytes" +) + +//---------------------------------------------------------------------- +// Peer public key (Ed25519 public key) +//---------------------------------------------------------------------- + +// PeerPublicKey is the binary representation of an Ed25519 public key +type PeerPublicKey struct { + Data []byte `size:"32"` // Ed25519 public key data +} + +// NewPeerPublicKey creates a key instance from binary data +func NewPeerPublicKey(data []byte) *PeerPublicKey { + pk := &PeerPublicKey{ + Data: make([]byte, 32), + } + if data != nil { + if len(data) < 32 { + CopyAlignedBlock(pk.Data, data) + } else { + copy(pk.Data, data[:32]) + } + } + return pk +} + +//---------------------------------------------------------------------- +// Peer identifier: +//---------------------------------------------------------------------- + +// PeerID is a wrpped PeerPublicKey +type PeerID PeerPublicKey + +// NewPeerID creates a new peer id from data. +func NewPeerID(data []byte) (p *PeerID) { + return (*PeerID)(NewPeerPublicKey(data)) +} + +// Equals returns true if two peer IDs match. +func (p *PeerID) Equals(q *PeerID) bool { + return bytes.Equal(p.Data, q.Data) +} + +// String returns a human-readable representation of a peer id. +func (p *PeerID) String() string { + return EncodeBinaryToString(p.Data) +} + +// Bytes returns the binary representation of a peer identifier. +func (p *PeerID) Bytes() []byte { + return Clone(p.Data) +} + +//---------------------------------------------------------------------- + +// PeerSignature is a EdDSA signature from the peer +type PeerSignature struct { + Data []byte `size:"64"` +} + +// NewPeerSignature is a EdDSA signatre with the private peer key +func NewPeerSignature(data []byte) *PeerSignature { + var v []byte + if data == nil { + v = make([]byte, 64) + } else { + v = Clone(data) + } + return &PeerSignature{ + Data: v, + } +} diff --git a/src/gnunet/util/peer_id.go b/src/gnunet/util/peer_id.go @@ -1,59 +0,0 @@ -// This file is part of gnunet-go, a GNUnet-implementation in Golang. -// Copyright (C) 2019-2022 Bernd Fix >Y< -// -// gnunet-go 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. -// -// gnunet-go 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/>. -// -// SPDX-License-Identifier: AGPL3.0-or-later - -package util - -import ( - "bytes" - - "github.com/bfix/gospel/crypto/ed25519" -) - -// PeerID is the 32-byte binary representation od a Ed25519 key -type PeerID struct { - Key []byte `size:"32"` -} - -// NewPeerID creates a new peer id from data. -func NewPeerID(data []byte) (p *PeerID) { - p = &PeerID{ - Key: make([]byte, 32), - } - if data != nil { - if len(data) < 32 { - CopyAlignedBlock(p.Key, data) - } else { - copy(p.Key, data[:32]) - } - } - return -} - -// Equals returns true if two peer IDs match. -func (p *PeerID) Equals(q *PeerID) bool { - return bytes.Equal(p.Key, q.Key) -} - -// String returns a human-readable representation of a peer id. -func (p *PeerID) String() string { - return EncodeBinaryToString(p.Key) -} - -func (p *PeerID) PublicKey() *ed25519.PublicKey { - return ed25519.NewPublicKeyFromBytes(p.Key) -} diff --git a/src/gnunet/util/rnd.go b/src/gnunet/util/rnd.go @@ -22,17 +22,21 @@ import ( "bytes" "crypto/rand" "encoding/binary" + + "github.com/bfix/gospel/logger" ) // RndArray fills a buffer with random content func RndArray(b []byte) { - rand.Read(b) + if _, err := rand.Read(b); err != nil { + logger.Printf(logger.ERROR, "[RndArray] failed: %s", err.Error()) + } } // NewRndArray creates a new buffer of given size; filled with random content. func NewRndArray(size int) []byte { b := make([]byte, size) - rand.Read(b) + RndArray(b) return b } @@ -42,7 +46,9 @@ func RndUInt64() uint64 { RndArray(b) var v uint64 c := bytes.NewBuffer(b) - binary.Read(c, binary.BigEndian, &v) + if err := binary.Read(c, binary.BigEndian, &v); err != nil { + logger.Printf(logger.ERROR, "[RndUInt64] failed: %s", err.Error()) + } return v } diff --git a/src/gnunet/util/time.go b/src/gnunet/util/time.go @@ -46,7 +46,7 @@ func NewAbsoluteTime(t time.Time) AbsoluteTime { // NewAbsoluteTimeEpoch set the point in time to the given time value func NewAbsoluteTimeEpoch(secs uint64) AbsoluteTime { return AbsoluteTime{ - Val: uint64(secs * 1000000), + Val: secs * 1000000, } } @@ -137,8 +137,9 @@ func (t AbsoluteTime) Compare(t2 AbsoluteTime) int { // Relative time //---------------------------------------------------------------------- -// RelativeTime is a timestamp defined relative to the current time. -// It actually is more like a duration than a time... +// RelativeTime is a timestamp defined relative to an AbsoluteTime. +// It is measured in microseconds and is actually more like a duration +// than a time... type RelativeTime struct { Val uint64 `order:"big"` } @@ -146,7 +147,7 @@ type RelativeTime struct { // NewRelativeTime is initialized with a given duration. func NewRelativeTime(d time.Duration) RelativeTime { return RelativeTime{ - Val: uint64(d.Milliseconds()), + Val: uint64(d.Microseconds()), } } @@ -155,12 +156,14 @@ func (t RelativeTime) String() string { if t.Val == math.MaxUint64 { return "Forever" } - return time.Duration(t.Val * 1000).String() + return time.Duration(t.Val * 1000000).String() } // Add two durations -func (t RelativeTime) Add(t2 RelativeTime) { - t.Val += t2.Val +func (t RelativeTime) Add(t2 RelativeTime) RelativeTime { + return RelativeTime{ + Val: t.Val + t2.Val, + } } // Compare two durations