From 913c80f317270b41f339048afa5d63278eecf8f4 Mon Sep 17 00:00:00 2001 From: Bernd Fix Date: Wed, 1 Jun 2022 20:59:52 +0200 Subject: Reworked gnunet/transport and gnunet/service. --- src/cmd/.gitignore | 1 - src/cmd/gnunet-service-gns-go/main.go | 130 ------- src/cmd/gnunet-service-revocation-go/main.go | 130 ------- src/cmd/peer_mockup/main.go | 68 ---- src/cmd/peer_mockup/peers.go | 46 --- src/cmd/peer_mockup/process.go | 128 ------- src/cmd/revoke-zonekey/main.go | 202 ----------- src/cmd/vanityid/main.go | 53 --- src/gnunet/build.sh | 3 + src/gnunet/cmd/.gitignore | 1 + src/gnunet/cmd/gnunet-service-dht-test-go/main.go | 152 +++++++++ src/gnunet/cmd/gnunet-service-gns-go/main.go | 139 ++++++++ .../cmd/gnunet-service-revocation-go/main.go | 139 ++++++++ src/gnunet/cmd/peer_mockup/main.go | 178 ++++++++++ src/gnunet/cmd/revoke-zonekey/main.go | 336 ++++++++++++++++++ src/gnunet/cmd/vanityid/main.go | 53 +++ src/gnunet/config/config.go | 68 +++- src/gnunet/config/gnunet-config.json | 48 ++- src/gnunet/core/core.go | 234 +++++++++++++ src/gnunet/core/core_test.go | 150 ++++++++ src/gnunet/core/event.go | 109 ++++++ src/gnunet/core/peer.go | 108 +++++- src/gnunet/core/peer_test.go | 72 ++++ src/gnunet/crypto/gns.go | 9 +- src/gnunet/crypto/hash.go | 16 +- src/gnunet/go.mod | 26 +- src/gnunet/go.sum | 128 ++----- src/gnunet/message/factory.go | 4 + src/gnunet/message/message.go | 8 + src/gnunet/message/msg_dht.go | 95 +++++- src/gnunet/message/msg_gns.go | 94 ----- src/gnunet/message/msg_hello.go | 103 ++++++ src/gnunet/message/msg_namecache.go | 12 +- src/gnunet/message/msg_transport.go | 82 +---- src/gnunet/modules.go | 28 +- src/gnunet/service/client.go | 30 +- src/gnunet/service/connection.go | 280 +++++++++++++++ src/gnunet/service/context.go | 87 ----- src/gnunet/service/dht/blocks/generic.go | 196 +++++++++++ src/gnunet/service/dht/blocks/generic_test.go | 67 ++++ src/gnunet/service/dht/blocks/gns.go | 172 ++++++++++ src/gnunet/service/dht/blocks/hello.go | 226 ++++++++++++ src/gnunet/service/dht/blocks/hello_test.go | 44 +++ src/gnunet/service/dht/blocks/types.go | 26 ++ src/gnunet/service/dht/bloomfilter.go | 123 +++++++ src/gnunet/service/dht/dhtstore_test.go | 89 +++++ src/gnunet/service/dht/module.go | 99 +++++- src/gnunet/service/dht/routingtable.go | 305 +++++++++++++++++ src/gnunet/service/dht/routingtable_test.go | 140 ++++++++ src/gnunet/service/dht/service.go | 134 ++++++++ src/gnunet/service/gns/block_handler.go | 18 +- src/gnunet/service/gns/dns.go | 4 +- src/gnunet/service/gns/module.go | 135 +++++--- src/gnunet/service/gns/service.go | 235 ++++++------- src/gnunet/service/module.go | 70 ++++ src/gnunet/service/namecache/module.go | 31 +- src/gnunet/service/revocation/module.go | 100 ++++-- src/gnunet/service/revocation/pow.go | 46 ++- src/gnunet/service/revocation/pow_test.go | 8 +- src/gnunet/service/revocation/service.go | 192 +++++------ src/gnunet/service/service.go | 200 +++++------ src/gnunet/service/store.go | 379 +++++++++++++++++++++ src/gnunet/test.sh | 3 + src/gnunet/transport/channel.go | 213 ------------ src/gnunet/transport/channel_netw.go | 285 ---------------- src/gnunet/transport/channel_test.go | 232 ------------- src/gnunet/transport/connection.go | 108 ++++-- src/gnunet/transport/endpoint.go | 282 +++++++++++++++ src/gnunet/transport/reader_writer.go | 157 +++++++++ src/gnunet/transport/session.go | 29 -- src/gnunet/transport/transport.go | 151 ++++++++ src/gnunet/util/address.go | 141 +++++++- src/gnunet/util/array.go | 65 ++-- src/gnunet/util/database.go | 159 +++++++-- src/gnunet/util/fs.go | 2 +- src/gnunet/util/key_value_store.go | 188 ---------- src/gnunet/util/misc.go | 75 +++- src/gnunet/util/peer_id.go | 11 +- src/gnunet/util/time.go | 17 + 79 files changed, 5999 insertions(+), 2708 deletions(-) delete mode 100644 src/cmd/.gitignore delete mode 100644 src/cmd/gnunet-service-gns-go/main.go delete mode 100644 src/cmd/gnunet-service-revocation-go/main.go delete mode 100644 src/cmd/peer_mockup/main.go delete mode 100644 src/cmd/peer_mockup/peers.go delete mode 100644 src/cmd/peer_mockup/process.go delete mode 100644 src/cmd/revoke-zonekey/main.go delete mode 100644 src/cmd/vanityid/main.go create mode 100755 src/gnunet/build.sh create mode 100644 src/gnunet/cmd/.gitignore create mode 100644 src/gnunet/cmd/gnunet-service-dht-test-go/main.go create mode 100644 src/gnunet/cmd/gnunet-service-gns-go/main.go create mode 100644 src/gnunet/cmd/gnunet-service-revocation-go/main.go create mode 100644 src/gnunet/cmd/peer_mockup/main.go create mode 100644 src/gnunet/cmd/revoke-zonekey/main.go create mode 100644 src/gnunet/cmd/vanityid/main.go create mode 100644 src/gnunet/core/core.go create mode 100644 src/gnunet/core/core_test.go create mode 100644 src/gnunet/core/event.go create mode 100644 src/gnunet/core/peer_test.go create mode 100644 src/gnunet/message/msg_hello.go create mode 100644 src/gnunet/service/connection.go delete mode 100644 src/gnunet/service/context.go create mode 100644 src/gnunet/service/dht/blocks/generic.go create mode 100644 src/gnunet/service/dht/blocks/generic_test.go create mode 100644 src/gnunet/service/dht/blocks/gns.go create mode 100644 src/gnunet/service/dht/blocks/hello.go create mode 100644 src/gnunet/service/dht/blocks/hello_test.go create mode 100644 src/gnunet/service/dht/blocks/types.go create mode 100644 src/gnunet/service/dht/bloomfilter.go create mode 100644 src/gnunet/service/dht/dhtstore_test.go create mode 100644 src/gnunet/service/dht/routingtable.go create mode 100644 src/gnunet/service/dht/routingtable_test.go create mode 100644 src/gnunet/service/dht/service.go create mode 100644 src/gnunet/service/module.go create mode 100644 src/gnunet/service/store.go create mode 100755 src/gnunet/test.sh delete mode 100644 src/gnunet/transport/channel.go delete mode 100644 src/gnunet/transport/channel_netw.go delete mode 100644 src/gnunet/transport/channel_test.go create mode 100644 src/gnunet/transport/endpoint.go create mode 100644 src/gnunet/transport/reader_writer.go delete mode 100644 src/gnunet/transport/session.go create mode 100644 src/gnunet/transport/transport.go delete mode 100644 src/gnunet/util/key_value_store.go (limited to 'src') diff --git a/src/cmd/.gitignore b/src/cmd/.gitignore deleted file mode 100644 index 1933786..0000000 --- a/src/cmd/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/test/ diff --git a/src/cmd/gnunet-service-gns-go/main.go b/src/cmd/gnunet-service-gns-go/main.go deleted file mode 100644 index 57eec7e..0000000 --- a/src/cmd/gnunet-service-gns-go/main.go +++ /dev/null @@ -1,130 +0,0 @@ -// This file is part of gnunet-go, a GNUnet-implementation in Golang. -// Copyright (C) 2019, 2020 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 . -// -// SPDX-License-Identifier: AGPL3.0-or-later - -package main - -import ( - "context" - "flag" - "os" - "os/signal" - "strings" - "syscall" - "time" - - "gnunet/config" - "gnunet/rpc" - "gnunet/service" - "gnunet/service/gns" - - "github.com/bfix/gospel/logger" -) - -func main() { - defer func() { - logger.Println(logger.INFO, "[gns] Bye.") - // flush last messages - logger.Flush() - }() - logger.Println(logger.INFO, "[gns] Starting service...") - - var ( - cfgFile string - srvEndp string - err error - logLevel int - rpcEndp string - ) - // handle command line arguments - flag.StringVar(&cfgFile, "c", "gnunet-config.json", "GNUnet configuration file") - flag.StringVar(&srvEndp, "s", "", "GNS service end-point") - flag.IntVar(&logLevel, "L", logger.INFO, "GNS log level (default: INFO)") - flag.StringVar(&rpcEndp, "R", "", "JSON-RPC endpoint (default: none)") - flag.Parse() - - // read configuration file and set missing arguments. - if err = config.ParseConfig(cfgFile); err != nil { - logger.Printf(logger.ERROR, "[gns] Invalid configuration file: %s\n", err.Error()) - return - } - - // apply configuration - logger.SetLogLevel(logLevel) - if len(srvEndp) == 0 { - srvEndp = config.Cfg.GNS.Endpoint - } - - // start a new GNS service - gns := gns.NewService() - srv := service.NewServiceImpl("gns", gns) - if err = srv.Start(srvEndp); err != nil { - logger.Printf(logger.ERROR, "[gns] Error: '%s'", err.Error()) - return - } - - // start JSON-RPC server on request - var cancel func() = func() {} - if len(rpcEndp) > 0 { - var ctx context.Context - ctx, cancel = context.WithCancel(context.Background()) - parts := strings.Split(rpcEndp, "+") - if parts[0] != "tcp" { - logger.Println(logger.ERROR, "[gns] RPC must have a TCP/IP endpoint") - return - } - config.Cfg.RPC.Endpoint = parts[1] - if err = rpc.Start(ctx); err != nil { - logger.Printf(logger.ERROR, "[gns] RPC failed to start: %s", err.Error()) - return - } - rpc.Register(gns) - } - - // handle OS signals - sigCh := make(chan os.Signal, 5) - signal.Notify(sigCh) - - // heart beat - tick := time.NewTicker(5 * time.Minute) - -loop: - for { - select { - // handle OS signals - case sig := <-sigCh: - switch sig { - case syscall.SIGKILL, syscall.SIGINT, syscall.SIGTERM: - logger.Printf(logger.INFO, "[gns] Terminating service (on signal '%s')\n", sig) - break loop - case syscall.SIGHUP: - logger.Println(logger.INFO, "[gns] SIGHUP") - case syscall.SIGURG: - // TODO: https://github.com/golang/go/issues/37942 - default: - logger.Println(logger.INFO, "[gns] Unhandled signal: "+sig.String()) - } - // handle heart beat - case now := <-tick.C: - logger.Println(logger.INFO, "[gns] Heart beat at "+now.String()) - } - } - - // terminating service - cancel() - srv.Stop() -} diff --git a/src/cmd/gnunet-service-revocation-go/main.go b/src/cmd/gnunet-service-revocation-go/main.go deleted file mode 100644 index ec5ce73..0000000 --- a/src/cmd/gnunet-service-revocation-go/main.go +++ /dev/null @@ -1,130 +0,0 @@ -// This file is part of gnunet-go, a GNUnet-implementation in Golang. -// Copyright (C) 2019, 2020 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 . -// -// SPDX-License-Identifier: AGPL3.0-or-later - -package main - -import ( - "context" - "flag" - "os" - "os/signal" - "strings" - "syscall" - "time" - - "gnunet/config" - "gnunet/rpc" - "gnunet/service" - "gnunet/service/revocation" - - "github.com/bfix/gospel/logger" -) - -func main() { - defer func() { - logger.Println(logger.INFO, "[revocation] Bye.") - // flush last messages - logger.Flush() - }() - logger.Println(logger.INFO, "[revocation] Starting service...") - - var ( - cfgFile string - srvEndp string - err error - logLevel int - rpcEndp string - ) - // handle command line arguments - flag.StringVar(&cfgFile, "c", "gnunet-config.json", "GNUnet configuration file") - flag.StringVar(&srvEndp, "s", "", "REVOCATION service end-point") - flag.IntVar(&logLevel, "L", logger.INFO, "REVOCATION log level (default: INFO)") - flag.StringVar(&rpcEndp, "R", "", "JSON-RPC endpoint (default: none)") - flag.Parse() - - // read configuration file and set missing arguments. - if err = config.ParseConfig(cfgFile); err != nil { - logger.Printf(logger.ERROR, "[revocation] Invalid configuration file: %s\n", err.Error()) - return - } - - // apply configuration - logger.SetLogLevel(logLevel) - if len(srvEndp) == 0 { - srvEndp = config.Cfg.GNS.Endpoint - } - - // start a new REVOCATION service - rvc := revocation.NewService() - srv := service.NewServiceImpl("revocation", rvc) - if err = srv.Start(srvEndp); err != nil { - logger.Printf(logger.ERROR, "[revocation] Error: '%s'\n", err.Error()) - return - } - - // start JSON-RPC server on request - var cancel func() = func() {} - if len(rpcEndp) > 0 { - var ctx context.Context - ctx, cancel = context.WithCancel(context.Background()) - parts := strings.Split(rpcEndp, "+") - if parts[0] != "tcp" { - logger.Println(logger.ERROR, "[revocation] RPC must have a TCP/IP endpoint") - return - } - config.Cfg.RPC.Endpoint = parts[1] - if err = rpc.Start(ctx); err != nil { - logger.Printf(logger.ERROR, "[revocation] RPC failed to start: %s", err.Error()) - return - } - rpc.Register(rvc) - } - - // handle OS signals - sigCh := make(chan os.Signal, 5) - signal.Notify(sigCh) - - // heart beat - tick := time.NewTicker(5 * time.Minute) - -loop: - for { - select { - // handle OS signals - case sig := <-sigCh: - switch sig { - case syscall.SIGKILL, syscall.SIGINT, syscall.SIGTERM: - logger.Printf(logger.INFO, "[revocation] Terminating service (on signal '%s')\n", sig) - break loop - case syscall.SIGHUP: - logger.Println(logger.INFO, "[revocation] SIGHUP") - case syscall.SIGURG: - // TODO: https://github.com/golang/go/issues/37942 - default: - logger.Println(logger.INFO, "[revocation] Unhandled signal: "+sig.String()) - } - // handle heart beat - case now := <-tick.C: - logger.Println(logger.INFO, "[revocation] Heart beat at "+now.String()) - } - } - - // terminating service - cancel() - srv.Stop() -} diff --git a/src/cmd/peer_mockup/main.go b/src/cmd/peer_mockup/main.go deleted file mode 100644 index 59bc002..0000000 --- a/src/cmd/peer_mockup/main.go +++ /dev/null @@ -1,68 +0,0 @@ -package main - -import ( - "encoding/hex" - "flag" - "fmt" - - "github.com/bfix/gospel/logger" - "gnunet/core" - "gnunet/transport" -) - -var ( - p *core.Peer // local peer (with private key) - t *core.Peer // remote peer -) - -func main() { - // handle command line arguments - var ( - asServer bool - err error - ch transport.Channel - ) - flag.BoolVar(&asServer, "s", false, "accept incoming connections") - flag.Parse() - - // setup peer instances from static data - if err = setupPeers(false); err != nil { - fmt.Println(err.Error()) - return - } - - fmt.Println("======================================================================") - fmt.Println("GNUnet peer mock-up (EXPERIMENTAL) (c) 2018,2019 by Bernd Fix, >Y<") - fmt.Printf(" Identity '%s'\n", p.GetIDString()) - fmt.Printf(" [%s]\n", hex.EncodeToString(p.GetID())) - fmt.Println("======================================================================") - - if asServer { - // run as server - fmt.Println("Waiting for connections...") - hdlr := make(chan transport.Channel) - go func() { - for { - select { - case ch = <-hdlr: - mc := transport.NewMsgChannel(ch) - if err = process(mc, t, p); err != nil { - logger.Println(logger.ERROR, err.Error()) - } - } - } - }() - _, err = transport.NewChannelServer("tcp+0.0.0.0:2086", hdlr) - } else { - // connect to peer - fmt.Println("Connecting to target peer") - if ch, err = transport.NewChannel("tcp+172.17.0.5:2086"); err != nil { - logger.Println(logger.ERROR, err.Error()) - } - mc := transport.NewMsgChannel(ch) - err = process(mc, p, t) - } - if err != nil { - fmt.Println(err) - } -} diff --git a/src/cmd/peer_mockup/peers.go b/src/cmd/peer_mockup/peers.go deleted file mode 100644 index 16d3c87..0000000 --- a/src/cmd/peer_mockup/peers.go +++ /dev/null @@ -1,46 +0,0 @@ -package main - -import ( - "github.com/bfix/gospel/data" - "gnunet/core" - "gnunet/util" -) - -func setupPeers(rnd bool) (err error) { - - //------------------------------------------------------------------ - // create local peer - //------------------------------------------------------------------ - secret := []byte{ - 0x78, 0xde, 0xcf, 0xc0, 0x26, 0x9e, 0x62, 0x3d, - 0x17, 0x24, 0xe6, 0x1b, 0x98, 0x25, 0xec, 0x2f, - 0x40, 0x6b, 0x1e, 0x39, 0xa5, 0x19, 0xac, 0x9b, - 0xb2, 0xdd, 0xf4, 0x6c, 0x12, 0x83, 0xdb, 0x86, - } - if rnd { - util.RndArray(secret) - } - p, err = core.NewPeer(secret, true) - if err != nil { - return - } - addr, _ := data.Marshal(util.NewIPAddress([]byte{172, 17, 0, 6}, 2086)) - p.AddAddress(util.NewAddress("tcp", addr)) - - //------------------------------------------------------------------ - // create remote peer - //------------------------------------------------------------------ - id := []byte{ - 0x92, 0xdc, 0xbf, 0x39, 0x40, 0x2d, 0xc6, 0x3c, - 0x97, 0xa6, 0x81, 0xe0, 0xfc, 0xd8, 0x7c, 0x74, - 0x17, 0xd3, 0xa3, 0x8c, 0x52, 0xfd, 0xe0, 0x49, - 0xbc, 0xd0, 0x1c, 0x0a, 0x0b, 0x8c, 0x02, 0x51, - } - t, err = core.NewPeer(id, false) - if err != nil { - return - } - addr, _ = data.Marshal(util.NewIPAddress([]byte{172, 17, 0, 5}, 2086)) - t.AddAddress(util.NewAddress("tcp", addr)) - return -} diff --git a/src/cmd/peer_mockup/process.go b/src/cmd/peer_mockup/process.go deleted file mode 100644 index 510fc48..0000000 --- a/src/cmd/peer_mockup/process.go +++ /dev/null @@ -1,128 +0,0 @@ -package main - -import ( - "errors" - "fmt" - - "gnunet/core" - "gnunet/crypto" - "gnunet/message" - "gnunet/transport" - "gnunet/util" - - "github.com/bfix/gospel/concurrent" -) - -var ( - sig = concurrent.NewSignaller() -) - -func process(ch *transport.MsgChannel, from, to *core.Peer) (err error) { - // create a new connection instance - c := transport.NewConnection(ch, from, to) - defer c.Close() - - // read and push next message - in := make(chan message.Message) - go func() { - for { - msg, err := c.Receive(sig) - if err != nil { - fmt.Printf("Receive: %s\n", err.Error()) - return - } - in <- msg - } - }() - - // are we initiating the connection? - init := (from == p) - if init { - peerid := util.NewPeerID(p.GetID()) - c.Send(message.NewTransportTcpWelcomeMsg(peerid), sig) - } - - // remember peer addresses (only ONE!) - pAddr := p.GetAddressList()[0] - tAddr := t.GetAddressList()[0] - - send := make(map[uint16]bool) - //received := make(map[uint16]bool) - pending := make(map[uint16]message.Message) - - // process loop - for { - select { - case m := <-in: - switch msg := m.(type) { - - case *message.TransportTcpWelcomeMsg: - peerid := util.NewPeerID(p.GetID()) - if init { - c.Send(message.NewHelloMsg(peerid), sig) - target := util.NewPeerID(t.GetID()) - c.Send(message.NewTransportPingMsg(target, tAddr), sig) - } else { - c.Send(message.NewTransportTcpWelcomeMsg(peerid), sig) - } - - case *message.HelloMsg: - - case *message.TransportPingMsg: - mOut := message.NewTransportPongMsg(msg.Challenge, pAddr) - if err := mOut.Sign(p.PrvKey()); err != nil { - return err - } - c.Send(mOut, sig) - - case *message.TransportPongMsg: - rc, err := msg.Verify(t.PubKey()) - if err != nil { - return err - } - if !rc { - return errors.New("PONG verification failed") - } - send[message.TRANSPORT_PONG] = true - if mOut, ok := pending[message.TRANSPORT_SESSION_SYN]; ok { - c.Send(mOut, sig) - } - - case *message.SessionSynMsg: - mOut := message.NewSessionSynAckMsg() - mOut.Timestamp = msg.Timestamp - if send[message.TRANSPORT_PONG] { - c.Send(mOut, sig) - } else { - pending[message.TRANSPORT_SESSION_SYN] = mOut - } - - case *message.SessionQuotaMsg: - c.SetBandwidth(msg.Quota) - - case *message.SessionAckMsg: - - case *message.SessionKeepAliveMsg: - c.Send(message.NewSessionKeepAliveRespMsg(msg.Nonce), sig) - - case *message.EphemeralKeyMsg: - rc, err := msg.Verify(t.PubKey()) - if err != nil { - return err - } - if !rc { - return errors.New("EPHKEY verification failed") - } - t.SetEphKeyMsg(msg) - c.Send(p.EphKeyMsg(), sig) - secret := crypto.SharedSecret(p.EphPrvKey(), t.EphKeyMsg().Public()) - c.SharedSecret(util.Clone(secret.Bits[:])) - - default: - fmt.Printf("!!! %v\n", msg) - } - default: - } - } - return nil -} diff --git a/src/cmd/revoke-zonekey/main.go b/src/cmd/revoke-zonekey/main.go deleted file mode 100644 index 7deeda6..0000000 --- a/src/cmd/revoke-zonekey/main.go +++ /dev/null @@ -1,202 +0,0 @@ -// This file is part of gnunet-go, a GNUnet-implementation in Golang. -// Copyright (C) 2019, 2020 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 . -// -// SPDX-License-Identifier: AGPL3.0-or-later - -package main - -import ( - "context" - "encoding/hex" - "flag" - "log" - "os" - "os/signal" - "sync" - "syscall" - - "gnunet/service/revocation" - "gnunet/util" - - "github.com/bfix/gospel/data" -) - -func main() { - log.Println("*** Compute revocation data for a zone key") - log.Println("*** Copyright (c) 2020, Bernd Fix >Y<") - log.Println("*** This is free software distributed under the Affero GPL v3.") - - // handle command line arguments - var ( - verbose bool // be verbose with messages - bits int // number of leading zero-bit requested - zonekey string // zonekey to be revoked - filename string // name of file for persistance - ) - flag.IntVar(&bits, "b", 25, "Number of leading zero bits") - flag.BoolVar(&verbose, "v", false, "verbose output") - flag.StringVar(&zonekey, "z", "", "Zone key to be revoked") - flag.StringVar(&filename, "f", "", "Name of file to store revocation") - flag.Parse() - - if len(filename) == 0 { - log.Fatal("Missing '-f' argument (filename fot revocation data)") - } - - // define layout of persistant data - var revData struct { - Rd *revocation.RevDataCalc // Revocation data - T util.RelativeTime // time spend in calculations - Last uint64 // last value used for PoW test - Numbits uint8 // number of leading zero-bits - } - dataBuf := make([]byte, 450) - - // read revocation object from file - file, err := os.Open(filename) - cont := true - if err != nil { - if len(zonekey) != 52 { - log.Fatal("Missing or invalid zonekey and no file specified -- aborting") - } - keyData, err := util.DecodeStringToBinary(zonekey, 32) - if err != nil { - log.Fatal("Invalid zonekey: " + err.Error()) - } - revData.Rd = revocation.NewRevDataCalc(keyData) - revData.Numbits = uint8(bits) - revData.T = util.NewRelativeTime(0) - cont = false - } else { - n, err := file.Read(dataBuf) - if err != nil { - log.Fatal("Error reading file: " + err.Error()) - } - if n != len(dataBuf) { - log.Fatal("File corrupted -- aborting") - } - if err = data.Unmarshal(&revData, dataBuf); err != nil { - log.Fatal("File corrupted: " + err.Error()) - } - bits = int(revData.Numbits) - if err = file.Close(); err != nil { - log.Fatal("Error closing file: " + err.Error()) - } - } - - if cont { - log.Printf("Revocation calculation started at %s\n", revData.Rd.Timestamp.String()) - log.Printf("Time spent on calculation: %s\n", revData.T.String()) - log.Printf("Last tested PoW value: %d\n", revData.Last) - log.Println("Continuing...") - } else { - log.Println("Starting new revocation calculation...") - } - log.Println("Press ^C to abort...") - - // pre-set difficulty - log.Printf("Difficulty: %d\n", bits) - if bits < 25 { - log.Println("WARNING: difficulty is less than 25!") - } - - // Start or continue calculation - ctx, cancelFcn := context.WithCancel(context.Background()) - wg := new(sync.WaitGroup) - wg.Add(1) - go func() { - defer wg.Done() - cb := func(average float64, last uint64) { - log.Printf("Improved PoW: %f average zero bits, %d steps\n", average, last) - } - - startTime := util.AbsoluteTimeNow() - average, last := revData.Rd.Compute(ctx, bits, revData.Last, cb) - if average < float64(bits) { - log.Printf("Incomplete revocation: Only %f zero bits on average!\n", average) - } else { - log.Println("Revocation data object:") - log.Println(" 0x" + hex.EncodeToString(revData.Rd.Blob())) - log.Println("Status:") - rc := revData.Rd.Verify(false) - switch { - case rc == -1: - log.Println(" Missing/invalid signature") - case rc == -2: - log.Println(" Expired revocation") - case rc == -3: - log.Println(" Wrong PoW sequence order") - case rc < 25: - log.Println(" Difficulty to small") - default: - log.Printf(" Difficulty: %d\n", rc) - } - } - if !cont || last != revData.Last { - revData.Last = last - revData.T = util.AbsoluteTimeNow().Diff(startTime) - - log.Println("Writing revocation data to file...") - file, err := os.Create(filename) - if err != nil { - log.Fatal("Can't write to output file: " + err.Error()) - } - buf, err := data.Marshal(&revData) - if err != nil { - log.Fatal("Internal error: " + err.Error()) - } - if len(buf) != len(dataBuf) { - log.Fatalf("Internal error: Buffer mismatch %d != %d", len(buf), len(dataBuf)) - } - n, err := file.Write(buf) - if err != nil { - log.Fatal("Can't write to output file: " + err.Error()) - } - if n != len(dataBuf) { - log.Fatal("Can't write data to output file!") - } - if err = file.Close(); err != nil { - log.Fatal("Error closing file: " + err.Error()) - } - } - }() - - go func() { - // handle OS signals - sigCh := make(chan os.Signal, 5) - signal.Notify(sigCh) - loop: - for { - select { - // 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()) - } - } - } - }() - wg.Wait() -} diff --git a/src/cmd/vanityid/main.go b/src/cmd/vanityid/main.go deleted file mode 100644 index 938df61..0000000 --- a/src/cmd/vanityid/main.go +++ /dev/null @@ -1,53 +0,0 @@ -package main - -import ( - "crypto/rand" - "encoding/hex" - "flag" - "fmt" - "regexp" - "time" - - "github.com/bfix/gospel/crypto/ed25519" - "gnunet/util" -) - -func main() { - // get arguments - flag.Parse() - prefixes := flag.Args() - num := len(prefixes) - if num == 0 { - fmt.Println("No prefixes specified -- done.") - return - } - - // pre-compile regexp - reg := make([]*regexp.Regexp, num) - for i, p := range prefixes { - reg[i] = regexp.MustCompile(p) - } - - // generate new keys in a loop - seed := make([]byte, 32) - start := time.Now() - for i := 0; ; i++ { - n, err := rand.Read(seed) - if err != nil || n != 32 { - panic(err) - } - 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) - 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) - i = 0 - start = time.Now() - } - } - } -} diff --git a/src/gnunet/build.sh b/src/gnunet/build.sh new file mode 100755 index 0000000..5ec677f --- /dev/null +++ b/src/gnunet/build.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +go install -v -gcflags "-N -l" ./... diff --git a/src/gnunet/cmd/.gitignore b/src/gnunet/cmd/.gitignore new file mode 100644 index 0000000..1933786 --- /dev/null +++ b/src/gnunet/cmd/.gitignore @@ -0,0 +1 @@ +/test/ diff --git a/src/gnunet/cmd/gnunet-service-dht-test-go/main.go b/src/gnunet/cmd/gnunet-service-dht-test-go/main.go new file mode 100644 index 0000000..cd2bd1a --- /dev/null +++ b/src/gnunet/cmd/gnunet-service-dht-test-go/main.go @@ -0,0 +1,152 @@ +// 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 . +// +// SPDX-License-Identifier: AGPL3.0-or-later + +package main + +import ( + "context" + "flag" + "os" + "os/signal" + "strings" + "syscall" + "time" + + "gnunet/config" + "gnunet/core" + "gnunet/rpc" + "gnunet/service" + "gnunet/service/dht" + + "github.com/bfix/gospel/logger" +) + +func main() { + defer func() { + logger.Println(logger.INFO, "[dht] Bye.") + // flush last messages + logger.Flush() + }() + logger.Println(logger.INFO, "[dht] Starting service...") + + var ( + cfgFile string + socket string + param string + err error + logLevel int + rpcEndp string + ) + // handle command line arguments + flag.StringVar(&cfgFile, "c", "gnunet-config.json", "GNUnet configuration file") + flag.StringVar(&socket, "s", "", "GNS service socket") + flag.StringVar(¶m, "p", "", "socket parameters (=,...)") + flag.IntVar(&logLevel, "L", logger.INFO, "DHT log level (default: INFO)") + flag.StringVar(&rpcEndp, "R", "", "JSON-RPC endpoint (default: none)") + flag.Parse() + + // read configuration file and set missing arguments. + if err = config.ParseConfig(cfgFile); err != nil { + logger.Printf(logger.ERROR, "[dht] Invalid configuration file: %s\n", err.Error()) + return + } + + // apply configuration + logger.SetLogLevel(logLevel) + if len(socket) == 0 { + socket = config.Cfg.GNS.Service.Socket + } + params := make(map[string]string) + if len(param) == 0 { + for _, p := range strings.Split(param, ",") { + kv := strings.SplitN(p, "=", 2) + params[kv[0]] = kv[1] + } + } else { + params = config.Cfg.GNS.Service.Params + } + + // instantiate core service + ctx, cancel := context.WithCancel(context.Background()) + var local *core.Peer + if local, err = core.NewLocalPeer(config.Cfg.Local); err != nil { + logger.Printf(logger.ERROR, "[dht] No local peer: %s\n", err.Error()) + return + } + var c *core.Core + if c, err = core.NewCore(ctx, local); err != nil { + logger.Printf(logger.ERROR, "[dht] core failed: %s\n", err.Error()) + return + } + + // start a new DHT service + dht := dht.NewService(ctx, c) + srv := service.NewSocketHandler("dht", dht) + if err = srv.Start(ctx, socket, params); err != nil { + logger.Printf(logger.ERROR, "[dht] Failed to start DHT service: '%s'", err.Error()) + return + } + + // start JSON-RPC server on request + if len(rpcEndp) > 0 { + parts := strings.Split(rpcEndp, ":") + if parts[0] != "tcp" { + logger.Println(logger.ERROR, "[dht] RPC must have a TCP/IP endpoint") + return + } + config.Cfg.RPC.Endpoint = parts[1] + if err = rpc.Start(ctx); err != nil { + logger.Printf(logger.ERROR, "[dht] RPC failed to start: %s", err.Error()) + return + } + rpc.Register(dht) + } + + // handle OS signals + sigCh := make(chan os.Signal, 5) + signal.Notify(sigCh) + + // heart beat + tick := time.NewTicker(5 * time.Minute) + +loop: + for { + select { + // handle OS signals + case sig := <-sigCh: + switch sig { + case syscall.SIGKILL, syscall.SIGINT, syscall.SIGTERM: + logger.Printf(logger.INFO, "[dht] Terminating service (on signal '%s')\n", sig) + break loop + case syscall.SIGHUP: + logger.Println(logger.INFO, "[dht] SIGHUP") + case syscall.SIGURG: + // TODO: https://github.com/golang/go/issues/37942 + default: + logger.Println(logger.INFO, "[dht] Unhandled signal: "+sig.String()) + } + // handle heart beat + case now := <-tick.C: + logger.Println(logger.INFO, "[dht] Heart beat at "+now.String()) + } + } + + // terminating service + cancel() + srv.Stop() +} diff --git a/src/gnunet/cmd/gnunet-service-gns-go/main.go b/src/gnunet/cmd/gnunet-service-gns-go/main.go new file mode 100644 index 0000000..6eb027b --- /dev/null +++ b/src/gnunet/cmd/gnunet-service-gns-go/main.go @@ -0,0 +1,139 @@ +// This file is part of gnunet-go, a GNUnet-implementation in Golang. +// Copyright (C) 2019, 2020 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 . +// +// SPDX-License-Identifier: AGPL3.0-or-later + +package main + +import ( + "context" + "flag" + "os" + "os/signal" + "strings" + "syscall" + "time" + + "gnunet/config" + "gnunet/rpc" + "gnunet/service" + "gnunet/service/gns" + + "github.com/bfix/gospel/logger" +) + +func main() { + defer func() { + logger.Println(logger.INFO, "[gns] Bye.") + // flush last messages + logger.Flush() + }() + logger.Println(logger.INFO, "[gns] Starting service...") + + var ( + cfgFile string + socket string + param string + err error + logLevel int + rpcEndp string + ) + // handle command line arguments + flag.StringVar(&cfgFile, "c", "gnunet-config.json", "GNUnet configuration file") + flag.StringVar(&socket, "s", "", "GNS service socket") + flag.StringVar(¶m, "p", "", "socket parameters (=,...)") + flag.IntVar(&logLevel, "L", logger.INFO, "GNS log level (default: INFO)") + flag.StringVar(&rpcEndp, "R", "", "JSON-RPC endpoint (default: none)") + flag.Parse() + + // read configuration file and set missing arguments. + if err = config.ParseConfig(cfgFile); err != nil { + logger.Printf(logger.ERROR, "[gns] Invalid configuration file: %s\n", err.Error()) + return + } + + // apply configuration (from file and command-line) + logger.SetLogLevel(logLevel) + if len(socket) == 0 { + socket = config.Cfg.GNS.Service.Socket + } + params := make(map[string]string) + if len(param) == 0 { + for _, p := range strings.Split(param, ",") { + kv := strings.SplitN(p, "=", 2) + params[kv[0]] = kv[1] + } + } else { + params = config.Cfg.GNS.Service.Params + } + + // start a new GNS service + ctx, cancel := context.WithCancel(context.Background()) + gns := gns.NewService() + srv := service.NewSocketHandler("gns", gns) + if err = srv.Start(ctx, socket, params); err != nil { + logger.Printf(logger.ERROR, "[gns] Error: '%s'", err.Error()) + return + } + + // start JSON-RPC server on request + if len(rpcEndp) > 0 { + parts := strings.Split(rpcEndp, ":") + if parts[0] != "tcp" { + logger.Println(logger.ERROR, "[gns] RPC must have a TCP/IP endpoint") + return + } + config.Cfg.RPC.Endpoint = parts[1] + if err = rpc.Start(ctx); err != nil { + logger.Printf(logger.ERROR, "[gns] RPC failed to start: %s", err.Error()) + return + } + rpc.Register(gns) + } + + // handle OS signals + sigCh := make(chan os.Signal, 5) + signal.Notify(sigCh) + + // heart beat + tick := time.NewTicker(5 * time.Minute) + +loop: + for { + select { + // handle OS signals + case sig := <-sigCh: + switch sig { + case syscall.SIGKILL, syscall.SIGINT, syscall.SIGTERM: + logger.Printf(logger.INFO, "[gns] Terminating service (on signal '%s')\n", sig) + break loop + case syscall.SIGHUP: + logger.Println(logger.INFO, "[gns] SIGHUP") + case syscall.SIGURG: + // TODO: https://github.com/golang/go/issues/37942 + default: + logger.Println(logger.INFO, "[gns] Unhandled signal: "+sig.String()) + } + // handle heart beat + case now := <-tick.C: + logger.Println(logger.INFO, "[gns] Heart beat at "+now.String()) + } + } + + // terminating service + cancel() + srv.Stop() +} diff --git a/src/gnunet/cmd/gnunet-service-revocation-go/main.go b/src/gnunet/cmd/gnunet-service-revocation-go/main.go new file mode 100644 index 0000000..e21732c --- /dev/null +++ b/src/gnunet/cmd/gnunet-service-revocation-go/main.go @@ -0,0 +1,139 @@ +// This file is part of gnunet-go, a GNUnet-implementation in Golang. +// Copyright (C) 2019, 2020 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 . +// +// SPDX-License-Identifier: AGPL3.0-or-later + +package main + +import ( + "context" + "flag" + "os" + "os/signal" + "strings" + "syscall" + "time" + + "gnunet/config" + "gnunet/rpc" + "gnunet/service" + "gnunet/service/revocation" + + "github.com/bfix/gospel/logger" +) + +func main() { + defer func() { + logger.Println(logger.INFO, "[revocation] Bye.") + // flush last messages + logger.Flush() + }() + logger.Println(logger.INFO, "[revocation] Starting service...") + + var ( + cfgFile string + socket string + param string + err error + logLevel int + rpcEndp string + ) + // handle command line arguments + flag.StringVar(&cfgFile, "c", "gnunet-config.json", "GNUnet configuration file") + flag.StringVar(&socket, "s", "", "GNS service socket") + flag.StringVar(¶m, "p", "", "socket parameters (=,...)") + flag.IntVar(&logLevel, "L", logger.INFO, "REVOCATION log level (default: INFO)") + flag.StringVar(&rpcEndp, "R", "", "JSON-RPC endpoint (default: none)") + flag.Parse() + + // read configuration file and set missing arguments. + if err = config.ParseConfig(cfgFile); err != nil { + logger.Printf(logger.ERROR, "[revocation] Invalid configuration file: %s\n", err.Error()) + return + } + + // apply configuration + logger.SetLogLevel(logLevel) + if len(socket) == 0 { + socket = config.Cfg.GNS.Service.Socket + } + params := make(map[string]string) + if len(param) == 0 { + for _, p := range strings.Split(param, ",") { + kv := strings.SplitN(p, "=", 2) + params[kv[0]] = kv[1] + } + } else { + params = config.Cfg.GNS.Service.Params + } + + // start a new REVOCATION service + ctx, cancel := context.WithCancel(context.Background()) + rvc := revocation.NewService() + srv := service.NewSocketHandler("revocation", rvc) + if err = srv.Start(ctx, socket, params); err != nil { + logger.Printf(logger.ERROR, "[revocation] Error: '%s'\n", err.Error()) + return + } + + // start JSON-RPC server on request + if len(rpcEndp) > 0 { + parts := strings.Split(rpcEndp, ":") + if parts[0] != "tcp" { + logger.Println(logger.ERROR, "[revocation] RPC must have a TCP/IP endpoint") + return + } + config.Cfg.RPC.Endpoint = parts[1] + if err = rpc.Start(ctx); err != nil { + logger.Printf(logger.ERROR, "[revocation] RPC failed to start: %s", err.Error()) + return + } + rpc.Register(rvc) + } + + // handle OS signals + sigCh := make(chan os.Signal, 5) + signal.Notify(sigCh) + + // heart beat + tick := time.NewTicker(5 * time.Minute) + +loop: + for { + select { + // handle OS signals + case sig := <-sigCh: + switch sig { + case syscall.SIGKILL, syscall.SIGINT, syscall.SIGTERM: + logger.Printf(logger.INFO, "[revocation] Terminating service (on signal '%s')\n", sig) + break loop + case syscall.SIGHUP: + logger.Println(logger.INFO, "[revocation] SIGHUP") + case syscall.SIGURG: + // TODO: https://github.com/golang/go/issues/37942 + default: + logger.Println(logger.INFO, "[revocation] Unhandled signal: "+sig.String()) + } + // handle heart beat + case now := <-tick.C: + logger.Println(logger.INFO, "[revocation] Heart beat at "+now.String()) + } + } + + // terminating service + cancel() + srv.Stop() +} diff --git a/src/gnunet/cmd/peer_mockup/main.go b/src/gnunet/cmd/peer_mockup/main.go new file mode 100644 index 0000000..4288fb1 --- /dev/null +++ b/src/gnunet/cmd/peer_mockup/main.go @@ -0,0 +1,178 @@ +package main + +import ( + "context" + "flag" + "fmt" + "os" + "os/signal" + "syscall" + "time" + + "gnunet/config" + "gnunet/core" + "gnunet/crypto" + "gnunet/message" + "gnunet/service" + + "github.com/bfix/gospel/logger" +) + +var ( + // configuration for local node + localCfg = &config.NodeConfig{ + PrivateSeed: "YGoe6XFH3XdvFRl+agx9gIzPTvxA229WFdkazEMdcOs=", + Endpoints: []string{ + "udp:127.0.0.1:2086", + }, + } + // configuration for remote node + remoteCfg = "3GXXMNb5YpIUO7ejIR2Yy0Cf5texuLfDjHkXcqbPxkc=" + remoteAddr = "udp:172.17.0.5:2086" + + // top-level variables used accross functions + local *core.Peer // local peer (with private key) + remote *core.Peer // remote peer + c *core.Core + secret *crypto.HashCode +) + +func main() { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // handle command line arguments + var ( + asServer bool + err error + ) + flag.BoolVar(&asServer, "s", false, "wait for incoming connections") + flag.Parse() + + // setup peer and core instances + if local, err = core.NewLocalPeer(localCfg); err != nil { + fmt.Println("local failed: " + err.Error()) + return + } + if c, err = core.NewCore(ctx, local); err != nil { + fmt.Println("core failed: " + err.Error()) + return + } + if remote, err = core.NewPeer(remoteCfg); err != nil { + fmt.Println("remote failed: " + err.Error()) + return + } + + fmt.Println("======================================================================") + fmt.Println("GNUnet peer mock-up (EXPERIMENTAL) (c) 2018-2022 by Bernd Fix, >Y<") + fmt.Printf(" Identity '%s'\n", local.GetIDString()) + fmt.Printf(" [%s]\n", local.GetID().String()) + fmt.Println("======================================================================") + + // handle messages coming from network + module := service.NewModuleImpl() + listener := module.Run(ctx, process, nil) + c.Register("mockup", listener) + + if !asServer { + // we start the message exchange + c.Send(ctx, remote.GetID(), message.NewTransportTCPWelcomeMsg(c.PeerID())) + } + + // handle OS signals + sigCh := make(chan os.Signal, 5) + signal.Notify(sigCh) + + // heart beat + tick := time.NewTicker(5 * time.Minute) + +loop: + for { + select { + // handle OS signals + case sig := <-sigCh: + switch sig { + case syscall.SIGKILL, syscall.SIGINT, syscall.SIGTERM: + logger.Printf(logger.INFO, "Terminating service (on signal '%s')\n", sig) + break loop + case syscall.SIGHUP: + logger.Println(logger.INFO, "SIGHUP") + case syscall.SIGURG: + // TODO: https://github.com/golang/go/issues/37942 + default: + logger.Println(logger.INFO, "Unhandled signal: "+sig.String()) + } + // handle heart beat + case now := <-tick.C: + logger.Println(logger.INFO, "Heart beat at "+now.String()) + } + } + // terminate pending routines + cancel() +} + +// 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)) + + 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") + return + } + c.Send(ctx, ev.Peer, mOut) + logger.Printf(logger.DBG, ">>> %s", mOut) + + case *message.TransportPongMsg: + rc, err := msg.Verify(remote.PubKey()) + if err != nil { + logger.Println(logger.ERROR, "PONG verification: "+err.Error()) + } + if !rc { + logger.Println(logger.ERROR, "PONG verification failed") + } + + case *message.SessionSynMsg: + mOut := message.NewSessionSynAckMsg() + mOut.Timestamp = msg.Timestamp + c.Send(ctx, ev.Peer, mOut) + logger.Printf(logger.DBG, ">>> %s", mOut) + + case *message.SessionQuotaMsg: + + case *message.SessionAckMsg: + + case *message.SessionKeepAliveMsg: + mOut := message.NewSessionKeepAliveRespMsg(msg.Nonce) + c.Send(ctx, ev.Peer, mOut) + logger.Printf(logger.DBG, ">>> %s", mOut) + + case *message.EphemeralKeyMsg: + rc, err := msg.Verify(remote.PubKey()) + if err != nil { + logger.Println(logger.ERROR, "EPHKEY verification: "+err.Error()) + return + } else if !rc { + logger.Println(logger.ERROR, "EPHKEY verification failed") + return + } + remote.SetEphKeyMsg(msg) + mOut := local.EphKeyMsg() + c.Send(ctx, ev.Peer, mOut) + logger.Printf(logger.DBG, ">>> %s", mOut) + secret = crypto.SharedSecret(local.EphPrvKey(), remote.EphKeyMsg().Public()) + + default: + fmt.Printf("!!! %v\n", msg) + } +} diff --git a/src/gnunet/cmd/revoke-zonekey/main.go b/src/gnunet/cmd/revoke-zonekey/main.go new file mode 100644 index 0000000..298a7e4 --- /dev/null +++ b/src/gnunet/cmd/revoke-zonekey/main.go @@ -0,0 +1,336 @@ +// This file is part of gnunet-go, a GNUnet-implementation in Golang. +// Copyright (C) 2019, 2020 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 . +// +// SPDX-License-Identifier: AGPL3.0-or-later + +package main + +import ( + "context" + "encoding/base64" + "flag" + "fmt" + "log" + "os" + "os/signal" + "sync" + "syscall" + + "gnunet/crypto" + "gnunet/service/revocation" + "gnunet/util" + + "github.com/bfix/gospel/data" +) + +//---------------------------------------------------------------------- +// Data structure used to calculate a valid revocation for a given +// zone key. +//---------------------------------------------------------------------- + +// 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 +) + +// RevData is the storage layout for persistent data used by this program. +// Data is read from and written to a file +type RevData struct { + Rd *revocation.RevDataCalc `` // Revocation data + T util.RelativeTime `` // time spend in calculations + Last uint64 `order:"big"` // last value used for PoW test + Numbits uint8 `` // number of leading zero-bits (difficulty) + State uint8 `` // processing state +} + +// ReadRevData restores revocation data from perstistent storage. If no +// stored data is found, a new revocation data structure is returned. +func ReadRevData(filename string, bits int, zk *crypto.ZoneKey) (rd *RevData, err error) { + // create new initialized revocation instance with no PoWs. + rd = &RevData{ + Rd: revocation.NewRevDataCalc(zk), + Numbits: uint8(bits), + T: util.NewRelativeTime(0), + State: S_NEW, + } + + // read revocation object from file. If the file does not exist, a new + // calculation is started; otherwise the old calculation will continue. + var file *os.File + if file, err = os.Open(filename); err != nil { + return + } + // read existing file + dataBuf := make([]byte, rd.size()) + var n int + if n, err = file.Read(dataBuf); err != nil { + err = fmt.Errorf("Error reading file: " + err.Error()) + return + } + if n != len(dataBuf) { + err = fmt.Errorf("File size mismatch") + return + } + if err = data.Unmarshal(&rd, dataBuf); err != nil { + err = fmt.Errorf("File corrupted: " + err.Error()) + return + } + if !zk.Equal(&rd.Rd.RevData.ZoneKeySig.ZoneKey) { + 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()) + } + return +} + +// Write revocation data to file +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()) + } + var buf []byte + if buf, err = data.Marshal(r); err != nil { + 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()) + } + var n int + if n, err = file.Write(buf); err != nil { + 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!") + } + if err = file.Close(); err != nil { + return fmt.Errorf("Error closing file: " + err.Error()) + } + return +} + +// size of the RevData instance in bytes. +func (r *RevData) size() int { + return 18 + r.Rd.Size() +} + +// revoke-zonekey generates a revocation message in a multi-step/multi-state +// process run stand-alone from other GNUnet services: +// +// (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 +// 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. +// +// +// (2) A fully generated PoW set can be signed with the private key to create +// the final revocation data to be send out. This requires to pass the "-k" +// and "-z" argument. +// +// The two steps can be run (sequentially) on separate machines; step one requires +// computing power nd memory and step two requires a trusted environment. +func main() { + log.Println("*** Compute revocation data for a zone key") + log.Println("*** Copyright (c) 2020-2022, Bernd Fix >Y<") + log.Println("*** This is free software distributed under the Affero GPL v3.") + + //------------------------------------------------------------------ + // handle command line arguments + //------------------------------------------------------------------ + var ( + verbose bool // be verbose with messages + bits int // number of leading zero-bit requested + 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 + ) + minDiff := revocation.MinDifficulty + flag.IntVar(&bits, "b", minDiff+1, "Number of leading zero bits") + flag.StringVar(&zonekey, "z", "", "Zone key to be revoked (zone ID)") + flag.StringVar(&prvkey, "k", "", "Private zone key (base54-encoded)") + flag.StringVar(&filename, "f", "", "Name of file to store revocation") + flag.BoolVar(&verbose, "v", false, "verbose output") + flag.BoolVar(&testing, "t", false, "test-mode only") + flag.Parse() + + // check arguments (difficulty, zonekey and filename) + if bits < minDiff { + if testing { + log.Printf("WARNING: difficulty is less than %d!", minDiff) + } else { + log.Printf("INFO: difficulty set to %d (required minimum)", minDiff) + bits = minDiff + } + } + if len(filename) == 0 { + log.Fatal("Missing '-f' argument (filename for revocation data)") + } + + //------------------------------------------------------------------ + // Handle zone keys. + //------------------------------------------------------------------ + var ( + keyData []byte // binary key data + zk *crypto.ZoneKey // GNUnet zone key + sk *crypto.ZonePrivate // GNUnet private zone key + err error + ) + // reconstruct public key + if keyData, err = util.DecodeStringToBinary(zonekey, 32); err != nil { + log.Fatal("Invalid zonekey encoding: " + err.Error()) + } + if zk, err = crypto.NewZoneKey(keyData); err != nil { + log.Fatal("Invalid zonekey format: " + err.Error()) + } + // reconstruct private key (optional) + if len(prvkey) > 0 { + if keyData, err = base64.StdEncoding.DecodeString(prvkey); err != nil { + log.Fatal("Invalid private zonekey encoding: " + err.Error()) + } + if sk, err = crypto.NewZonePrivate(zk.Type, keyData); err != nil { + log.Fatal("Invalid zonekey format: " + err.Error()) + } + // verify consistency + if !zk.Equal(sk.Public()) { + log.Fatal("Public and private zone keys don't match.") + } + } + + //------------------------------------------------------------------ + // Read revocation data from file to continue calculation or to sign + // the revocation. If no file exists, a new (empty) instance is + // returned. + //------------------------------------------------------------------ + rd, err := ReadRevData(filename, bits, zk) + + // handle revocation data state + switch rd.State { + case S_NEW: + log.Println("Starting new revocation calculation...") + rd.State = S_CONT + + case S_CONT: + 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: + // 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 { + log.Fatal("Failed to sign revocation: " + err.Error()) + } + // write final revocation + rd.State = S_SIGNED + if err = rd.Write(filename); err != nil { + log.Fatal("Failed to write revocation: " + err.Error()) + } + log.Println("Revocation complete and ready for (later) use.") + return + } + // Continue (or start) calculation + log.Println("Press ^C to abort...") + log.Printf("Difficulty: %d\n", bits) + + ctx, cancelFcn := context.WithCancel(context.Background()) + wg := new(sync.WaitGroup) + wg.Add(1) + go func() { + defer wg.Done() + // show progress messages + cb := func(average float64, last uint64) { + log.Printf("Improved PoW: %.2f average zero bits, %d steps\n", average, last) + } + + // calculate revocation data until the required difficulty is met + // or the process is terminated by the user (by pressing ^C). + startTime := util.AbsoluteTimeNow() + average, last := rd.Rd.Compute(ctx, bits, rd.Last, cb) + + // check achieved diffiulty (average) + if average < float64(bits) { + // 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 + } else { + // we have reached the required PoW difficulty + rd.State = S_DONE + // check if we have a valid revocation. + log.Println("Revocation calculation complete:") + diff, rc := rd.Rd.Verify(false) + switch { + case rc == -1: + log.Println(" Missing/invalid signature") + case rc == -2: + log.Println(" Expired revocation") + case rc == -3: + log.Println(" Wrong PoW sequence order") + case diff < float64(revocation.MinAvgDifficulty): + log.Println(" Difficulty to small") + default: + log.Printf(" Difficulty is %.2f\n", diff) + } + } + // update elapsed time + rd.T.Add(util.AbsoluteTimeNow().Diff(startTime)) + rd.Last = last + + log.Println("Writing revocation data to file...") + if err = rd.Write(filename); err != nil { + log.Fatal("Can't write to file: " + err.Error()) + } + }() + + go func() { + // handle OS signals + sigCh := make(chan os.Signal, 5) + signal.Notify(sigCh) + loop: + for { + select { + // 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()) + } + } + } + }() + wg.Wait() +} diff --git a/src/gnunet/cmd/vanityid/main.go b/src/gnunet/cmd/vanityid/main.go new file mode 100644 index 0000000..938df61 --- /dev/null +++ b/src/gnunet/cmd/vanityid/main.go @@ -0,0 +1,53 @@ +package main + +import ( + "crypto/rand" + "encoding/hex" + "flag" + "fmt" + "regexp" + "time" + + "github.com/bfix/gospel/crypto/ed25519" + "gnunet/util" +) + +func main() { + // get arguments + flag.Parse() + prefixes := flag.Args() + num := len(prefixes) + if num == 0 { + fmt.Println("No prefixes specified -- done.") + return + } + + // pre-compile regexp + reg := make([]*regexp.Regexp, num) + for i, p := range prefixes { + reg[i] = regexp.MustCompile(p) + } + + // generate new keys in a loop + seed := make([]byte, 32) + start := time.Now() + for i := 0; ; i++ { + n, err := rand.Read(seed) + if err != nil || n != 32 { + panic(err) + } + 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) + 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) + i = 0 + start = time.Now() + } + } + } +} diff --git a/src/gnunet/config/config.go b/src/gnunet/config/config.go index 914a017..41a65f0 100644 --- a/src/gnunet/config/config.go +++ b/src/gnunet/config/config.go @@ -28,56 +28,96 @@ import ( "github.com/bfix/gospel/logger" ) -/////////////////////////////////////////////////////////////////////// +//---------------------------------------------------------------------- +// Configuration for local node +//---------------------------------------------------------------------- + +// NodeConfig holds parameters for the local node instance +type NodeConfig struct { + PrivateSeed string `json:"privateSeed"` // Node private key seed (base64) + Endpoints []string `json:"endpoints"` // list of endpoints available +} + +//---------------------------------------------------------------------- +// Bootstrap configuration +//---------------------------------------------------------------------- + +// BootstrapConfig holds parameters for the initial connection to the network. +type BootstrapConfig struct { + Nodes []string `json:"nodes"` // bootstrap nodes +} + +//---------------------------------------------------------------------- // RPC configuration +//---------------------------------------------------------------------- // RPCConfig contains parameters for the JSON-RPC service type RPCConfig struct { - Endpoint string `json:"endpoint"` // end-point of JSON-RPC service + Endpoint string `json:"endpoint"` // endpoint for JSON-RPC service +} + +//---------------------------------------------------------------------- +// Generic service endpoint configuration (socket) +//---------------------------------------------------------------------- + +type ServiceConfig struct { + Socket string `json:"socket"` // socket file name + Params map[string]string `json:"params"` // socket parameters } -/////////////////////////////////////////////////////////////////////// +//---------------------------------------------------------------------- // GNS configuration +//---------------------------------------------------------------------- // GNSConfig contains parameters for the GNU Name System service type GNSConfig struct { - Endpoint string `json:"endpoint"` // end-point of GNS service - DHTReplLevel int `json:"dhtReplLevel"` // DHT replication level - MaxDepth int `json:"maxDepth"` // maximum recursion depth in resolution + Service *ServiceConfig `json:"service"` // socket for GNS service + DHTReplLevel int `json:"dhtReplLevel"` // DHT replication level + MaxDepth int `json:"maxDepth"` // maximum recursion depth in resolution } -/////////////////////////////////////////////////////////////////////// +//---------------------------------------------------------------------- // DHT configuration +//---------------------------------------------------------------------- // DHTConfig contains parameters for the distributed hash table (DHT) type DHTConfig struct { - Endpoint string `json:"endpoint"` // end-point of DHT service + Service *ServiceConfig `json:"service"` // socket for DHT service + Storage string `json:"storage"` // filesystem storage location + Cache string `json:"cache"` // key/value cache } -/////////////////////////////////////////////////////////////////////// +//---------------------------------------------------------------------- // Namecache configuration +//---------------------------------------------------------------------- // NamecacheConfig contains parameters for the local name cache type NamecacheConfig struct { - Endpoint string `json:"endpoint"` // end-point of Namecache service + Service *ServiceConfig `json:"service"` // socket for Namecache service + Storage string `json:"storage"` // key/value cache } -/////////////////////////////////////////////////////////////////////// +//---------------------------------------------------------------------- // Revocation configuration +//---------------------------------------------------------------------- // RevocationConfig contains parameters for the key revocation service type RevocationConfig struct { - Endpoint string `json:"endpoint"` // end-point of Revocation service - Storage string `json:"storage"` // persistance mechanism for revocation data + Service *ServiceConfig `json:"service"` // socket for Revocation service + Storage string `json:"storage"` // persistance mechanism for revocation data } -/////////////////////////////////////////////////////////////////////// +//---------------------------------------------------------------------- +// Combined configuration +//---------------------------------------------------------------------- // Environment settings type Environment map[string]string // Config is the aggregated configuration for GNUnet. type Config struct { + Local *NodeConfig `json:"local"` + Bootstrap *BootstrapConfig `json:"bootstrap"` Env Environment `json:"environ"` RPC *RPCConfig `json:"rpc"` DHT *DHTConfig `json:"dht"` diff --git a/src/gnunet/config/gnunet-config.json b/src/gnunet/config/gnunet-config.json index daf65f9..941cf21 100644 --- a/src/gnunet/config/gnunet-config.json +++ b/src/gnunet/config/gnunet-config.json @@ -1,24 +1,58 @@ { + "local": { + "privateSeed": "YGoe6XFH3XdvFRl+agx9gIzPTvxA229WFdkazEMdcOs=", + "endpoints": [ + "r5n+ip+udp:127.0.0.1:6666" + ] + }, + "bootstrap": { + "nodes": [ + "gnunet://hello/7KTBJ90340HF1Q2GB0A57E2XJER4FDHX8HP5GHEB9125VPWPD27G/BNMDFN6HJCPWSPNBSEC06MC1K8QN1Z2DHRQSRXDTFR7FTBD4JHNBJ2RJAAEZ31FWG1Q3PMN3PXGZQ3Q7NTNEKQZFA7TE2Y46FM8E20R/1653499308?r5n%2Bip%2Budp%3A127.0.0.1%3A7654" + ] + }, "environ": { "TMP": "/tmp", "RT_SYS": "${TMP}/gnunet-system-runtime" }, "dht": { - "endpoint": "unix+${RT_SYS}/gnunet-service-dht.sock" + "service": { + "socket": "${RT_SYS}/gnunet-service-dht.sock", + "params": { + "perm": "0770" + } + }, + "storage": "dht_file_store+/var/lib/gnunet/dht/store", + "cache": "dht_file_cache+/var/lib/gnunet/dht/cache+1000" }, "gns": { - "endpoint": "unix+${RT_SYS}/gnunet-service-gns-go.sock+perm=0770", + "service": { + "socket": "${RT_SYS}/gnunet-service-gns-go.sock", + "params": { + "perm": "0770" + } + }, "dhtReplLevel": 10, "maxDepth": 250 }, "namecache": { - "endpoint": "unix+${RT_SYS}/gnunet-service-namecache.sock" + "service": { + "socket": "${RT_SYS}/gnunet-service-namecache.sock", + "params": { + "perm": "0770" + } + }, + "storage": "dht_file_cache:/var/lib/gnunet/namecache:1000" }, "revocation": { - "endpoint": "unix+${RT_SYS}/gnunet-service-revocation-go.sock+perm=0770", - "storage": "redis+localhost:6397++15" + "service": { + "socket": "${RT_SYS}/gnunet-service-revocation-go.sock", + "params": { + "perm": "0770" + } + }, + "storage": "redis:localhost:6397::15" }, "rpc": { - "endpoint": "tcp+127.0.0.1:80" + "endpoint": "tcp:127.0.0.1:80" } -} +} \ No newline at end of file diff --git a/src/gnunet/core/core.go b/src/gnunet/core/core.go new file mode 100644 index 0000000..c3bf355 --- /dev/null +++ b/src/gnunet/core/core.go @@ -0,0 +1,234 @@ +// 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 . +// +// SPDX-License-Identifier: AGPL3.0-or-later + +package core + +import ( + "context" + "gnunet/message" + "gnunet/service/dht/blocks" + "gnunet/transport" + "gnunet/util" + "net" + "time" + + "github.com/bfix/gospel/data" +) + +// Core service +type Core struct { + // local peer instance + local *Peer + + // incoming messages from transport + incoming chan *transport.TransportMessage + + // reference to transport implementation + trans *transport.Transport + + // registered listeners + listeners map[string]*Listener + + // list of known peers with addresses + peers *util.PeerAddrList +} + +//---------------------------------------------------------------------- + +// NewCore creates and runs a new core instance. +func NewCore(ctx context.Context, local *Peer) (c *Core, err error) { + // create new core instance + incoming := make(chan *transport.TransportMessage) + c = &Core{ + local: local, + incoming: incoming, + listeners: make(map[string]*Listener), + trans: transport.NewTransport(ctx, incoming), + peers: util.NewPeerAddrList(), + } + // add all local peer endpoints to transport. + for _, addr := range local.addrList { + if _, err = c.trans.AddEndpoint(ctx, addr); err != nil { + return + } + } + // run message pump + go func() { + // wait for incoming messages + for { + select { + // get (next) message from transport + case tm := <-c.incoming: + var ev *Event + + // inspect message for peer state events + m, err := tm.Message() + if err == nil { + switch msg := m.(type) { + case *message.HelloMsg: + // keep peer addresses + for _, addr := range msg.Addresses { + a := &util.Address{ + Netw: addr.Transport, + Address: addr.Address, + Expires: addr.ExpireOn, + } + c.Learn(ctx, msg.PeerID, a) + } + // generate EV_CONNECT event + ev = new(Event) + ev.ID = EV_CONNECT + ev.Peer = tm.Peer + ev.Msg = msg + c.dispatch(ev) + } + } + // generate EV_MESSAGE event + ev = new(Event) + ev.ID = EV_MESSAGE + ev.Peer = tm.Peer + ev.Msg, _ = tm.Message() + c.dispatch(ev) + + // wait for termination + case <-ctx.Done(): + return + } + } + }() + return +} + +//---------------------------------------------------------------------- + +// Send is a function that allows the local peer to send a protocol +// message to a remote peer. +func (c *Core) Send(ctx context.Context, peer *util.PeerID, msg message.Message) error { + // TODO: select best endpoint protocol for transport; now fixed to UDP + netw := "udp" + addr := c.peers.Get(peer.String(), netw) + payload, err := data.Marshal(msg) + if err != nil { + return err + } + tm := transport.NewTransportMessage(c.PeerID(), payload) + return c.trans.Send(ctx, addr, tm) +} + +// Learn a (new) address for peer +func (c *Core) Learn(ctx context.Context, peer *util.PeerID, addr *util.Address) (err error) { + if c.peers.Add(peer.String(), addr) == 1 { + // new peer id: send HELLO message to newly added peer + node := c.local + var hello *blocks.HelloBlock + hello, err = node.HelloData(time.Hour) + if err != nil { + return + } + msg := message.NewHelloMsg(node.GetID()) + for _, a := range hello.Addresses() { + ha := message.NewHelloAddress(a) + msg.AddAddress(ha) + } + err = c.Send(ctx, peer, msg) + } + return +} + +// PeerID returns the peer id of the local node. +func (c *Core) PeerID() *util.PeerID { + return c.local.GetID() +} + +// 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 +// peer is offered through the PEER_CONNECTED signal. +func (c *Core) TryConnect(peer *util.PeerID, addr net.Addr) error { + // select endpoint for address + if ep := c.findEndpoint(peer, addr); ep == nil { + return transport.ErrTransNoEndpoint + } + return nil +} + +func (c *Core) findEndpoint(peer *util.PeerID, addr net.Addr) transport.Endpoint { + return nil +} + +// Hold is a function which tells the underlay to keep a hold on to a +// connection to a peer P. Underlays are usually limited in the number +// of active connections. With this function the DHT can indicate to the +// underlay which connections should preferably be preserved. +func (c *Core) Hold(peer *util.PeerID) {} + +// Drop is a function which tells the underlay to drop the connection to a +// peer P. This function is only there for symmetry and used during the +// peer's shutdown to release all of the remaining HOLDs. As R5N always +// prefers the longest-lived connections, it would never drop an active +// connection that it has called HOLD() on before. Nevertheless, underlay +// implementations should not rely on this always being true. A call to +// DROP() also does not imply that the underlay must close the connection: +// it merely removes the preference to preserve the connection that was +// 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. +//---------------------------------------------------------------------- + +// Register a named event listener. +func (c *Core) Register(name string, l *Listener) { + c.listeners[name] = l +} + +// Unregister named event listener. +func (c *Core) Unregister(name string) *Listener { + if l, ok := c.listeners[name]; ok { + delete(c.listeners, name) + return l + } + return nil +} + +// internal: dispatch event to listeners +func (c *Core) dispatch(ev *Event) { + // 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 { + if mt != 0 && !l.filter.CheckMsgType(mt) { + // skip event + return + } + } + go func() { + l.ch <- ev + }() + } + } +} diff --git a/src/gnunet/core/core_test.go b/src/gnunet/core/core_test.go new file mode 100644 index 0000000..102abf1 --- /dev/null +++ b/src/gnunet/core/core_test.go @@ -0,0 +1,150 @@ +// 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 . +// +// SPDX-License-Identifier: AGPL3.0-or-later + +package core + +import ( + "context" + "gnunet/config" + "gnunet/util" + "testing" + "time" +) + +var ( + peer1Cfg = &config.NodeConfig{ + PrivateSeed: "iYK1wSi5XtCP774eNFk1LYXqKlOPEpwKBw+2/bMkE24=", + Endpoints: []string{"udp://127.0.0.1:20861"}, + } + + peer2Cfg = &config.NodeConfig{ + PrivateSeed: "Bv9umksEO51jjWWrOGEH+4r8wl9Vi+LItpdBpTOi2PE=", + Endpoints: []string{"udp://127.0.0.1:20862"}, + } +) + +//---------------------------------------------------------------------- +// create and run a node with given spec +//---------------------------------------------------------------------- + +type TestNode struct { + id int + t *testing.T + peer *Peer + core *Core + addr *util.Address +} + +func (n *TestNode) Learn(ctx context.Context, peer *util.PeerID, addr *util.Address) { + n.t.Logf("[%d] Learning %s for %s", n.id, addr.StringAll(), peer.String()) + n.core.Learn(ctx, peer, addr) +} + +func NewTestNode(t *testing.T, ctx context.Context, cfg *config.NodeConfig) (node *TestNode, err error) { + + // create test node + node = new(TestNode) + node.t = t + node.id = util.NextID() + + // create peer object + if node.peer, err = NewLocalPeer(cfg); err != nil { + return + } + t.Logf("[%d] Node %s starting", node.id, node.peer.GetID()) + + // create core service + if node.core, err = NewCore(ctx, node.peer); err != nil { + return + } + for _, addr := range node.core.trans.Endpoints() { + s := addr.Network() + ":" + addr.String() + if node.addr, err = util.ParseAddress(s); err != nil { + continue + } + t.Logf("[%d] Listening on %s", node.id, s) + } + + // register as event listener + incoming := make(chan *Event) + node.core.Register("test", 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 EV_CONNECT: + t.Logf("[%d] <<< Peer %s connected", node.id, ev.Peer) + case EV_DISCONNECT: + t.Logf("[%d] <<< Peer %s diconnected", node.id, ev.Peer) + case EV_MESSAGE: + t.Logf("[%d] <<< Msg from %s of type %d", node.id, ev.Peer, ev.Msg.Header().MsgType) + } + + // handle termination signal + case <-ctx.Done(): + t.Logf("[%d] Shutting down node", node.id) + return + + // handle heart beat + case now := <-tick.C: + t.Logf("[%d] Heart beat at %s", node.id, now.String()) + } + } + }() + return +} + +//---------------------------------------------------------------------- +// Two node GNUnet (smallest and simplest network) +//---------------------------------------------------------------------- + +// TestCoreSimple test a two node network +func TestCoreSimple(t *testing.T) { + + // setup execution context + ctx, cancel := context.WithCancel(context.Background()) + defer func() { + cancel() + time.Sleep(time.Second) + }() + + // create and run nodes + node1, err := NewTestNode(t, ctx, peer1Cfg) + if err != nil { + t.Fatal(err) + } + node2, err := NewTestNode(t, ctx, peer2Cfg) + if err != nil { + t.Fatal(err) + } + + // learn peer addresses (triggers HELLO) + for _, addr := range node2.core.trans.Endpoints() { + node1.Learn(ctx, node2.peer.GetID(), util.NewAddressWrap(addr)) + } + + // wait for 5 seconds + time.Sleep(5 * time.Second) +} diff --git a/src/gnunet/core/event.go b/src/gnunet/core/event.go new file mode 100644 index 0000000..4eab112 --- /dev/null +++ b/src/gnunet/core/event.go @@ -0,0 +1,109 @@ +// 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 . +// +// SPDX-License-Identifier: AGPL3.0-or-later + +package core + +import ( + "gnunet/message" + "gnunet/util" +) + +//---------------------------------------------------------------------- +// Core events and listeners +//---------------------------------------------------------------------- + +// Event types +const ( + EV_CONNECT = iota // peer connected + EV_DISCONNECT // peer disconnected + EV_MESSAGE // incoming message +) + +// EventFilter is a filter for events a listener is interested in. +// The filter works on event types; if EV_MESSAGE is set, messages +// can be filtered by message type also. +type EventFilter struct { + evTypes map[int]bool + msgTypes map[uint16]bool +} + +// NewEventFilter creates a new empty filter instance. +func NewEventFilter() *EventFilter { + return &EventFilter{ + evTypes: make(map[int]bool), + msgTypes: make(map[uint16]bool), + } +} + +// AddEvent add an event id to filter +func (f *EventFilter) AddEvent(ev int) { + f.evTypes[ev] = true +} + +// AddMsgType adds a message type to filter +func (f *EventFilter) AddMsgType(mt uint16) { + f.evTypes[EV_MESSAGE] = true + f.msgTypes[mt] = true +} + +// CheckEvent returns true if an event id is matched +// by the filter or the filter is empty. +func (f *EventFilter) CheckEvent(ev int) bool { + if len(f.evTypes) == 0 { + return true + } + _, ok := f.evTypes[ev] + return ok +} + +// CheckMsgType returns true if a message type is matched +// by the filter or the filter is empty. +func (f *EventFilter) CheckMsgType(mt uint16) bool { + if len(f.msgTypes) == 0 { + return true + } + _, ok := f.msgTypes[mt] + return ok +} + +// Event sent to listeners +type Event struct { + ID int // event type + Peer *util.PeerID // remote peer + Msg message.Message // GNUnet message (can be nil) +} + +//---------------------------------------------------------------------- + +// Listener for network events +type Listener struct { + ch chan *Event // listener channel + filter *EventFilter // event filter settimgs +} + +// NewListener for given filter and receiving channel +func NewListener(ch chan *Event, f *EventFilter) *Listener { + if f == nil { + // set empty default filter + f = NewEventFilter() + } + return &Listener{ + ch: ch, + filter: f, + } +} diff --git a/src/gnunet/core/peer.go b/src/gnunet/core/peer.go index f81bab8..2b6fe74 100644 --- a/src/gnunet/core/peer.go +++ b/src/gnunet/core/peer.go @@ -1,5 +1,5 @@ // This file is part of gnunet-go, a GNUnet-implementation in Golang. -// Copyright (C) 2019, 2020 Bernd Fix >Y< +// 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 @@ -19,14 +19,30 @@ package core import ( + "encoding/base64" "fmt" + "time" + "gnunet/config" "gnunet/message" + "gnunet/service/dht/blocks" "gnunet/util" "github.com/bfix/gospel/crypto/ed25519" ) +//---------------------------------------------------------------------- +// GNUnet P2P network node (local or remote): +// +// * A LOCAL node has a long-term EdDSA key pair used for signing. The +// public key is the node identifier (PeerID). +// Local nodes hold additional attributes like ephemeral keys for message +// exchange or a list of network addresses the node can be reached on. +// +// * A REMOTE node only has a public EdDSA key used by the local node +// to verify signatures from the remote node. +//---------------------------------------------------------------------- + // Peer represents a node in the GNUnet P2P network. type Peer struct { prv *ed25519.PrivateKey // node private key (long-term signing key) @@ -37,27 +53,87 @@ type Peer struct { ephMsg *message.EphemeralKeyMsg // ephemeral signing key message } -// NewPeer instantiates a new peer object from given data. If a local peer -// is created, the data is the seed for generating the private key of the node; -// for a remote peer the data is the binary representation of its public key. -func NewPeer(data []byte, local bool) (p *Peer, err error) { +//---------------------------------------------------------------------- +// Create new peer objects +//---------------------------------------------------------------------- + +// NewLocalPeer creates a new local node from configuration data. +func NewLocalPeer(cfg *config.NodeConfig) (p *Peer, err error) { p = new(Peer) - if local { - p.prv = ed25519.NewPrivateKeyFromSeed(data) - p.pub = p.prv.Public() - p.ephPrv, p.ephMsg, err = message.NewEphemeralKey(p.pub.Bytes(), p.prv) - if err != nil { + + // get the key material for local node + var data []byte + if data, err = base64.StdEncoding.DecodeString(cfg.PrivateSeed); err != nil { + return + } + p.prv = ed25519.NewPrivateKeyFromSeed(data) + p.pub = p.prv.Public() + p.idString = util.EncodeBinaryToString(p.pub.Bytes()) + p.ephPrv, p.ephMsg, err = message.NewEphemeralKey(p.pub.Bytes(), p.prv) + if err != nil { + return + } + // set the endpoint addresses for local node + p.addrList = make([]*util.Address, len(cfg.Endpoints)) + var addr *util.Address + for i, a := range cfg.Endpoints { + if addr, err = util.ParseAddress(a); err != nil { return } - } else { - p.prv = nil - p.pub = ed25519.NewPublicKeyFromBytes(data) + addr.Expires = util.NewAbsoluteTime(time.Now().Add(12 * time.Hour)) + p.addrList[i] = addr } + return +} + +// NewPeer instantiates a new (remote) peer object from given peer ID string. +func NewPeer(peerID string) (p *Peer, err error) { + p = new(Peer) + + // get the key material for local node + var data []byte + if data, err = util.DecodeStringToBinary(peerID, 32); err != nil { + return + } + p.prv = nil + p.pub = ed25519.NewPublicKeyFromBytes(data) p.idString = util.EncodeBinaryToString(p.pub.Bytes()) p.addrList = make([]*util.Address, 0) return } +//---------------------------------------------------------------------- +//---------------------------------------------------------------------- + +// Address returns a peer address for the given transport protocol +func (p *Peer) Address(transport string) *util.Address { + for _, addr := range p.addrList { + // skip expired entries + if addr.Expires.Expired() { + continue + } + // filter by transport protocol + if len(transport) > 0 && transport != addr.Netw { + continue + } + return addr + } + return nil +} + +// HelloData returns the current HELLO data for the peer +func (p *Peer) HelloData(ttl time.Duration) (h *blocks.HelloBlock, err error) { + // assemble HELLO data + h = new(blocks.HelloBlock) + h.PeerID = p.GetID() + h.Expire = util.NewAbsoluteTime(time.Now().Add(ttl)) + h.SetAddresses(p.addrList) + + // sign data + err = h.Sign(p.prv) + return +} + // EphKeyMsg returns a new initialized message to negotiate session keys. func (p *Peer) EphKeyMsg() *message.EphemeralKeyMsg { return p.ephMsg @@ -84,8 +160,10 @@ func (p *Peer) PubKey() *ed25519.PublicKey { } // GetID returns the node ID (public key) in binary format -func (p *Peer) GetID() []byte { - return p.pub.Bytes() +func (p *Peer) GetID() *util.PeerID { + return &util.PeerID{ + Key: util.Clone(p.pub.Bytes()), + } } // GetIDString returns the string representation of the public key of the node. diff --git a/src/gnunet/core/peer_test.go b/src/gnunet/core/peer_test.go new file mode 100644 index 0000000..28b328f --- /dev/null +++ b/src/gnunet/core/peer_test.go @@ -0,0 +1,72 @@ +// 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 . +// +// SPDX-License-Identifier: AGPL3.0-or-later + +package core + +import ( + "gnunet/config" + "gnunet/service/dht/blocks" + "testing" + "time" +) + +// test data +var ( + cfg = &config.NodeConfig{ + PrivateSeed: "YGoe6XFH3XdvFRl+agx9gIzPTvxA229WFdkazEMdcOs=", + Endpoints: []string{ + "r5n+ip+udp://127.0.0.1:6666", + }, + } + TTL = 6 * time.Hour +) + +func TestPeerHello(t *testing.T) { + + // generate new local node + node, err := NewLocalPeer(cfg) + if err != nil { + t.Fatal(err) + } + + // get HELLO data for the node + h, err := node.HelloData(TTL) + + // convert to URL and back + u := h.URL() + t.Log(u) + h2, err := blocks.ParseHelloURL(u) + if err != nil { + t.Fatal(err) + } + u2 := h2.URL() + t.Log(u2) + + // check if HELLO data is the same + if !h.Equals(h2) { + t.Fatal("HELLO data mismatch") + } + // verify signature + ok, err := h.Verify() + if err != nil { + t.Fatal(err) + } + if !ok { + t.Fatal("failed to verify signature") + } +} diff --git a/src/gnunet/crypto/gns.go b/src/gnunet/crypto/gns.go index 6aa7972..f4c5627 100644 --- a/src/gnunet/crypto/gns.go +++ b/src/gnunet/crypto/gns.go @@ -58,7 +58,8 @@ import ( // example the RSA crypto scheme is outlined: // // (1) Register/define a new GNS_TYPE_RSAKEY -// (2) Add ZONE_RSAKEY to the "Zone types" declarations below. +// (2) Add ZONE_RSAKEY and GNS_TYPE_RSAKEY to the "Zone types" +// declarations in this file. // (3) Code the implementation in a file named `gns_rsakey.go`: // You have to implement three interfaces (ZonePrivateImpl, // ZoneKeyImpl and ZoneSigImpl) in three separate custom types. @@ -145,6 +146,12 @@ type ZoneSigImpl interface { var ( ZONE_PKEY = uint32(enums.GNS_TYPE_PKEY) ZONE_EDKEY = uint32(enums.GNS_TYPE_EDKEY) + + // register available zone types for BlockHandler + ZoneTypes = []int{ + enums.GNS_TYPE_PKEY, + enums.GNS_TYPE_EDKEY, + } ) //---------------------------------------------------------------------- diff --git a/src/gnunet/crypto/hash.go b/src/gnunet/crypto/hash.go index bc715d4..049a8ef 100644 --- a/src/gnunet/crypto/hash.go +++ b/src/gnunet/crypto/hash.go @@ -19,6 +19,7 @@ package crypto import ( + "bytes" "crypto/sha512" "gnunet/util" @@ -29,11 +30,20 @@ type HashCode struct { Bits []byte `size:"64"` } -// NewHashCode creates a new, uninitalized hash value -func NewHashCode() *HashCode { - return &HashCode{ +// Equals tests if two hash results are equal. +func (hc *HashCode) Equals(n *HashCode) bool { + return bytes.Equal(hc.Bits, n.Bits) +} + +// NewHashCode creates a new (initalized) hash value +func NewHashCode(buf []byte) *HashCode { + hc := &HashCode{ Bits: make([]byte, 64), } + if buf != nil { + util.CopyAlignedBlock(hc.Bits, buf) + } + return hc } // Hash returns the SHA-512 hash value of a given blob diff --git a/src/gnunet/go.mod b/src/gnunet/go.mod index 8527379..7383eaf 100644 --- a/src/gnunet/go.mod +++ b/src/gnunet/go.mod @@ -1,21 +1,25 @@ module gnunet -go 1.17 +go 1.18 require ( - github.com/bfix/gospel v1.2.10 - github.com/go-redis/redis/v8 v8.5.0 - github.com/go-sql-driver/mysql v1.5.0 + github.com/bfix/gospel v1.2.11 + 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/mattn/go-sqlite3 v1.14.6 - github.com/miekg/dns v1.1.26 - golang.org/x/crypto v0.0.0-20220128200615-198e4374d7ed + github.com/mattn/go-sqlite3 v1.14.13 + github.com/miekg/dns v1.1.49 + golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898 ) require ( - github.com/cespare/xxhash/v2 v2.1.1 // indirect + github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect - go.opentelemetry.io/otel v0.16.0 // indirect - golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect - golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect + golang.org/x/mod v0.4.2 // indirect + golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2 // indirect + golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect + golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2 // indirect + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect ) + +replace github.com/bfix/gospel v1.2.11 => /vault/prj/libs/Go/Gospel diff --git a/src/gnunet/go.sum b/src/gnunet/go.sum index 4fa501c..ea3c328 100644 --- a/src/gnunet/go.sum +++ b/src/gnunet/go.sum @@ -1,123 +1,67 @@ -github.com/bfix/gospel v1.2.10 h1:a8l/sET2y+FVKIO5M1l5hdTlqLxstvkhp+b6FpAkxOU= -github.com/bfix/gospel v1.2.10/go.mod h1:cdu63bA9ZdfeDoqZ+vnWOcbY9Puwdzmf5DMxMGMznRI= -github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/bfix/gospel v1.2.11 h1:z/c6MFNq/lz4mO8+PK60a3NvH+lbTKAlLCShuFFZUvg= +github.com/bfix/gospel v1.2.11/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= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/go-redis/redis/v8 v8.5.0 h1:L3r1Q3I5WOUdXZGCP6g44EruKh0u3n6co5Hl5xWkdGA= -github.com/go-redis/redis/v8 v8.5.0/go.mod h1:YmEcgBDttjnkbMzDAhDtQxY9yVA7jMN6PCR5HeMvqFE= -github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= -github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= +github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= +github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= +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/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 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= -github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg= -github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= -github.com/miekg/dns v1.1.26 h1:gPxPSwALAeHJSjarOs00QjVdV9QoBvc1D2ujQUr5BzU= -github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= -github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= -github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.15.0 h1:1V1NfVQR87RtWAgp1lv9JZJ5Jap+XFGKPi00andXGi4= -github.com/onsi/ginkgo v1.15.0/go.mod h1:hF8qUzuuC8DJGygJH3726JnCZX4MYbRB8yFfISqnKUg= -github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.10.5 h1:7n6FEkpFmfCoo2t+YYqXH0evK+a9ICQz0xcAy9dYcaQ= -github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opentelemetry.io/otel v0.16.0 h1:uIWEbdeb4vpKPGITLsRVUS44L5oDbDUCZxn8lkxhmgw= -go.opentelemetry.io/otel v0.16.0/go.mod h1:e4GKElweB8W2gWUqbghw0B8t5MCTccc9212eNHnOHwA= +github.com/mattn/go-sqlite3 v1.14.13 h1:1tj15ngiFfcZzii7yd82foL+ks+ouQcj8j/TPq3fk1I= +github.com/mattn/go-sqlite3 v1.14.13/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/miekg/dns v1.1.49 h1:qe0mQU3Z/XpFeE+AEBo2rqaS1IPBJ3anmqZ4XiZJVG8= +github.com/miekg/dns v1.1.49/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20220128200615-198e4374d7ed h1:YoWVYYAfvQ4ddHv3OKmIvX7NCAhFGTj62VP2l2kfBbA= -golang.org/x/crypto v0.0.0-20220128200615-198e4374d7ed/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898 h1:SLP7Q4Di66FONjDJbCYrCRrh97focO6sLogHO7/g8F0= +golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2 h1:NWy5+hlRbC7HK+PmcXVUmW1IMyFce7to56IUvhUFm7Y= +golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2 h1:BonxutuHCTL0rBDnZlKjpGIQFTjyUVTexFOdWkB6Fg0= +golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= diff --git a/src/gnunet/message/factory.go b/src/gnunet/message/factory.go index 8cbf9ce..6681eae 100644 --- a/src/gnunet/message/factory.go +++ b/src/gnunet/message/factory.go @@ -56,12 +56,16 @@ func NewEmptyMessage(msgType uint16) (Message, error) { //------------------------------------------------------------------ // DHT //------------------------------------------------------------------ + case DHT_CLIENT_PUT: + return NewDHTClientPutMsg(nil, 0, nil), nil case DHT_CLIENT_GET: return NewDHTClientGetMsg(nil), nil case DHT_CLIENT_GET_STOP: return NewDHTClientGetStopMsg(nil), nil case DHT_CLIENT_RESULT: return NewDHTClientResultMsg(nil), nil + case DHT_CLIENT_GET_RESULTS_KNOWN: + return NewDHTClientGetResultsKnownMsg(nil), nil //------------------------------------------------------------------ // GNS diff --git a/src/gnunet/message/message.go b/src/gnunet/message/message.go index 1b826d7..60e36c9 100644 --- a/src/gnunet/message/message.go +++ b/src/gnunet/message/message.go @@ -29,11 +29,19 @@ var ( ErrMsgHeaderTooSmall = errors.New("Message header too small") ) +//---------------------------------------------------------------------- + // Message is an interface for all GNUnet-specific messages. type Message interface { + // Header of message Header() *Header + + // String returns a human-readable message + String() string } +//---------------------------------------------------------------------- + // Header encapsulates the common part of all GNUnet messages (at the // beginning of the data). type Header struct { diff --git a/src/gnunet/message/msg_dht.go b/src/gnunet/message/msg_dht.go index 9b73557..bb8fc1a 100644 --- a/src/gnunet/message/msg_dht.go +++ b/src/gnunet/message/msg_dht.go @@ -27,6 +27,54 @@ import ( "gnunet/util" ) +//---------------------------------------------------------------------- +// DHT_CLIENT_PUT +//---------------------------------------------------------------------- + +// DHTClientPutMsg is the message for putting values into the DHT +type DHTClientPutMsg struct { + MsgSize uint16 `order:"big"` // total size of message + MsgType uint16 `order:"big"` // DHT_CLIENT_PUT (142) + Type uint32 `order:"big"` // The type of the data (BLOCK_TYPE_???) + Options uint32 `order:"big"` // Message options (DHT_RO_???) + ReplLevel uint32 `order:"big"` // Replication level for this message + Expire util.AbsoluteTime // Expiration time + Key *crypto.HashCode // The key to be used + Data []byte `size:"*"` // Block data +} + +// NewDHTClientPutMsg creates a new default DHTClientPutMsg object. +func NewDHTClientPutMsg(key *crypto.HashCode, btype int, data []byte) *DHTClientPutMsg { + if key == nil { + key = new(crypto.HashCode) + } + var size uint16 = 88 + if data != nil { + size += uint16(len(data)) + } + return &DHTClientPutMsg{ + MsgSize: size, + MsgType: DHT_CLIENT_PUT, + Type: uint32(btype), + Options: uint32(enums.DHT_RO_NONE), + ReplLevel: 1, + Expire: util.AbsoluteTimeNever(), + Key: key, + Data: data, + } +} + +// String returns a human-readable representation of the message. +func (m *DHTClientPutMsg) String() string { + return fmt.Sprintf("DHTClientPutMsg{Type=%d,Expire=%s,Options=%d,Repl=%d,Key=%s}", + m.Type, m.Expire, m.Options, m.ReplLevel, hex.EncodeToString(m.Key.Bits)) +} + +// Header returns the message header in a separate instance. +func (m *DHTClientPutMsg) Header() *Header { + return &Header{m.MsgSize, m.MsgType} +} + //---------------------------------------------------------------------- // DHT_CLIENT_GET //---------------------------------------------------------------------- @@ -102,7 +150,7 @@ type DHTClientResultMsg struct { // NewDHTClientResultMsg creates a new default DHTClientResultMsg object. func NewDHTClientResultMsg(key *crypto.HashCode) *DHTClientResultMsg { if key == nil { - key = crypto.NewHashCode() + key = crypto.NewHashCode(nil) } return &DHTClientResultMsg{ MsgSize: 64, // empty message size (no data) @@ -163,3 +211,48 @@ func (m *DHTClientGetStopMsg) String() string { func (m *DHTClientGetStopMsg) Header() *Header { return &Header{m.MsgSize, m.MsgType} } + +//---------------------------------------------------------------------- +// DHT_CLIENT_GET_RESULTS_KNOWN +//---------------------------------------------------------------------- + +// DHTClientGetResultsKnownMsg is the message for putting values into the DHT +type DHTClientGetResultsKnownMsg struct { + MsgSize uint16 `order:"big"` // total size of message + MsgType uint16 `order:"big"` // DHT_CLIENT_GET_RESULTS_KNOWN (156) + Reserved uint32 `order:"big"` // Reserved for further use + Key *crypto.HashCode // The key to search for + ID uint64 `order:"big"` // Unique ID identifying this request + Known []*crypto.HashCode `size:"*"` // list of known results +} + +// NewDHTClientPutMsg creates a new default DHTClientPutMsg object. +func NewDHTClientGetResultsKnownMsg(key *crypto.HashCode) *DHTClientGetResultsKnownMsg { + if key == nil { + key = new(crypto.HashCode) + } + return &DHTClientGetResultsKnownMsg{ + MsgSize: 80, + MsgType: DHT_CLIENT_GET_RESULTS_KNOWN, + Key: key, + ID: 0, + Known: make([]*crypto.HashCode, 0), + } +} + +// AddKnown adds a known result to the list +func (m *DHTClientGetResultsKnownMsg) AddKnown(hc *crypto.HashCode) { + m.Known = append(m.Known, hc) + m.MsgSize += 64 +} + +// String returns a human-readable representation of the message. +func (m *DHTClientGetResultsKnownMsg) String() string { + return fmt.Sprintf("DHTClientGetResultsKnownMsg{Id:%d,Key=%s,Num=%d}", + m.ID, hex.EncodeToString(m.Key.Bits), len(m.Known)) +} + +// Header returns the message header in a separate instance. +func (m *DHTClientGetResultsKnownMsg) Header() *Header { + return &Header{m.MsgSize, m.MsgType} +} diff --git a/src/gnunet/message/msg_gns.go b/src/gnunet/message/msg_gns.go index 35630d6..9b85e40 100644 --- a/src/gnunet/message/msg_gns.go +++ b/src/gnunet/message/msg_gns.go @@ -25,15 +25,9 @@ import ( "gnunet/enums" "gnunet/util" - "github.com/bfix/gospel/data" "github.com/bfix/gospel/logger" ) -// Error messages -var ( - ErrBlockNotDecrypted = fmt.Errorf("GNS block not decrypted") -) - //---------------------------------------------------------------------- // GNS_LOOKUP //---------------------------------------------------------------------- @@ -147,94 +141,6 @@ func (rs *RecordSet) Expires() util.AbsoluteTime { return expires } -// SignedBlockData represents the signed and encrypted list of resource -// records stored in a GNSRecordSet -type SignedBlockData struct { - Purpose *crypto.SignaturePurpose `` // Size and purpose of signature (8 bytes) - Expire util.AbsoluteTime `` // Expiration time of the block. - EncData []byte `size:"*"` // encrypted GNSRecordSet - - // transient data (not serialized) - data []byte // decrypted GNSRecord set -} - -// Block is the result of GNS lookups for a given label in a zone. -// An encrypted and signed container for GNS resource records that represents -// the "atomic" data structure associated with a GNS label in a given zone. -type Block struct { - DerivedKeySig *crypto.ZoneSignature // Derived key used for signing - Block *SignedBlockData - - // transient data (not serialized) - checked bool // block integrity checked - verified bool // block signature verified (internal) - decrypted bool // block data decrypted (internal) -} - -// String returns the human-readable representation of a GNSBlock -func (b *Block) String() string { - return fmt.Sprintf("GNSBlock{Verified=%v,Decrypted=%v,data=[%d]}", - b.verified, b.decrypted, len(b.Block.EncData)) -} - -// Records returns the list of resource records in a block. -func (b *Block) Records() ([]*ResourceRecord, error) { - // check if block is decrypted - if !b.decrypted { - return nil, ErrBlockNotDecrypted - } - // parse block data into record set - rs := NewRecordSet() - if err := data.Unmarshal(rs, b.Block.data); err != nil { - return nil, err - } - return rs.Records, nil -} - -// Verify the integrity of the block data from a signature. -func (b *Block) Verify(zkey *crypto.ZoneKey, label string) (err error) { - // Integrity check performed - b.checked = true - - // verify derived key - dkey := b.DerivedKeySig.ZoneKey - dkey2, _ := zkey.Derive(label, "gns") - if !dkey.Equal(dkey2) { - return fmt.Errorf("invalid signature key for GNS Block") - } - // verify signature - var buf []byte - if buf, err = data.Marshal(b.Block); err != nil { - return - } - b.verified, err = b.DerivedKeySig.Verify(buf) - return -} - -// Decrypt block data with a key derived from zone key and label. -func (b *Block) Decrypt(zkey *crypto.ZoneKey, label string) (err error) { - // decrypt payload - b.Block.data, err = zkey.Decrypt(b.Block.EncData, label, b.Block.Expire) - b.decrypted = true - return -} - -// NewBlock instantiates an empty GNS block -func NewBlock() *Block { - return &Block{ - DerivedKeySig: nil, - Block: &SignedBlockData{ - Purpose: new(crypto.SignaturePurpose), - Expire: *new(util.AbsoluteTime), - EncData: nil, - data: nil, - }, - checked: false, - verified: false, - decrypted: false, - } -} - // ResourceRecord is the GNUnet-specific representation of resource // records (not to be confused with DNS resource records). type ResourceRecord struct { diff --git a/src/gnunet/message/msg_hello.go b/src/gnunet/message/msg_hello.go new file mode 100644 index 0000000..18fe9d5 --- /dev/null +++ b/src/gnunet/message/msg_hello.go @@ -0,0 +1,103 @@ +// 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 . +// +// SPDX-License-Identifier: AGPL3.0-or-later + +package message + +import ( + "fmt" + "gnunet/util" +) + +//---------------------------------------------------------------------- +// HELLO +// +// A HELLO message is used to exchange information about transports with +// other peers. This struct is always followed by the actual network +// addresses which have the format: +// +// 1) transport-name (0-terminated) +// 2) address-length (uint16_t, network byte order) +// 3) address expiration +// 4) address (address-length bytes) +//---------------------------------------------------------------------- + +// HelloAddress represents a (generic) peer address with expiration date +type HelloAddress struct { + Transport string // Name of transport + AddrSize uint16 `order:"big"` // Size of address entry + ExpireOn util.AbsoluteTime // Expiry date + Address []byte `size:"AddrSize"` // Address specification +} + +// NewHelloAddress create a new HELLO address from the given address +func NewHelloAddress(a *util.Address) *HelloAddress { + addr := &HelloAddress{ + Transport: a.Netw, + AddrSize: uint16(len(a.Address)), + ExpireOn: a.Expires, + Address: make([]byte, len(a.Address)), + } + copy(addr.Address, a.Address) + return addr +} + +// String returns a human-readable representation of the message. +func (a *HelloAddress) String() string { + return fmt.Sprintf("Address{%s,expire=%s}", + util.AddressString(a.Transport, a.Address), a.ExpireOn) +} + +// HelloMsg is a message send by peers to announce their presence +type HelloMsg struct { + MsgSize uint16 `order:"big"` // total size of message + MsgType uint16 `order:"big"` // HELLO (17) + FriendOnly uint32 `order:"big"` // =1: do not gossip this HELLO + PeerID *util.PeerID // EdDSA public key (long-term) + Addresses []*HelloAddress `size:"*"` // List of end-point addressess +} + +// NewHelloMsg creates a new HELLO msg for a given peer. +func NewHelloMsg(peerid *util.PeerID) *HelloMsg { + if peerid == nil { + peerid = util.NewPeerID(nil) + } + return &HelloMsg{ + MsgSize: 40, + MsgType: HELLO, + FriendOnly: 0, + PeerID: peerid, + Addresses: make([]*HelloAddress, 0), + } +} + +// String returns a human-readable representation of the message. +func (m *HelloMsg) String() string { + return fmt.Sprintf("HelloMsg{peer=%s,friendsonly=%d,addr=%v}", + m.PeerID, m.FriendOnly, m.Addresses) +} + +// AddAddress adds a new address to the HELLO message. +func (m *HelloMsg) AddAddress(a *HelloAddress) { + m.Addresses = append(m.Addresses, a) + m.MsgSize += uint16(len(a.Transport)) + a.AddrSize + 11 +} + +// Header returns the message header in a separate instance. +func (m *HelloMsg) Header() *Header { + return &Header{m.MsgSize, m.MsgType} +} diff --git a/src/gnunet/message/msg_namecache.go b/src/gnunet/message/msg_namecache.go index c3a0ac7..5acb807 100644 --- a/src/gnunet/message/msg_namecache.go +++ b/src/gnunet/message/msg_namecache.go @@ -21,8 +21,8 @@ package message import ( "encoding/hex" "fmt" - "gnunet/crypto" + "gnunet/service/dht/blocks" "gnunet/util" ) @@ -41,7 +41,7 @@ type NamecacheLookupMsg struct { // NewNamecacheLookupMsg creates a new default message. func NewNamecacheLookupMsg(query *crypto.HashCode) *NamecacheLookupMsg { if query == nil { - query = crypto.NewHashCode() + query = crypto.NewHashCode(nil) } return &NamecacheLookupMsg{ MsgSize: 72, @@ -114,7 +114,7 @@ type NamecacheCacheMsg struct { } // NewNamecacheCacheMsg creates a new default message. -func NewNamecacheCacheMsg(block *Block) *NamecacheCacheMsg { +func NewNamecacheCacheMsg(block *blocks.GNSBlock) *NamecacheCacheMsg { msg := &NamecacheCacheMsg{ MsgSize: 108, MsgType: NAMECACHE_BLOCK_CACHE, @@ -125,10 +125,10 @@ func NewNamecacheCacheMsg(block *Block) *NamecacheCacheMsg { } if block != nil { msg.DerivedKeySig = block.DerivedKeySig - msg.Expire = block.Block.Expire - size := len(block.Block.EncData) + msg.Expire = block.Body.Expire + size := len(block.Body.Data) msg.EncData = make([]byte, size) - copy(msg.EncData, block.Block.EncData) + copy(msg.EncData, block.Body.Data) msg.MsgSize += uint16(size) } return msg diff --git a/src/gnunet/message/msg_transport.go b/src/gnunet/message/msg_transport.go index 6e64c4d..d0e927b 100644 --- a/src/gnunet/message/msg_transport.go +++ b/src/gnunet/message/msg_transport.go @@ -1,5 +1,5 @@ // This file is part of gnunet-go, a GNUnet-implementation in Golang. -// Copyright (C) 2019, 2020 Bernd Fix >Y< +// 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 @@ -224,86 +224,6 @@ func (m *TransportPongMsg) Verify(pub *ed25519.PublicKey) (bool, error) { return pub.EdVerify(data, sig) } -//---------------------------------------------------------------------- -// HELLO -// -// A HELLO message is used to exchange information about -// transports with other peers. This struct is always -// followed by the actual network addresses which have -// the format: -// -// 1) transport-name (0-terminated) -// 2) address-length (uint16_t, network byte order) -// 3) address expiration -// 4) address (address-length bytes) -//---------------------------------------------------------------------- - -// HelloAddress represents a (generic) peer address with expiration date -type HelloAddress struct { - Transport string // Name of transport - AddrSize uint16 `order:"big"` // Size of address entry - ExpireOn util.AbsoluteTime // Expiry date - Address []byte `size:"AddrSize"` // Address specification -} - -// NewHelloAddress create a new HELLO address from the given address -func NewHelloAddress(a *util.Address) *HelloAddress { - addr := &HelloAddress{ - Transport: a.Transport, - AddrSize: uint16(len(a.Address)), - ExpireOn: util.AbsoluteTimeNow().Add(12 * time.Hour), - Address: make([]byte, len(a.Address)), - } - copy(addr.Address, a.Address) - return addr -} - -// String returns a human-readable representation of the message. -func (a *HelloAddress) String() string { - return fmt.Sprintf("Address{%s,expire=%s}", - util.AddressString(a.Transport, a.Address), a.ExpireOn) -} - -// HelloMsg is a message send by peers to announce their presence -type HelloMsg struct { - MsgSize uint16 `order:"big"` // total size of message - MsgType uint16 `order:"big"` // HELLO (17) - FriendOnly uint32 `order:"big"` // =1: do not gossip this HELLO - PeerID *util.PeerID // EdDSA public key (long-term) - Addresses []*HelloAddress `size:"*"` // List of end-point addressess -} - -// NewHelloMsg creates a new HELLO msg for a given peer. -func NewHelloMsg(peerid *util.PeerID) *HelloMsg { - if peerid == nil { - peerid = util.NewPeerID(nil) - } - return &HelloMsg{ - MsgSize: 40, - MsgType: HELLO, - FriendOnly: 0, - PeerID: peerid, - Addresses: make([]*HelloAddress, 0), - } -} - -// String returns a human-readable representation of the message. -func (m *HelloMsg) String() string { - return fmt.Sprintf("HelloMsg{peer=%s,friendsonly=%d,addr=%v}", - m.PeerID, m.FriendOnly, m.Addresses) -} - -// AddAddress adds a new address to the HELLO message. -func (m *HelloMsg) AddAddress(a *HelloAddress) { - m.Addresses = append(m.Addresses, a) - m.MsgSize += uint16(len(a.Transport)) + a.AddrSize + 11 -} - -// Header returns the message header in a separate instance. -func (m *HelloMsg) Header() *Header { - return &Header{m.MsgSize, m.MsgType} -} - //---------------------------------------------------------------------- // TRANSPORT_SESSION_ACK //---------------------------------------------------------------------- diff --git a/src/gnunet/modules.go b/src/gnunet/modules.go index f4fdb1b..e47699f 100644 --- a/src/gnunet/modules.go +++ b/src/gnunet/modules.go @@ -52,29 +52,33 @@ func (inst Instances) Register() { rpc.Register(inst.Revocation) } -// Local reference to instance list +// Local reference to instance list. The list is initialized +// by core. var ( Modules Instances ) +/* TODO: implement // Initialize instance list and link module functions as required. -func init() { +// This function is called by core on start-up. +func Init(ctx context.Context) { // Namecache (no calls to other modules) - Modules.Namecache = new(namecache.NamecacheModule) + Modules.Namecache = namecache.NewModule(ctx, c) // DHT (no calls to other modules) - Modules.DHT = new(dht.Module) + Modules.DHT = dht.NewModule(ctx, c) // Revocation (no calls to other modules) - Modules.Revocation = revocation.NewModule() + Modules.Revocation = revocation.NewModule(ctx, c) // GNS (calls Namecache, DHT and Identity) - Modules.GNS = &gns.Module{ - LookupLocal: Modules.Namecache.Get, - StoreLocal: Modules.Namecache.Put, - LookupRemote: Modules.DHT.Get, - RevocationQuery: Modules.Revocation.Query, - RevocationRevoke: Modules.Revocation.Revoke, - } + gns := gns.NewModule(ctx, c) + Modules.GNS = gns + gns.LookupLocal = Modules.Namecache.Get + gns.StoreLocal = Modules.Namecache.Put + gns.LookupRemote = Modules.DHT.Get + gns.RevocationQuery = Modules.Revocation.Query + gns.RevocationRevoke = Modules.Revocation.Revoke } +*/ diff --git a/src/gnunet/service/client.go b/src/gnunet/service/client.go index a196c09..19dc4c4 100644 --- a/src/gnunet/service/client.go +++ b/src/gnunet/service/client.go @@ -19,39 +19,39 @@ package service import ( + "context" "gnunet/message" - "gnunet/transport" "github.com/bfix/gospel/logger" ) // Client type: Use to perform client-side interactions with GNUnet services. type Client struct { - ch *transport.MsgChannel // channel for message exchange + ch *Connection // channel for message exchange } -// NewClient creates a new client instance for the given channel endpoint. -func NewClient(endp string) (*Client, error) { - // create a new channel to endpoint. - ch, err := transport.NewChannel(endp) +// NewClient connects to a socket with given path +func NewClient(ctx context.Context, path string) (*Client, error) { + // create a connection + ch, err := NewConnection(ctx, path) if err != nil { return nil, err } // wrap into a message channel for the client. return &Client{ - ch: transport.NewMsgChannel(ch), + ch: ch, }, nil } -// SendRequest sends a give message to the service. -func (c *Client) SendRequest(ctx *SessionContext, req message.Message) error { - return c.ch.Send(req, ctx.Signaller()) +// SendRequest sends a message to the service. +func (c *Client) SendRequest(ctx context.Context, req message.Message) error { + return c.ch.Send(ctx, req) } // ReceiveResponse waits for a response from the service; it can be interrupted // by sending "false" to the cmd channel. -func (c *Client) ReceiveResponse(ctx *SessionContext) (message.Message, error) { - return c.ch.Receive(ctx.Signaller()) +func (c *Client) ReceiveResponse(ctx context.Context) (message.Message, error) { + return c.ch.Receive(ctx) } // Close a client; no further message exchange is possible. @@ -62,15 +62,15 @@ func (c *Client) Close() error { // RequestResponse is a helper method for a one request - one response // secenarios of client/serice interactions. func RequestResponse( - ctx *SessionContext, + ctx context.Context, caller string, callee string, - endp 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(endp) + cl, err := NewClient(ctx, path) if err != nil { return nil, err } diff --git a/src/gnunet/service/connection.go b/src/gnunet/service/connection.go new file mode 100644 index 0000000..1c690c5 --- /dev/null +++ b/src/gnunet/service/connection.go @@ -0,0 +1,280 @@ +// 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 . +// +// SPDX-License-Identifier: AGPL3.0-or-later + +package service + +import ( + "context" + "errors" + "fmt" + "gnunet/message" + "net" + "os" + "strconv" + + "github.com/bfix/gospel/data" + "github.com/bfix/gospel/logger" +) + +// Error codes +var ( + ErrConnectionNotOpened = errors.New("channel not opened") + ErrConnectionInterrupted = errors.New("channel interrupted") +) + +//====================================================================== + +// Connection is a channel for GNUnet message exchange (send/receive) +// based on Unix domain sockets. It is used locally by services and +// clients in the standard GNUnet environment. +type Connection struct { + path string // file name of Unix socket + conn net.Conn // associated connection + buf []byte // read/write buffer +} + +// NewConnection creates a new connection to a socket with given path. +// This is used by clients to connect to a service. +func NewConnection(ctx context.Context, path string) (s *Connection, err error) { + var d net.Dialer + s = new(Connection) + s.path = path + s.buf = make([]byte, 65536) + s.conn, err = d.DialContext(ctx, "unix", path) + return +} + +// Close a socket connection +func (s *Connection) Close() error { + if s.conn != nil { + rc := s.conn.Close() + s.conn = nil + return rc + } + return ErrConnectionNotOpened +} + +// Send a GNUnet message over a socket. +func (s *Connection) Send(ctx context.Context, msg message.Message) error { + // convert message to binary data + data, err := data.Marshal(msg) + if err != nil { + return err + } + // check message header size and packet size + mh, err := message.GetMsgHeader(data) + if err != nil { + return err + } + if len(data) != int(mh.MsgSize) { + return errors.New("send: message size mismatch") + } + + // send packet + n, err := s.write(ctx, data) + if err != nil { + return err + } + if n != len(data) { + return errors.New("incomplete send") + } + return nil +} + +// Receive GNUnet messages from socket. +func (s *Connection) Receive(ctx context.Context) (message.Message, error) { + // get bytes from socket + get := func(pos, count int) error { + n, err := s.read(ctx, s.buf[pos:pos+count]) + if err != nil { + return err + } + if n != count { + return errors.New("not enough bytes on network") + } + return nil + } + // read header first + if err := get(0, 4); err != nil { + return nil, err + } + mh, err := message.GetMsgHeader(s.buf[:4]) + if err != nil { + return nil, err + } + // get rest of message + if err := get(4, int(mh.MsgSize)-4); err != nil { + return nil, err + } + msg, err := message.NewEmptyMessage(mh.MsgType) + if err != nil { + return nil, err + } + if msg == nil { + return nil, fmt.Errorf("message{%d} is nil", mh.MsgType) + } + if err = data.Unmarshal(msg, s.buf[:mh.MsgSize]); err != nil { + return nil, err + } + return msg, nil +} + +//---------------------------------------------------------------------- +// internal methods +//---------------------------------------------------------------------- + +// result of read/write operations on sockets. +type result struct { + n int // number of bytes read/written + err error // error (or nil) +} + +// Read bytes from a socket into buffer: Returns the number of read +// bytes and an error code. Only works on open channels ;) +func (s *Connection) read(ctx context.Context, buf []byte) (int, error) { + // check if the channel is open + if s.conn == nil { + return 0, ErrConnectionNotOpened + } + // perform read operation + ch := make(chan *result) + go func() { + n, err := s.conn.Read(buf) + ch <- &result{n, err} + }() + for { + select { + // terminate on request + case <-ctx.Done(): + return 0, ErrConnectionInterrupted + + // handle result of read operation + case res := <-ch: + return res.n, res.err + } + } +} + +// Write buffer to socket and returns the number of bytes written and an +// optional error code. +func (s *Connection) write(ctx context.Context, buf []byte) (int, error) { + // check if we have an open socket to write to. + if s.conn == nil { + return 0, ErrConnectionNotOpened + } + // perform write operation + ch := make(chan *result) + go func() { + n, err := s.conn.Write(buf) + ch <- &result{n, err} + }() + for { + select { + // handle terminate command + case <-ctx.Done(): + return 0, ErrConnectionInterrupted + + // handle result of write operation + case res := <-ch: + return res.n, res.err + } + } +} + +//====================================================================== + +// ConnectionManager to handle client connections on a socket. +type ConnectionManager struct { + listener net.Listener // reference to listener object + running bool // server running? +} + +// NewConnectionManager creates a new socket connection manager. Incoming +// connections from clients are dispatched to a handler channel. +func NewConnectionManager( + ctx context.Context, // execution context + path string, // socket file name + params map[string]string, // connection parameters + hdlr chan *Connection, // handler for incoming connections +) (cs *ConnectionManager, err error) { + + // instantiate channel server + cs = &ConnectionManager{ + listener: nil, + running: false, + } + // create listener + var lc net.ListenConfig + if cs.listener, err = lc.Listen(ctx, "unix", path); err != nil { + 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 { + logger.Printf( + logger.ERROR, + "MsgChannelServer: Invalid permissions '%s'\n", + value) + } + } + } + } + // run go routine to handle channel requests from clients + cs.running = true + go func() { + for cs.running { + conn, err := cs.listener.Accept() + if err != nil { + break + } + // handle connection + c := &Connection{ + conn: conn, + path: path, + buf: make([]byte, 65536), + } + hdlr <- c + } + if cs.listener != nil { + cs.listener.Close() + } + }() + return cs, nil +} + +// Close a network channel server (= stop the server) +func (s *ConnectionManager) Close() error { + s.running = false + if s.listener != nil { + err := s.listener.Close() + s.listener = nil + return err + } + return nil +} diff --git a/src/gnunet/service/context.go b/src/gnunet/service/context.go deleted file mode 100644 index 4ae786d..0000000 --- a/src/gnunet/service/context.go +++ /dev/null @@ -1,87 +0,0 @@ -// This file is part of gnunet-go, a GNUnet-implementation in Golang. -// Copyright (C) 2019, 2020 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 . -// -// SPDX-License-Identifier: AGPL3.0-or-later - -package service - -import ( - "sync" - - "gnunet/util" - - "github.com/bfix/gospel/concurrent" -) - -// SessionContext is used to set a context for each client connection handled -// by a service; the session is handled by the 'ServeClient' method of a -// service implementation. -type SessionContext struct { - ID int // session identifier - wg *sync.WaitGroup // wait group for the session - sig *concurrent.Signaller // signaller for the session - pending int // number of pending go-routines - active bool // is the context active (un-cancelled)? - onCancel *sync.Mutex // only run one Cancel() at a time -} - -// NewSessionContext instantiates a new session context. -func NewSessionContext() *SessionContext { - return &SessionContext{ - ID: util.NextID(), - wg: new(sync.WaitGroup), - sig: concurrent.NewSignaller(), - pending: 0, - active: true, - onCancel: new(sync.Mutex), - } -} - -// Cancel all go-routines associated with this context. -func (ctx *SessionContext) Cancel() { - ctx.onCancel.Lock() - if ctx.active { - // we are going out-of-business - ctx.active = false - // send signal to terminate... - ctx.sig.Send(true) - // wait for session go-routines to finish - ctx.wg.Wait() - } - ctx.onCancel.Unlock() -} - -// Add a go-routine to the wait group. -func (ctx *SessionContext) Add() { - ctx.wg.Add(1) - ctx.pending++ -} - -// Remove a go-routine from the wait group. -func (ctx *SessionContext) Remove() { - ctx.wg.Done() - ctx.pending-- -} - -// Waiting returns the number of waiting go-routines. -func (ctx *SessionContext) Waiting() int { - return ctx.pending -} - -// Signaller returns the working instance for the context. -func (ctx *SessionContext) Signaller() *concurrent.Signaller { - return ctx.sig -} diff --git a/src/gnunet/service/dht/blocks/generic.go b/src/gnunet/service/dht/blocks/generic.go new file mode 100644 index 0000000..6301e3b --- /dev/null +++ b/src/gnunet/service/dht/blocks/generic.go @@ -0,0 +1,196 @@ +// 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 . +// +// SPDX-License-Identifier: AGPL3.0-or-later + +package blocks + +import ( + "bytes" + "encoding/gob" + "encoding/hex" + "fmt" + "gnunet/crypto" + "gnunet/util" + + "github.com/bfix/gospel/data" +) + +//---------------------------------------------------------------------- +// Query/Block interfaces for generic DHT handling +//---------------------------------------------------------------------- + +// DHT Query interface +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 + + // Set stores the value of a named query parameter + Set(key string, value any) + + // Verify the integrity of a retrieved block (optional). Override in + // custom query types to implement block-specific integrity checks + // (see GNSQuery for example). + Verify(blk Block) error + + // Decrypt block content (optional). Override in custom query types to + // implement block-specific encryption (see GNSQuery for example). + Decrypt(blk Block) error + + // String returns the human-readable representation of a query + String() string +} + +// DHT Block interface +type Block interface { + + // Data returns the DHT block data (unstructured without type and + // expiration information. + Data() []byte + + // Return the block type + Type() uint16 + + // Expire returns the block expiration + Expire() util.AbsoluteTime + + // Verify the integrity of a block (optional). Override in custom query + // 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 + + // String returns the human-readable representation of a block + String() string +} + +// Unwrap (raw) block to a specific block type +func Unwrap(blk Block, obj interface{}) error { + return data.Unmarshal(obj, blk.Data()) +} + +//---------------------------------------------------------------------- +// Generic interface implementations without persistent attributes +//---------------------------------------------------------------------- + +// GenericQuery is the binary representation of a DHT key +type GenericQuery struct { + // Key for repository queries (local/remote) + key *crypto.HashCode + + // query parameters (binary value representation) + params map[string][]byte +} + +// Key interface method implementation +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 +} + +// 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() + } +} + +// Verify interface method implementation +func (q *GenericQuery) Verify(b Block) error { + // no verification, no errors ;) + return nil +} + +// Decrypt interface method implementation +func (q *GenericQuery) Decrypt(b Block) error { + // no decryption, no errors ;) + return nil +} + +// 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)) +} + +// NewGenericQuery creates a simple Query from hash code. +func NewGenericQuery(buf []byte) *GenericQuery { + return &GenericQuery{ + key: crypto.NewHashCode(buf), + params: make(map[string][]byte), + } +} + +//---------------------------------------------------------------------- + +// GenericBlock is the block in simple binary representation +type GenericBlock struct { + block []byte // block data + btype uint16 // block type + expire util.AbsoluteTime // expiration date +} + +// Data interface method implementation +func (b *GenericBlock) Data() []byte { + return b.block +} + +// Type returns the block type +func (b *GenericBlock) Type() uint16 { + return b.btype +} + +// Expire returns the block expiration +func (b *GenericBlock) Expire() util.AbsoluteTime { + return b.expire +} + +// String returns the human-readable representation of a block +func (b *GenericBlock) String() string { + return fmt.Sprintf("GenericBlock{type=%d,expires=%s,data=[%d]}", + b.btype, b.expire.String(), len(b.block)) +} + +// Verify interface method implementation +func (b *GenericBlock) Verify() error { + // no verification, no errors ;) + return 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 + } +} diff --git a/src/gnunet/service/dht/blocks/generic_test.go b/src/gnunet/service/dht/blocks/generic_test.go new file mode 100644 index 0000000..51ee5a1 --- /dev/null +++ b/src/gnunet/service/dht/blocks/generic_test.go @@ -0,0 +1,67 @@ +// 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 . +// +// 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 new file mode 100644 index 0000000..2085677 --- /dev/null +++ b/src/gnunet/service/dht/blocks/gns.go @@ -0,0 +1,172 @@ +// 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 . +// +// SPDX-License-Identifier: AGPL3.0-or-later + +package blocks + +import ( + "errors" + "fmt" + "gnunet/crypto" + "gnunet/util" + + "github.com/bfix/gospel/data" +) + +// Error messages +var ( + ErrBlockNotDecrypted = fmt.Errorf("GNS block not decrypted") +) + +//---------------------------------------------------------------------- +// Query key for GNS lookups +//---------------------------------------------------------------------- + +// GNSQuery specifies the context for a basic GNS name lookup of an (atomic) +// label in a given zone identified by its public key. +type GNSQuery struct { + GenericQuery + Zone *crypto.ZoneKey // Public zone key + Label string // Atomic label + derived *crypto.ZoneKey // Derived zone key from (pkey,label) +} + +// Verify the integrity of the block data from a signature. +func (q *GNSQuery) Verify(b Block) (err error) { + switch blk := b.(type) { + case *GNSBlock: + // Integrity check performed + blk.checked = true + + // verify derived key + dkey := blk.DerivedKeySig.ZoneKey + dkey2, _ := q.Zone.Derive(q.Label, "gns") + if !dkey.Equal(dkey2) { + return fmt.Errorf("invalid signature key for GNS Block") + } + // verify signature + var buf []byte + if buf, err = data.Marshal(blk.Body); err != nil { + return + } + blk.verified, err = blk.DerivedKeySig.Verify(buf) + + default: + err = errors.New("can't verify block type") + } + return +} + +// Decrypt block data with a key derived from zone key and label. +func (q *GNSQuery) Decrypt(b Block) (err error) { + switch blk := b.(type) { + case *GNSBlock: + // decrypt GNS payload + blk.data, err = q.Zone.Decrypt(blk.Body.Data, q.Label, blk.Body.Expire) + blk.decrypted = true + return + + default: + err = errors.New("can't decrypt block type") + } + return +} + +// NewGNSQuery assembles a new Query object for the given zone and label. +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") + gq := crypto.Hash(pd.Bytes()).Bits + return &GNSQuery{ + GenericQuery: *NewGenericQuery(gq), + Zone: zkey, + Label: label, + derived: pd, + } +} + +//---------------------------------------------------------------------- +// GNS blocks +//---------------------------------------------------------------------- + +// SignedGNSBlockData represents the signed content of a GNS block +type SignedGNSBlockData struct { + Purpose *crypto.SignaturePurpose `` // Size and purpose of signature (8 bytes) + Expire util.AbsoluteTime `` // Expiration time of the block. + Data []byte `size:"*"` // Block data content +} + +// GNSBlock is the result of GNS lookups for a given label in a zone. +// An encrypted and signed container for GNS resource records that represents +// the "atomic" data structure associated with a GNS label in a given zone. +type GNSBlock struct { + GenericBlock + + // persistent + DerivedKeySig *crypto.ZoneSignature // Derived key used for signing + Body *SignedGNSBlockData + + // transient data (not serialized) + checked bool // block integrity checked + verified bool // block signature verified (internal) + decrypted bool // block decrypted (internal) + data []byte // decrypted data +} + +// Data block interface implementation +func (b *GNSBlock) Data() []byte { + buf, _ := data.Marshal(b) + return buf +} + +// String returns the human-readable representation of a GNSBlock +func (b *GNSBlock) String() string { + return fmt.Sprintf("GNSBlock{Verified=%v,Decrypted=%v,data=[%d]}", + b.verified, b.decrypted, len(b.Body.Data)) +} + +// NewBlock instantiates an empty GNS block +func NewBlock() *GNSBlock { + return &GNSBlock{ + DerivedKeySig: nil, + Body: &SignedGNSBlockData{ + Purpose: new(crypto.SignaturePurpose), + Expire: *new(util.AbsoluteTime), + Data: nil, + }, + checked: false, + verified: false, + decrypted: false, + data: nil, + } +} + +// Verify the integrity of the block data from a signature. +// 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) { + // verify signature + var buf []byte + if buf, err = data.Marshal(b.Body); err != nil { + return + } + _, err = b.DerivedKeySig.Verify(buf) + return +} diff --git a/src/gnunet/service/dht/blocks/hello.go b/src/gnunet/service/dht/blocks/hello.go new file mode 100644 index 0000000..77fc2ae --- /dev/null +++ b/src/gnunet/service/dht/blocks/hello.go @@ -0,0 +1,226 @@ +// 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 . +// +// SPDX-License-Identifier: AGPL3.0-or-later + +package blocks + +import ( + "bytes" + "encoding/binary" + "fmt" + "gnunet/util" + "net/url" + "strconv" + "strings" + + "github.com/bfix/gospel/crypto/ed25519" + "github.com/bfix/gospel/data" +) + +//---------------------------------------------------------------------- +// HELLO URLs are used for bootstrapping a node and for adding nodes +// outside of GNUnet message exchange (e.g. command-line tools) +//---------------------------------------------------------------------- + +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). +type HelloBlock struct { + PeerID *util.PeerID `` // peer identifier + Signature *ed25519.EdSignature `` // signature + Expire util.AbsoluteTime `` // Expiration date + AddrBin []byte `size:"*"` // raw address data + + // transient attributes + addrs []*util.Address // cooked address data +} + +// SetAddresses adds a bulk of addresses for this HELLO block. +func (h *HelloBlock) SetAddresses(a []*util.Address) { + h.addrs = util.Clone(a) + h.finalize() +} + +// Addresses returns the list of addresses +func (h *HelloBlock) Addresses() []*util.Address { + return util.Clone(h.addrs) +} + +// ParseHelloURL parses a HELLO URL of the following form: +// gnunet://hello///? +// The addresses are encoded. +func ParseHelloURL(u string) (h *HelloBlock, err error) { + // check and trim prefix + if !strings.HasPrefix(u, helloPrefix) { + err = fmt.Errorf("invalid HELLO-URL prefix: '%s'", u) + return + } + u = u[len(helloPrefix):] + + // split remainder into parts + p := strings.Split(u, "/") + if len(p) != 3 { + err = fmt.Errorf("invalid HELLO-URL: '%s'", u) + return + } + + // assemble HELLO data + h = new(HelloBlock) + + // (1) parse peer public key (peer ID) + var buf []byte + if buf, err = util.DecodeStringToBinary(p[0], 32); err != nil { + return + } + h.PeerID = util.NewPeerID(buf) + + // (2) parse signature + if buf, err = util.DecodeStringToBinary(p[1], 64); err != nil { + return + } + if h.Signature, err = ed25519.NewEdSignatureFromBytes(buf); err != nil { + return + } + + // (3) split last element into parts + q := strings.SplitN(p[2], "?", 2) + + // (4) parse expiration date + var exp uint64 + if exp, err = strconv.ParseUint(q[0], 10, 64); err != nil { + return + } + h.Expire = util.NewAbsoluteTimeEpoch(exp) + + // (5) process addresses. + h.addrs = make([]*util.Address, 0) + var ua string + for _, a := range strings.Split(q[1], "&") { + // unescape URL query + if ua, err = url.QueryUnescape(a); err != nil { + return + } + // parse address and append it to list + var addr *util.Address + if addr, err = util.ParseAddress(ua); err != nil { + return + } + h.addrs = append(h.addrs, addr) + } + + // (6) generate raw address data so block is complete + h.finalize() + return +} + +// ParseHelloFromBytes converts a byte array into a HelloBlock instance. +func ParseHelloFromBytes(buf []byte) (h *HelloBlock, err error) { + h = new(HelloBlock) + if err = data.Unmarshal(h, buf); err == nil { + err = h.finalize() + } + return +} + +// finalize block data (generate dependent fields) +func (h *HelloBlock) finalize() (err error) { + if h.addrs == nil { + err = data.Unmarshal(h.addrs, h.AddrBin) + } else if h.AddrBin == nil { + wrt := new(bytes.Buffer) + for _, a := range h.addrs { + wrt.WriteString(a.String()) + wrt.WriteByte(0) + } + h.AddrBin = wrt.Bytes() + } + 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 msg +} +*/ + +// 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.Bytes()), + h.Expire.Epoch(), + ) + for i, a := range h.addrs { + if i > 0 { + u += "&" + } + u += url.QueryEscape(a.String()) + } + return u +} + +// Equals returns true if two HELLOs are the same. The expiration +// timestamp is ignored in the comparision. +func (h *HelloBlock) Equals(g *HelloBlock) bool { + if !h.PeerID.Equals(g.PeerID) || + !util.Equals(h.Signature.Bytes(), g.Signature.Bytes()) || + len(h.addrs) != len(g.addrs) { + return false + } + for i, a := range h.addrs { + if !a.Equals(g.addrs[i]) { + return false + } + } + return true +} + +// Verify the integrity of the HELLO data +func (h *HelloBlock) Verify() (bool, error) { + // assemble signed data and public key + sd := h.signedData() + pub := ed25519.NewPublicKeyFromBytes(h.PeerID.Key) + return pub.EdVerify(sd, h.Signature) +} + +// Sign the HELLO data with private key +func (h *HelloBlock) Sign(prv *ed25519.PrivateKey) (err error) { + // assemble signed data + sd := h.signedData() + h.Signature, err = prv.EdSign(sd) + return +} + +// signedData assembles a data block for sign and verify operations. +func (h *HelloBlock) signedData() []byte { + buf := new(bytes.Buffer) + buf.Write(h.PeerID.Key) + binary.Write(buf, binary.BigEndian, h.Expire) + for _, a := range h.addrs { + buf.Write(a.Address) + } + return buf.Bytes() +} diff --git a/src/gnunet/service/dht/blocks/hello_test.go b/src/gnunet/service/dht/blocks/hello_test.go new file mode 100644 index 0000000..089259a --- /dev/null +++ b/src/gnunet/service/dht/blocks/hello_test.go @@ -0,0 +1,44 @@ +// 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 . +// +// SPDX-License-Identifier: AGPL3.0-or-later + +package blocks + +import "testing" + +const ( + helloURL = "gnunet://hello" + + "/7KTBJ90340HF1Q2GB0A57E2XJER4FDHX8HP5GHEB9125VPWPD27G" + + + "/BNMDFN6HJCPWSPNBSEC06MC1K8QN1Z2DHRQSRXDTFR7FTBD4JHN" + + "BJ2RJAAEZ31FWG1Q3PMN3PXGZQ3Q7NTNEKQZFA7TE2Y46FM8E20R" + + "/1653499308" + + "?r5n%2Bip%2Budp%3A1.2.3.4%3A6789" + + "&gnunet%2Btcp%3A12.3.4.5" +) + +func TestHelloURL(t *testing.T) { + + hd, err := ParseHelloURL(helloURL) + if err != nil { + t.Fatal(err) + } + u := hd.URL() + if u != helloURL { + t.Fatal("urls don't match") + } +} diff --git a/src/gnunet/service/dht/blocks/types.go b/src/gnunet/service/dht/blocks/types.go new file mode 100644 index 0000000..04edb6e --- /dev/null +++ b/src/gnunet/service/dht/blocks/types.go @@ -0,0 +1,26 @@ +// 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 . +// +// 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 new file mode 100644 index 0000000..dcfd935 --- /dev/null +++ b/src/gnunet/service/dht/bloomfilter.go @@ -0,0 +1,123 @@ +// 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 . +// +// 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 new file mode 100644 index 0000000..3cb8080 --- /dev/null +++ b/src/gnunet/service/dht/dhtstore_test.go @@ -0,0 +1,89 @@ +// 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 . +// +// SPDX-License-Identifier: AGPL3.0-or-later + +package dht + +import ( + "encoding/hex" + "gnunet/crypto" + "gnunet/service" + blocks "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) { + + // create file store + fs, err := service.NewFileCache("/var/lib/gnunet/dht/cache", "100") + 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 := 20 // 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) + t.Logf("> %d: %s -- %s", i, hex.EncodeToString(k), hex.EncodeToString(buf)) + + // 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 i, key := range keys { + // get block + val, err := fs.Get(key) + if err != nil { + t.Fatal(err) + } + buf := val.Data() + t.Logf("< %d: %s -- %s", i, hex.EncodeToString(key.Key().Bits), hex.EncodeToString(buf)) + + // 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/module.go b/src/gnunet/service/dht/module.go index 1588a06..d369f3f 100644 --- a/src/gnunet/service/dht/module.go +++ b/src/gnunet/service/dht/module.go @@ -19,9 +19,13 @@ package dht import ( + "context" + "gnunet/config" + "gnunet/core" "gnunet/message" "gnunet/service" - "gnunet/service/gns" + "gnunet/service/dht/blocks" + "net/http" ) //====================================================================== @@ -32,16 +36,99 @@ import ( // Put and get blocks into/from a DHT. //---------------------------------------------------------------------- -// Module handles the permanent storage of blocks under the query key. +// Module handles the permanent storage of blocks under a query key. 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 + + rtable *RoutingTable // routing table +} + +// NewModule returns a new module instance. It initializes the storage +// mechanism for persistence. +func NewModule(ctx context.Context, c *core.Core) (m *Module) { + // create permanent storage handler + store, err := service.NewDHTStore(config.Cfg.DHT.Storage) + if err != nil { + return nil + } + // create cache handler + cache, err := service.NewDHTStore(config.Cfg.DHT.Cache) + if err != nil { + return nil + } + // create routing table + rt := NewRoutingTable(NewPeerAddress(c.PeerID())) + + // return module instance + m = &Module{ + ModuleImpl: *service.NewModuleImpl(), + store: store, + cache: cache, + core: c, + rtable: rt, + } + // register as listener for core events + listener := m.Run(ctx, m.event, m.Filter()) + c.Register("dht", listener) + + return } -// Get a GNS block from the DHT -func (nc *Module) Get(ctx *service.SessionContext, query *gns.Query) (*message.Block, error) { +//---------------------------------------------------------------------- + +// Get a block from the DHT +func (nc *Module) Get(ctx context.Context, query blocks.Query) (block blocks.Block, err error) { + + // check if we have the requested block in cache or permanent storage. + block, err = nc.cache.Get(query) + if err == nil { + // yes: we are done + return + } + block, err = nc.store.Get(query) + if err == nil { + // yes: we are done + return + } + // retrieve the block from the DHT + return nil, nil } -// Put a GNS block into the DHT -func (nc *Module) Put(ctx *service.SessionContext, block *message.Block) error { +// Put a block into the DHT +func (nc *Module) Put(ctx context.Context, key blocks.Query, block blocks.Block) error { return nil } + +//---------------------------------------------------------------------- + +// Filter returns the event filter for the module +func (m *Module) Filter() *core.EventFilter { + f := core.NewEventFilter() + f.AddEvent(core.EV_CONNECT) + f.AddEvent(core.EV_DISCONNECT) + 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) + return f +} + +// Event handler +func (nc *Module) event(ctx context.Context, ev *core.Event) { + +} + +//---------------------------------------------------------------------- + +// RPC returns the route and handler function for a JSON-RPC request +func (m *Module) RPC() (string, func(http.ResponseWriter, *http.Request)) { + return "/gns/", func(wrt http.ResponseWriter, req *http.Request) { + wrt.Write([]byte(`{"msg": "This is DHT" }`)) + } +} diff --git a/src/gnunet/service/dht/routingtable.go b/src/gnunet/service/dht/routingtable.go new file mode 100644 index 0000000..895a1b2 --- /dev/null +++ b/src/gnunet/service/dht/routingtable.go @@ -0,0 +1,305 @@ +// 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 . +// +// SPDX-License-Identifier: AGPL3.0-or-later + +package dht + +import ( + "bytes" + "crypto/sha512" + "encoding/hex" + "gnunet/util" + "math/rand" + "sync" + + "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) +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 +) + +//====================================================================== +//====================================================================== + +// 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 +} + +// 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)) + return r +} + +// String returns a human-readble representation of an address. +func (addr *PeerAddress) String() string { + return hex.EncodeToString(addr.addr[:]) +} + +// Equals returns true if two peer addresses are the same. +func (addr *PeerAddress) Equals(p *PeerAddress) bool { + return bytes.Equal(addr.addr[:], p.addr[:]) +} + +// 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] + } + r := math.NewIntFromBytes(d.addr[:]) + return r, numBuckets - r.BitLen() +} + +//====================================================================== +// Routing table implementation +//====================================================================== + +// RoutingTable holds the (local) routing table for a node. +// The index of of an address is the number of bits in the +// 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]bool // keep list of peers + rwlock sync.RWMutex // lock for write operations + l2nse float64 // log2 of estimated network size +} + +// NewRoutingTable creates a new routing table for the reference address. +func NewRoutingTable(ref *PeerAddress) *RoutingTable { + rt := new(RoutingTable) + rt.ref = ref + rt.list = make(map[*PeerAddress]bool) + rt.buckets = make([]*Bucket, numBuckets) + for i := range rt.buckets { + rt.buckets[i] = NewBucket(numK) + } + return rt +} + +// Add new peer address to routing table. +// Returns true if the entry was added, false otherwise. +func (rt *RoutingTable) Add(p *PeerAddress, connected bool) bool { + // ensure one write and no readers + rt.rwlock.Lock() + defer rt.rwlock.Unlock() + + // compute distance (bucket index) and insert address. + _, idx := p.Distance(rt.ref) + if rt.buckets[idx].Add(p, connected) { + rt.list[p] = true + return true + } + // Full bucket: we did not add the address to the routing table. + 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.rwlock.Lock() + defer rt.rwlock.Unlock() + + // compute distance (bucket index) and remove entry from bucket + _, idx := p.Distance(rt.ref) + if rt.buckets[idx].Remove(p) { + delete(rt.list, p) + return true + } + return false +} + +//---------------------------------------------------------------------- +// routing functions +//---------------------------------------------------------------------- + +// SelectClosestPeer for a given peer address and bloomfilter. +func (rt *RoutingTable) SelectClosestPeer(p *PeerAddress, bf *PeerBloomFilter) (n *PeerAddress) { + // no writer allowed + rt.rwlock.RLock() + defer rt.rwlock.RUnlock() + + // find closest address + var dist *math.Int + for _, b := range rt.buckets { + if k, d := b.SelectClosestPeer(p, bf); n == nil || (d != nil && d.Cmp(dist) < 0) { + dist = d + n = k + } + } + return +} + +// SelectRandomPeer returns a random address from table (that is not +// included in the bloomfilter) +func (rt *RoutingTable) SelectRandomPeer(bf *PeerBloomFilter) *PeerAddress { + // no writer allowed + rt.rwlock.RLock() + defer rt.rwlock.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 { + return k + } + idx-- + } + } + return nil +} + +// 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 { + if float64(hops) < rt.l2nse { + return rt.SelectRandomPeer(bf) + } + return rt.SelectClosestPeer(p, bf) +} + +// 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) + return n.Equals(p) +} + +// ComputeOutDegree computes the number of neighbors that a message should be forwarded to. +// 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 { + hf := float64(hop) + if hf > 4*rt.l2nse { + return 0 + } + if hf > 2*rt.l2nse { + return 1 + } + if repl == 0 { + repl = 1 + } else if repl > 16 { + repl = 16 + } + rm1 := float64(repl - 1) + return 1 + int(rm1/(rt.l2nse+rm1*hf)) +} + +//====================================================================== +// Routing table buckets +//====================================================================== + +// PeerEntry in a k-Bucket: use routing specific attributes +// for book-keeping +type PeerEntry struct { + addr *PeerAddress // peer address + connected bool // is peer connected? +} + +// Bucket holds peer entries with approx. same distance from node +type Bucket struct { + list []*PeerEntry + rwlock sync.RWMutex +} + +// NewBucket creates a new entry list of given size +func NewBucket(n int) *Bucket { + return &Bucket{ + list: make([]*PeerEntry, 0, n), + } +} + +// Add peer address to the bucket if there is free space. +// Returns true if entry is added, false otherwise. +func (b *Bucket) Add(p *PeerAddress, connected bool) bool { + // only one writer and no readers + b.rwlock.Lock() + defer b.rwlock.Unlock() + + // check for free space in bucket + if len(b.list) < numK { + // append entry at the end + pe := &PeerEntry{ + addr: p, + connected: connected, + } + b.list = append(b.list, pe) + return true + } + return false +} + +// Remove peer address from the bucket. +// 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() + + for i, pe := range b.list { + if pe.addr.Equals(p) { + // found entry: remove it + b.list = append(b.list[:i], b.list[i+1:]...) + return true + } + } + return false +} + +// 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) { + // no writer allowed + b.rwlock.RLock() + defer b.rwlock.RUnlock() + + for _, pe := range b.list { + // skip addresses in bloomfilter + if bf.Contains(pe.addr) { + continue + } + // check for shorter distance + if d, _ := p.Distance(pe.addr); n == nil || d.Cmp(dist) < 0 { + // remember best match + dist = d + n = pe.addr + } + } + return +} diff --git a/src/gnunet/service/dht/routingtable_test.go b/src/gnunet/service/dht/routingtable_test.go new file mode 100644 index 0000000..2579356 --- /dev/null +++ b/src/gnunet/service/dht/routingtable_test.go @@ -0,0 +1,140 @@ +// 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 . +// +// SPDX-License-Identifier: AGPL3.0-or-later + +package dht + +import ( + "gnunet/config" + "gnunet/core" + "gnunet/util" + "math/rand" + "testing" +) + +const ( + NUMP = 1000 // Total number of peers + EPOCHS = 10000 // number of epochs to run +) + +type Entry struct { + addr *PeerAddress // address of peer + ttl int64 // time to live (in epochs) + born int64 // epoch of birth + last int64 // last action + drop int64 // drop (in epochs) + revive int64 // revive dropped (in epochs) + online bool // peer connected? +} + +// test data +var ( + cfg = &config.NodeConfig{ + PrivateSeed: "YGoe6XFH3XdvFRl+agx9gIzPTvxA229WFdkazEMdcOs=", + Endpoints: []string{ + "r5n+ip+udp://127.0.0.1:6666", + }, + } +) + +// TestRT connects and disconnects random peers to test the base +// functionality of the routing table algorithms. +func TestRT(t *testing.T) { + // start deterministic randomizer + rand.Seed(19031962) + + // helper functions + genRemotePeer := func() *PeerAddress { + d := make([]byte, 32) + if _, err := rand.Read(d); err != nil { + panic(err) + } + return NewPeerAddress(util.NewPeerID(d)) + } + + // create routing table and start command handler + local, err := core.NewLocalPeer(cfg) + if err != nil { + t.Fatal(err) + } + rt := NewRoutingTable(NewPeerAddress(local.GetID())) + + // create a task list + tasks := make([]*Entry, NUMP) + for i := range tasks { + tasks[i] = new(Entry) + tasks[i].addr = genRemotePeer() + tasks[i].born = rand.Int63n(EPOCHS) + tasks[i].ttl = 1000 + rand.Int63n(7000) + tasks[i].drop = 2000 + rand.Int63n(3000) + tasks[i].revive = rand.Int63n(2000) + tasks[i].online = false + } + + // actions: + connected := func(task *Entry, e int64, msg string) { + rt.Add(task.addr, true) + task.online = true + task.last = e + t.Logf("[%6d] %s %s\n", e, task.addr, msg) + } + disconnected := func(task *Entry, e int64, msg string) { + rt.Remove(task.addr) + task.online = false + task.last = e + t.Logf("[%6d] %s %s\n", e, task.addr, msg) + } + + // run epochs + var e int64 + for e = 0; e < EPOCHS; e++ { + for _, task := range tasks { + // birth + if task.born == e { + connected(task, e, "connected") + continue + } + // death + if task.born+task.ttl == e { + disconnected(task, e, "disconnected") + continue + } + if task.online { + // drop out + if task.last+task.drop == e { + disconnected(task, e, "dropped out") + continue + } + } else { + // drop in + if task.last+task.drop == e { + connected(task, e, "dropped in") + continue + } + } + } + } + + // execute some routing functions on remaining table + k := genRemotePeer() + bf := NewPeerBloomFilter() + n := rt.SelectClosestPeer(k, bf) + t.Logf("Closest: %s -> %s\n", k, n) + + n = rt.SelectRandomPeer(bf) + t.Logf("Random: %s\n", n) +} diff --git a/src/gnunet/service/dht/service.go b/src/gnunet/service/dht/service.go new file mode 100644 index 0000000..2a189bb --- /dev/null +++ b/src/gnunet/service/dht/service.go @@ -0,0 +1,134 @@ +// This file is part of gnunet-go, a GNUnet-implementation in Golang. +// Copyright (C) 2019, 2020 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 . +// +// SPDX-License-Identifier: AGPL3.0-or-later + +package dht + +import ( + "context" + "fmt" + "io" + + "gnunet/core" + "gnunet/message" + "gnunet/service" + + "github.com/bfix/gospel/logger" +) + +// Error codes +var ( + ErrInvalidID = fmt.Errorf("invalid/unassociated ID") + ErrBlockExpired = fmt.Errorf("block expired") + ErrInvalidResponseType = fmt.Errorf("invald response type") +) + +//---------------------------------------------------------------------- +// "GNUnet R5N DHT" service implementation +//---------------------------------------------------------------------- + +// Service implements a DHT service +type Service struct { + Module +} + +// NewService creates a new DHT service instance +func NewService(ctx context.Context, c *core.Core) service.Service { + return &Service{ + Module: *NewModule(ctx, c), + } +} + +// ServeClient processes a client channel. +func (s *Service) ServeClient(ctx context.Context, id int, mc *service.Connection) { + reqID := 0 + var cancel context.CancelFunc + ctx, cancel = context.WithCancel(ctx) + +loop: + for { + // receive next message from client + reqID++ + logger.Printf(logger.DBG, "[dht:%d:%d] Waiting for client request...\n", id, reqID) + msg, err := mc.Receive(ctx) + if err != nil { + if err == io.EOF { + logger.Printf(logger.INFO, "[dht:%d:%d] Client channel closed.\n", id, reqID) + } else if err == service.ErrConnectionInterrupted { + logger.Printf(logger.INFO, "[dht:%d:%d] Service operation interrupted.\n", id, reqID) + } else { + logger.Printf(logger.ERROR, "[dht:%d:%d] Message-receive failed: %s\n", id, reqID, err.Error()) + } + break 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) + } + // close client connection + mc.Close() + + // cancel all tasks running for this session/connection + 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 service.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 index b05c59e..c93fca1 100644 --- a/src/gnunet/service/gns/block_handler.go +++ b/src/gnunet/service/gns/block_handler.go @@ -76,7 +76,7 @@ type BlockHandler interface { // resource records in the same block. 'cm' maps the resource type // to an integer count (how many records of a type are present in the // GNS block). - Coexist(cm util.CounterMap) bool + Coexist(cm util.Counter[int]) bool // Records returns a list of RR of the given types associated with // the custom handler @@ -103,7 +103,7 @@ type BlockHandler interface { // BlockHandlerList is a list of block handlers instantiated. type BlockHandlerList struct { list map[int]BlockHandler // list of handler instances - counts util.CounterMap // count number of RRs by type + counts util.Counter[int] // count number of RRs by type } // NewBlockHandlerList instantiates an a list of active block handlers @@ -112,7 +112,7 @@ func NewBlockHandlerList(records []*message.ResourceRecord, labels []string) (*B // initialize block handler list hl := &BlockHandlerList{ list: make(map[int]BlockHandler), - counts: make(util.CounterMap), + counts: make(util.Counter[int]), } // first pass: build list of shadow records in this block @@ -260,7 +260,7 @@ func (h *ZoneKeyHandler) AddRecord(rec *message.ResourceRecord, labels []string) // Coexist return a flag indicating how a resource record of a given type // is to be treated (see BlockHandler interface) -func (h *ZoneKeyHandler) Coexist(cm util.CounterMap) bool { +func (h *ZoneKeyHandler) Coexist(cm util.Counter[int]) bool { // only one type (GNS_TYPE_PKEY) is present return len(cm) == 1 && cm.Num(enums.GNS_TYPE_PKEY) == 1 } @@ -335,7 +335,7 @@ func (h *Gns2DnsHandler) AddRecord(rec *message.ResourceRecord, labels []string) // Coexist return a flag indicating how a resource record of a given type // is to be treated (see BlockHandler interface) -func (h *Gns2DnsHandler) Coexist(cm util.CounterMap) bool { +func (h *Gns2DnsHandler) Coexist(cm util.Counter[int]) bool { // only one type (GNS_TYPE_GNS2DNS) is present return len(cm) == 1 && cm.Num(enums.GNS_TYPE_GNS2DNS) > 0 } @@ -405,7 +405,7 @@ func (h *BoxHandler) AddRecord(rec *message.ResourceRecord, labels []string) err // Coexist return a flag indicating how a resource record of a given type // is to be treated (see BlockHandler interface) -func (h *BoxHandler) Coexist(cm util.CounterMap) bool { +func (h *BoxHandler) Coexist(cm util.Counter[int]) bool { // anything goes... return true } @@ -469,7 +469,7 @@ func (h *LehoHandler) AddRecord(rec *message.ResourceRecord, labels []string) er // Coexist return a flag indicating how a resource record of a given type // is to be treated (see BlockHandler interface) -func (h *LehoHandler) Coexist(cm util.CounterMap) bool { +func (h *LehoHandler) Coexist(cm util.Counter[int]) bool { // requires exactly one LEHO and any number of other records. return cm.Num(enums.GNS_TYPE_LEHO) == 1 } @@ -527,7 +527,7 @@ func (h *CnameHandler) AddRecord(rec *message.ResourceRecord, labels []string) e // Coexist return a flag indicating how a resource record of a given type // is to be treated (see BlockHandler interface) -func (h *CnameHandler) Coexist(cm util.CounterMap) bool { +func (h *CnameHandler) Coexist(cm util.Counter[int]) bool { // only a single CNAME allowed return len(cm) == 1 && cm.Num(enums.GNS_TYPE_DNS_CNAME) == 1 } @@ -581,7 +581,7 @@ func (h *VpnHandler) AddRecord(rec *message.ResourceRecord, labels []string) err // Coexist return a flag indicating how a resource record of a given type // is to be treated (see BlockHandler interface) -func (h *VpnHandler) Coexist(cm util.CounterMap) bool { +func (h *VpnHandler) Coexist(cm util.Counter[int]) bool { // anything goes return true } diff --git a/src/gnunet/service/gns/dns.go b/src/gnunet/service/gns/dns.go index 3bcc35e..8818ca6 100644 --- a/src/gnunet/service/gns/dns.go +++ b/src/gnunet/service/gns/dns.go @@ -19,6 +19,7 @@ package gns import ( + "context" "fmt" "net" "strings" @@ -27,7 +28,6 @@ import ( "gnunet/crypto" "gnunet/enums" "gnunet/message" - "gnunet/service" "gnunet/util" "github.com/bfix/gospel/logger" @@ -205,7 +205,7 @@ func QueryDNS(id int, name string, server net.IP, kind RRTypeList) *message.Reco // parallel; the first result delivered by any of the servers is returned // as the result list of matching resource records. func (gns *Module) ResolveDNS( - ctx *service.SessionContext, + ctx context.Context, name string, servers []string, kind RRTypeList, diff --git a/src/gnunet/service/gns/module.go b/src/gnunet/service/gns/module.go index 5ff81f2..31ad6c7 100644 --- a/src/gnunet/service/gns/module.go +++ b/src/gnunet/service/gns/module.go @@ -19,18 +19,22 @@ package gns import ( + "context" "fmt" "net/http" "strings" "gnunet/config" + "gnunet/core" "gnunet/crypto" "gnunet/enums" "gnunet/message" "gnunet/service" + "gnunet/service/dht/blocks" "gnunet/service/revocation" "gnunet/util" + "github.com/bfix/gospel/data" "github.com/bfix/gospel/logger" ) @@ -44,34 +48,6 @@ var ( ErrGNSRecursionExceeded = fmt.Errorf("recursion depth exceeded") ) -//---------------------------------------------------------------------- -// Query for simple GNS lookups -//---------------------------------------------------------------------- - -// Query specifies the context for a basic GNS name lookup of an (atomic) -// label in a given zone identified by its public key. -type Query struct { - Zone *crypto.ZoneKey // Public zone key - Label string // Atomic label - Derived *crypto.ZoneKey // Derived key from (pkey,label) - Key *crypto.HashCode // Key for repository queries (local/remote) -} - -// NewQuery assembles a new Query object for the given zone and label. -func NewQuery(zkey *crypto.ZoneKey, label string) *Query { - // 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") - key := crypto.Hash(pd.Bytes()) - return &Query{ - Zone: zkey, - Label: label, - Derived: pd, - Key: key, - } -} - //---------------------------------------------------------------------- // The GNS module (recursively) resolves GNS names: // Resolves DNS-like names (e.g. "minecraft.servers.bob.games"; a name is @@ -112,25 +88,50 @@ func NewQuery(zkey *crypto.ZoneKey, label string) *Query { // Module handles the resolution of GNS names to RRs bundled in a block. type Module struct { + service.ModuleImpl + // Use function references for calls to methods in other modules: - LookupLocal func(ctx *service.SessionContext, query *Query) (*message.Block, error) - StoreLocal func(ctx *service.SessionContext, block *message.Block) error - LookupRemote func(ctx *service.SessionContext, query *Query) (*message.Block, error) - RevocationQuery func(ctx *service.SessionContext, zkey *crypto.ZoneKey) (valid bool, err error) - RevocationRevoke func(ctx *service.SessionContext, rd *revocation.RevData) (success bool, err error) + LookupLocal func(ctx context.Context, query *blocks.GNSQuery) (*blocks.GNSBlock, error) + StoreLocal func(ctx context.Context, query *blocks.GNSQuery, block *blocks.GNSBlock) error + LookupRemote func(ctx context.Context, query blocks.Query) (blocks.Block, error) + RevocationQuery func(ctx context.Context, zkey *crypto.ZoneKey) (valid bool, err error) + RevocationRevoke func(ctx context.Context, rd *revocation.RevData) (success bool, err error) } -// RPC returns the route and handler function for a JSON-RPC request -func (m *Module) RPC() (string, func(http.ResponseWriter, *http.Request)) { - return "/gns/", func(wrt http.ResponseWriter, req *http.Request) { - wrt.Write([]byte(`{"msg": "This is GNS" }`)) +func NewModule(ctx context.Context, c *core.Core) (m *Module) { + m = &Module{ + ModuleImpl: *service.NewModuleImpl(), } + // register as listener for core events + listener := m.Run(ctx, m.event, m.Filter()) + c.Register("gns", listener) + + return +} + +//---------------------------------------------------------------------- + +// Filter returns the event filter for the service +func (m *Module) Filter() *core.EventFilter { + f := core.NewEventFilter() + f.AddMsgType(message.GNS_LOOKUP) + f.AddMsgType(message.GNS_LOOKUP_RESULT) + f.AddMsgType(message.GNS_REVERSE_LOOKUP) + f.AddMsgType(message.GNS_REVERSE_LOOKUP_RESULT) + return f } +// Event handler +func (m *Module) event(ctx context.Context, ev *core.Event) { + +} + +//---------------------------------------------------------------------- + // Resolve a GNS name with multiple labels. If pkey is not nil, the name // is interpreted as "relative to current zone". func (m *Module) Resolve( - ctx *service.SessionContext, + ctx context.Context, path string, zkey *crypto.ZoneKey, kind RRTypeList, @@ -142,7 +143,7 @@ func (m *Module) Resolve( return nil, ErrGNSRecursionExceeded } // get the labels in reverse order - names := util.ReverseStringList(strings.Split(path, ".")) + names := util.Reverse(strings.Split(path, ".")) logger.Printf(logger.DBG, "[gns] Resolver called for %v\n", names) // check for relative path @@ -157,7 +158,7 @@ func (m *Module) Resolve( // ResolveAbsolute resolves a fully qualified GNS absolute name // (with multiple labels). func (m *Module) ResolveAbsolute( - ctx *service.SessionContext, + ctx context.Context, labels []string, kind RRTypeList, mode int, @@ -184,7 +185,7 @@ func (m *Module) ResolveAbsolute( // processing simple (PKEY,Label) lookups in sequence and handle intermediate // GNS record types func (m *Module) ResolveRelative( - ctx *service.SessionContext, + ctx context.Context, labels []string, zkey *crypto.ZoneKey, kind RRTypeList, @@ -200,7 +201,7 @@ func (m *Module) ResolveRelative( logger.Printf(logger.DBG, "[gns] ResolveRelative '%s' in '%s'\n", labels[0], util.EncodeBinaryToString(zkey.Bytes())) // resolve next level - var block *message.Block + var block *blocks.GNSBlock if block, err = m.Lookup(ctx, zkey, labels[0], mode); err != nil { // failed to resolve name return @@ -221,7 +222,7 @@ func (m *Module) ResolveRelative( } // post-process block by inspecting contained resource records for // special GNS types - if records, err = block.Records(); err != nil { + if records, err = m.records(block.Body.Data); err != nil { return } // assemble a list of block handlers for this block: if multiple @@ -238,10 +239,7 @@ func (m *Module) ResolveRelative( // handle special block cases in priority order: //-------------------------------------------------------------- - if hdlr := hdlrs.GetHandler( - enums.GNS_TYPE_PKEY, - enums.GNS_TYPE_EDKEY, - ); hdlr != nil { + if hdlr := hdlrs.GetHandler(crypto.ZoneTypes...); hdlr != nil { // (1) zone key record: inst := hdlr.(*ZoneKeyHandler) // if labels are pending, set new zone and continue resolution; @@ -267,7 +265,7 @@ func (m *Module) ResolveRelative( } // ... otherwise we need to handle delegation to DNS: returns a // list of found resource records in DNS (filter by 'kind') - lbls := strings.Join(util.ReverseStringList(labels[1:]), ".") + lbls := strings.Join(util.Reverse(labels[1:]), ".") if len(lbls) > 0 { lbls += "." } @@ -361,7 +359,7 @@ func (m *Module) ResolveRelative( // a PKEY TLD), it is also resolved with GNS. All other names are resolved // via DNS queries. func (m *Module) ResolveUnknown( - ctx *service.SessionContext, + ctx context.Context, name string, labels []string, zkey *crypto.ZoneKey, @@ -372,7 +370,7 @@ func (m *Module) ResolveUnknown( if strings.HasSuffix(name, ".+") { // resolve server name relative to current zone name = strings.TrimSuffix(name, ".+") - for _, label := range util.ReverseStringList(labels) { + for _, label := range util.Reverse(labels) { name += "." + label } if set, err = m.Resolve(ctx, name, zkey, kind, enums.GNS_LO_DEFAULT, depth+1); err != nil { @@ -397,7 +395,7 @@ func (m *Module) ResolveUnknown( // GetZoneKey returns the zone key (or nil) from an absolute GNS path. func (m *Module) GetZoneKey(path string) *crypto.ZoneKey { - labels := util.ReverseStringList(strings.Split(path, ".")) + labels := util.Reverse(strings.Split(path, ".")) if len(labels[0]) == 52 { if data, err := util.DecodeStringToBinary(labels[0], 32); err == nil { if zkey, err := crypto.NewZoneKey(data); err == nil { @@ -410,13 +408,13 @@ func (m *Module) GetZoneKey(path string) *crypto.ZoneKey { // Lookup name in GNS. func (m *Module) Lookup( - ctx *service.SessionContext, + ctx context.Context, zkey *crypto.ZoneKey, label string, - mode int) (block *message.Block, err error) { + mode int) (block *blocks.GNSBlock, err error) { // create query (lookup key) - query := NewQuery(zkey, label) + query := blocks.NewGNSQuery(zkey, label) // try local lookup first if block, err = m.LookupLocal(ctx, query); err != nil { @@ -427,7 +425,8 @@ func (m *Module) Lookup( if block == nil { if mode == enums.GNS_LO_DEFAULT { // get the block from a remote lookup - if block, err = m.LookupRemote(ctx, query); err != nil || block == nil { + var blk blocks.Block + if blk, err = m.LookupRemote(ctx, query); err != nil || blk == nil { if err != nil { logger.Printf(logger.ERROR, "[gns] remote Lookup failed: %s\n", err.Error()) block = nil @@ -437,8 +436,13 @@ func (m *Module) Lookup( // lookup fails completely -- no result return } + // convert to GNSBlock + if err = blocks.Unwrap(blk, &block); err != nil { + logger.Println(logger.DBG, "[gns] remote Lookup: GNS unwrap failed") + return + } // store RRs from remote locally. - m.StoreLocal(ctx, block) + m.StoreLocal(ctx, query, block) } } return @@ -456,3 +460,22 @@ func (m *Module) newLEHORecord(name string, expires util.AbsoluteTime) *message. rr.Data[len(name)] = 0 return rr } + +// Records returns the list of resource records from binary data. +func (m *Module) records(buf []byte) ([]*message.ResourceRecord, error) { + // parse data into record set + rs := message.NewRecordSet() + if err := data.Unmarshal(rs, buf); err != nil { + return nil, err + } + return rs.Records, nil +} + +//---------------------------------------------------------------------- + +// RPC returns the route and handler function for a JSON-RPC request +func (m *Module) RPC() (string, func(http.ResponseWriter, *http.Request)) { + return "/gns/", func(wrt http.ResponseWriter, req *http.Request) { + wrt.Write([]byte(`{"msg": "This is GNS" }`)) + } +} diff --git a/src/gnunet/service/gns/service.go b/src/gnunet/service/gns/service.go index 6fa1eb2..326d831 100644 --- a/src/gnunet/service/gns/service.go +++ b/src/gnunet/service/gns/service.go @@ -19,6 +19,7 @@ package gns import ( + "context" "encoding/hex" "fmt" "io" @@ -28,8 +29,8 @@ import ( "gnunet/enums" "gnunet/message" "gnunet/service" + "gnunet/service/dht/blocks" "gnunet/service/revocation" - "gnunet/transport" "gnunet/util" "github.com/bfix/gospel/data" @@ -64,115 +65,117 @@ func NewService() service.Service { return inst } -// Start the GNS service -func (s *Service) Start(spec string) error { - return nil -} - -// Stop the GNS service -func (s *Service) Stop() error { - return nil -} - // ServeClient processes a client channel. -func (s *Service) ServeClient(ctx *service.SessionContext, mc *transport.MsgChannel) { +func (s *Service) ServeClient(ctx context.Context, id int, mc *service.Connection) { reqID := 0 -loop: + var cancel context.CancelFunc + ctx, cancel = context.WithCancel(ctx) + for { // receive next message from client reqID++ - logger.Printf(logger.DBG, "[gns:%d:%d] Waiting for client request...\n", ctx.ID, reqID) - msg, err := mc.Receive(ctx.Signaller()) + logger.Printf(logger.DBG, "[gns:%d:%d] Waiting for client request...\n", id, reqID) + msg, err := mc.Receive(ctx) if err != nil { if err == io.EOF { - logger.Printf(logger.INFO, "[gns:%d:%d] Client channel closed.\n", ctx.ID, reqID) - } else if err == transport.ErrChannelInterrupted { - logger.Printf(logger.INFO, "[gns:%d:%d] Service operation interrupted.\n", ctx.ID, reqID) + logger.Printf(logger.INFO, "[gns:%d:%d] Client channel closed.\n", id, reqID) + } else if err == service.ErrConnectionInterrupted { + logger.Printf(logger.INFO, "[gns:%d:%d] Service operation interrupted.\n", id, reqID) } else { - logger.Printf(logger.ERROR, "[gns:%d:%d] Message-receive failed: %s\n", ctx.ID, reqID, err.Error()) + logger.Printf(logger.ERROR, "[gns:%d:%d] Message-receive failed: %s\n", id, reqID, err.Error()) } - break loop + break } - logger.Printf(logger.INFO, "[gns:%d:%d] Received request: %v\n", ctx.ID, reqID, msg) - - // perform lookup - switch m := msg.(type) { - case *message.LookupMsg: - //---------------------------------------------------------- - // GNS_LOOKUP - //---------------------------------------------------------- - - // perform lookup on block (locally and remote) - go func(id int, m *message.LookupMsg) { - logger.Printf(logger.INFO, "[gns:%d:%d] Lookup request received.\n", ctx.ID, id) - resp := message.NewGNSLookupResultMsg(m.ID) - ctx.Add() - defer func() { - // send response - if resp != nil { - if err := mc.Send(resp, ctx.Signaller()); err != nil { - logger.Printf(logger.ERROR, "[gns:%d:%d] Failed to send response: %s\n", ctx.ID, id, err.Error()) - } - } - // go-routine finished - logger.Printf(logger.DBG, "[gns:%d:%d] Lookup request finished.\n", ctx.ID, id) - ctx.Remove() - }() - - label := m.GetName() - kind := NewRRTypeList(int(m.Type)) - recset, err := s.Resolve(ctx, label, m.Zone, kind, int(m.Options), 0) - if err != nil { - logger.Printf(logger.ERROR, "[gns:%d:%d] Failed to lookup block: %s\n", ctx.ID, id, err.Error()) - if err == transport.ErrChannelInterrupted { - resp = nil + 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) + } + // close client connection + mc.Close() + + // cancel all tasks running for this session/connection + logger.Printf(logger.INFO, "[gns:%d] Start closing session...\n", id) + cancel() +} + +// Handle a single incoming message +func (s *Service) HandleMessage(ctx context.Context, msg message.Message, back service.Responder) bool { + // assemble log label + label := "" + if v := ctx.Value("label"); v != nil { + label = v.(string) + } + // perform lookup + switch m := msg.(type) { + case *message.LookupMsg: + //---------------------------------------------------------- + // GNS_LOOKUP + //---------------------------------------------------------- + + // perform lookup on block (locally and remote) + go func(m *message.LookupMsg) { + logger.Printf(logger.INFO, "[gns%s] Lookup request received.\n", label) + resp := message.NewGNSLookupResultMsg(m.ID) + defer func() { + // send response + if resp != nil { + if err := back.Send(ctx, resp); err != nil { + logger.Printf(logger.ERROR, "[gns%s] Failed to send response: %s\n", label, err.Error()) } + } + // go-routine finished + logger.Printf(logger.DBG, "[gns%s] Lookup request finished.\n", label) + }() + + label := m.GetName() + kind := NewRRTypeList(int(m.Type)) + recset, err := s.Resolve(ctx, label, m.Zone, kind, int(m.Options), 0) + if err != nil { + logger.Printf(logger.ERROR, "[gns%s] Failed to lookup block: %s\n", label, err.Error()) + if err == service.ErrConnectionInterrupted { + resp = nil + } + return + } + // handle records + if recset != nil { + logger.Printf(logger.DBG, "[gns%s] Received record set with %d entries\n", label, recset.Count) + + // get records from block + if recset.Count == 0 { + logger.Printf(logger.WARN, "[gns%s] No records in block\n", label) return } - // handle records - if recset != nil { - logger.Printf(logger.DBG, "[gns:%d:%d] Received record set with %d entries\n", ctx.ID, id, recset.Count) - - // get records from block - if recset.Count == 0 { - logger.Printf(logger.WARN, "[gns:%d:%d] No records in block\n", ctx.ID, id) - return - } - // process records - for i, rec := range recset.Records { - logger.Printf(logger.DBG, "[gns:%d:%d] Record #%d: %v\n", ctx.ID, id, i, rec) - - // is this the record type we are looking for? - if rec.Type == m.Type || int(m.Type) == enums.GNS_TYPE_ANY { - // add it to the response message - resp.AddRecord(rec) - } + // process records + for i, rec := range recset.Records { + logger.Printf(logger.DBG, "[gns%s] Record #%d: %v\n", label, i, rec) + + // is this the record type we are looking for? + if rec.Type == m.Type || int(m.Type) == enums.GNS_TYPE_ANY { + // add it to the response message + resp.AddRecord(rec) } } - }(reqID, m) - - default: - //---------------------------------------------------------- - // UNKNOWN message type received - //---------------------------------------------------------- - logger.Printf(logger.ERROR, "[gns:%d:%d] Unhandled message of type (%d)\n", ctx.ID, reqID, msg.Header().MsgType) - break loop - } - } - // close client connection - mc.Close() + } + }(m) - // cancel all tasks running for this session/connection - logger.Printf(logger.INFO, "[gns:%d] Start closing session... [%d]\n", ctx.ID, ctx.Waiting()) - ctx.Cancel() + default: + //---------------------------------------------------------- + // UNKNOWN message type received + //---------------------------------------------------------- + logger.Printf(logger.ERROR, "[gns%s] Unhandled message of type (%d)\n", label, msg.Header().MsgType) + return false + } + return true } //====================================================================== -// Revocationrelated methods +// Revocation-related methods //====================================================================== // QueryKeyRevocation checks if a key has been revoked -func (s *Service) QueryKeyRevocation(ctx *service.SessionContext, zkey *crypto.ZoneKey) (valid bool, err error) { +func (s *Service) QueryKeyRevocation(ctx context.Context, zkey *crypto.ZoneKey) (valid bool, err error) { logger.Printf(logger.DBG, "[gns] QueryKeyRev(%s)...\n", util.EncodeBinaryToString(zkey.Bytes())) // assemble request @@ -180,7 +183,7 @@ func (s *Service) QueryKeyRevocation(ctx *service.SessionContext, zkey *crypto.Z // get response from Revocation service var resp message.Message - if resp, err = service.RequestResponse(ctx, "gns", "Revocation", config.Cfg.Revocation.Endpoint, req); err != nil { + if resp, err = service.RequestResponse(ctx, "gns", "Revocation", config.Cfg.Revocation.Service.Socket, req); err != nil { return } @@ -195,7 +198,7 @@ func (s *Service) QueryKeyRevocation(ctx *service.SessionContext, zkey *crypto.Z } // RevokeKey revokes a key with given revocation data -func (s *Service) RevokeKey(ctx *service.SessionContext, rd *revocation.RevData) (success bool, err error) { +func (s *Service) RevokeKey(ctx context.Context, rd *revocation.RevData) (success bool, err error) { logger.Printf(logger.DBG, "[gns] RevokeKey(%s)...\n", rd.ZoneKeySig.ID()) // assemble request @@ -206,7 +209,7 @@ func (s *Service) RevokeKey(ctx *service.SessionContext, rd *revocation.RevData) // get response from Revocation service var resp message.Message - if resp, err = service.RequestResponse(ctx, "gns", "Revocation", config.Cfg.Revocation.Endpoint, req); err != nil { + if resp, err = service.RequestResponse(ctx, "gns", "Revocation", config.Cfg.Revocation.Service.Socket, req); err != nil { return } @@ -225,17 +228,17 @@ func (s *Service) RevokeKey(ctx *service.SessionContext, rd *revocation.RevData) //====================================================================== // LookupNamecache returns a cached lookup (if available) -func (s *Service) LookupNamecache(ctx *service.SessionContext, query *Query) (block *message.Block, err error) { - logger.Printf(logger.DBG, "[gns] LookupNamecache(%s)...\n", hex.EncodeToString(query.Key.Bits)) +func (s *Service) LookupNamecache(ctx context.Context, query *blocks.GNSQuery) (block *blocks.GNSBlock, err error) { + logger.Printf(logger.DBG, "[gns] LookupNamecache(%s)...\n", hex.EncodeToString(query.Key().Bits)) // assemble Namecache request - req := message.NewNamecacheLookupMsg(query.Key) + req := message.NewNamecacheLookupMsg(query.Key()) req.ID = uint32(util.NextID()) block = nil // get response from Namecache service var resp message.Message - if resp, err = service.RequestResponse(ctx, "gns", "Namecache", config.Cfg.Namecache.Endpoint, req); err != nil { + if resp, err = service.RequestResponse(ctx, "gns", "Namecache", config.Cfg.Namecache.Service.Socket, req); err != nil { return } @@ -250,7 +253,7 @@ func (s *Service) LookupNamecache(ctx *service.SessionContext, query *Query) (bl break } // check if block was found - if len(m.EncData) == 0 || util.IsNull(m.EncData) { + if len(m.EncData) == 0 || util.IsAll(m.EncData, 0) { logger.Println(logger.DBG, "[gns] block not found in namecache") break } @@ -262,21 +265,21 @@ func (s *Service) LookupNamecache(ctx *service.SessionContext, query *Query) (bl } // assemble GNSBlock from message - block = new(message.Block) + block = new(blocks.GNSBlock) block.DerivedKeySig = m.DerivedKeySig - sb := new(message.SignedBlockData) + sb := new(blocks.SignedGNSBlockData) sb.Purpose = new(crypto.SignaturePurpose) sb.Purpose.Purpose = enums.SIG_GNS_RECORD_SIGN sb.Purpose.Size = uint32(16 + len(m.EncData)) sb.Expire = m.Expire - sb.EncData = m.EncData - block.Block = sb + sb.Data = m.EncData + block.Body = sb // verify and decrypt block - if err = block.Verify(query.Zone, query.Label); err != nil { + if err = query.Verify(block); err != nil { break } - if err = block.Decrypt(query.Zone, query.Label); err != nil { + if err = query.Decrypt(block); err != nil { break } default: @@ -287,7 +290,7 @@ func (s *Service) LookupNamecache(ctx *service.SessionContext, query *Query) (bl } // StoreNamecache stores a lookup in the local namecache. -func (s *Service) StoreNamecache(ctx *service.SessionContext, block *message.Block) (err error) { +func (s *Service) StoreNamecache(ctx context.Context, query *blocks.GNSQuery, block *blocks.GNSBlock) (err error) { logger.Println(logger.DBG, "[gns] StoreNamecache()...") // assemble Namecache request @@ -296,7 +299,7 @@ func (s *Service) StoreNamecache(ctx *service.SessionContext, block *message.Blo // get response from Namecache service var resp message.Message - if resp, err = service.RequestResponse(ctx, "gns", "Namecache", config.Cfg.Namecache.Endpoint, req); err != nil { + if resp, err = service.RequestResponse(ctx, "gns", "Namecache", config.Cfg.Namecache.Service.Socket, req); err != nil { return } @@ -327,13 +330,13 @@ func (s *Service) StoreNamecache(ctx *service.SessionContext, block *message.Blo //====================================================================== // LookupDHT gets a GNS block from the DHT for the given query key. -func (s *Service) LookupDHT(ctx *service.SessionContext, query *Query) (block *message.Block, err error) { - logger.Printf(logger.DBG, "[gns] LookupDHT(%s)...\n", hex.EncodeToString(query.Key.Bits)) +func (s *Service) LookupDHT(ctx context.Context, query blocks.Query) (block blocks.Block, err error) { + logger.Printf(logger.DBG, "[gns] LookupDHT(%s)...\n", hex.EncodeToString(query.Key().Bits)) block = nil // client-connect to the DHT service logger.Println(logger.DBG, "[gns] Connecting to DHT service...") - cl, err := service.NewClient(config.Cfg.DHT.Endpoint) + cl, err := service.NewClient(ctx, config.Cfg.DHT.Service.Socket) if err != nil { return nil, err } @@ -360,7 +363,7 @@ func (s *Service) LookupDHT(ctx *service.SessionContext, query *Query) (block *m ) // send DHT GET request and wait for response - reqGet := message.NewDHTClientGetMsg(query.Key) + reqGet := message.NewDHTClientGetMsg(query.Key()) reqGet.ID = uint64(util.NextID()) reqGet.ReplLevel = uint32(enums.DHT_GNS_REPLICATION_LEVEL) reqGet.Type = uint32(enums.BLOCK_TYPE_GNS_NAMERECORD) @@ -368,16 +371,16 @@ func (s *Service) LookupDHT(ctx *service.SessionContext, query *Query) (block *m if err = interact(reqGet, true); err != nil { // check for aborted remote lookup: we need to cancel the query - if err == transport.ErrChannelInterrupted { + if err == service.ErrConnectionInterrupted { logger.Println(logger.WARN, "[gns] remote Lookup aborted -- cleaning up.") // send DHT GET_STOP request and terminate - reqStop := message.NewDHTClientGetStopMsg(query.Key) + reqStop := message.NewDHTClientGetStopMsg(query.Key()) reqStop.ID = reqGet.ID if err = interact(reqStop, false); err != nil { logger.Printf(logger.ERROR, "[gns] remote Lookup abort failed: %s\n", err.Error()) } - return nil, transport.ErrChannelInterrupted + return nil, service.ErrConnectionInterrupted } } @@ -407,22 +410,24 @@ func (s *Service) LookupDHT(ctx *service.SessionContext, query *Query) (block *m } // get GNSBlock from message - block = message.NewBlock() + 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()) break } + // verify and decrypt block - if err = block.Verify(query.Zone, query.Label); err != nil { + if err = qGNS.Verify(block); err != nil { break } - if err = block.Decrypt(query.Zone, query.Label); err != nil { + if err = qGNS.Decrypt(block); err != nil { break } // we got a result from DHT that was not in the namecache, // so store it there now. - if err = s.StoreNamecache(ctx, block); err != nil { + if err = s.StoreNamecache(ctx, qGNS, block.(*blocks.GNSBlock)); err != nil { logger.Printf(logger.ERROR, "[gns] can't store block in Namecache: %s\n", err.Error()) } } diff --git a/src/gnunet/service/module.go b/src/gnunet/service/module.go new file mode 100644 index 0000000..98307d6 --- /dev/null +++ b/src/gnunet/service/module.go @@ -0,0 +1,70 @@ +// 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 . +// +// SPDX-License-Identifier: AGPL3.0-or-later + +package service + +import ( + "context" + "gnunet/core" + "net/http" +) + +// Module is an interface for GNUnet service modules (workers). +type Module interface { + // RPC returns the route and handler for JSON-RPC requests + RPC() (string, func(http.ResponseWriter, *http.Request)) + + // Filter returns the event filter for the module + Filter() *core.EventFilter +} + +// EventHandler is a function prototype for event handling +type EventHandler func(context.Context, *core.Event) + +// ModuleImpl is an event-handling type used by Module implementations. +type ModuleImpl struct { + ch chan *core.Event // channel for core events. +} + +// NewModuleImplementation returns a new base module and starts +func NewModuleImpl() (m *ModuleImpl) { + return &ModuleImpl{ + ch: make(chan *core.Event), + } +} + +// Run event handling loop +func (m *ModuleImpl) Run(ctx context.Context, hdlr EventHandler, filter *core.EventFilter) (listener *core.Listener) { + // listener for registration + listener = core.NewListener(m.ch, filter) + // run event loop + go func() { + for { + select { + // Handle events + case event := <-m.ch: + hdlr(ctx, event) + + // wait for terminate signal + case <-ctx.Done(): + return + } + } + }() + return +} diff --git a/src/gnunet/service/namecache/module.go b/src/gnunet/service/namecache/module.go index d5aa014..9d5bca1 100644 --- a/src/gnunet/service/namecache/module.go +++ b/src/gnunet/service/namecache/module.go @@ -19,9 +19,11 @@ package namecache import ( - "gnunet/message" + "context" + "gnunet/config" + "gnunet/core" "gnunet/service" - "gnunet/service/gns" + "gnunet/service/dht/blocks" ) //====================================================================== @@ -34,12 +36,29 @@ import ( // Namecache handles the transient storage of GNS blocks under the query key. type NamecacheModule struct { + service.ModuleImpl + + cache service.DHTStore // transient block cache +} + +// NewModule creates a new module instance. +func NewModule(ctx context.Context, c *core.Core) (m *NamecacheModule) { + m = &NamecacheModule{ + ModuleImpl: *service.NewModuleImpl(), + } + m.cache, _ = service.NewDHTStore(config.Cfg.Namecache.Storage) + return } -func (nc *NamecacheModule) Get(ctx *service.SessionContext, query *gns.Query) (*message.Block, error) { - return nil, nil +// Get an entry from the cache if available. +func (m *NamecacheModule) Get(ctx context.Context, query *blocks.GNSQuery) (block *blocks.GNSBlock, err error) { + var b blocks.Block + b, err = m.cache.Get(query) + err = blocks.Unwrap(b, block) + return } -func (nc *NamecacheModule) Put(ctx *service.SessionContext, block *message.Block) error { - return nil +// Put entry into the cache. +func (m *NamecacheModule) Put(ctx context.Context, query *blocks.GNSQuery, block *blocks.GNSBlock) error { + return m.cache.Put(query, block) } diff --git a/src/gnunet/service/revocation/module.go b/src/gnunet/service/revocation/module.go index d13c069..37b57ab 100644 --- a/src/gnunet/service/revocation/module.go +++ b/src/gnunet/service/revocation/module.go @@ -19,8 +19,11 @@ package revocation import ( + "context" "gnunet/config" + "gnunet/core" "gnunet/crypto" + "gnunet/message" "gnunet/service" "gnunet/util" "net/http" @@ -33,55 +36,71 @@ import ( // "GNUnet Revocation" implementation //====================================================================== +// The minimum average difficulty acceptable for a set of revocation PoWs +const MinAvgDifficulty = 23 + // Module handles the revocation-related calls to other modules. type Module struct { - bloomf *data.BloomFilter // bloomfilter for fast revocation check - kvs util.KeyValueStore // storage for known revocations + service.ModuleImpl + + bloomf *data.BloomFilter // bloomfilter for fast revocation check + kvs service.KVStore // storage for known revocations } -// Init a revocation module -func (m *Module) Init() error { - // Initialize access to revocation data storage - var err error - if m.kvs, err = util.OpenKVStore(config.Cfg.Revocation.Storage); err != nil { - return err - } - // traverse the storage and build bloomfilter for all keys - m.bloomf = data.NewBloomFilter(1000000, 1e-8) - keys, err := m.kvs.List() - if err != nil { - return err +// NewModule returns an initialized revocation module +func NewModule(ctx context.Context, c *core.Core) (m *Module) { + // create and init instance + m = &Module{ + ModuleImpl: *service.NewModuleImpl(), } - for _, key := range keys { - buf, err := util.DecodeStringToBinary(key, 32) - if err != nil { - return err + init := func() (err error) { + // Initialize access to revocation data storage + if m.kvs, err = service.NewKVStore(config.Cfg.Revocation.Storage); err != nil { + return } - m.bloomf.Add(buf) + // traverse the storage and build bloomfilter for all keys + m.bloomf = data.NewBloomFilter(1000000, 1e-8) + var keys []string + if keys, err = m.kvs.List(); err != nil { + return + } + for _, key := range keys { + m.bloomf.Add([]byte(key)) + } + return } - return nil -} - -// NewModule returns an initialized revocation module -func NewModule() *Module { - m := new(Module) - if err := m.Init(); err != nil { + if err := init(); err != nil { logger.Printf(logger.ERROR, "[revocation] Failed to initialize module: %s\n", err.Error()) return nil } + // register as listener for core events + listener := m.Run(ctx, m.event, m.Filter()) + c.Register("gns", listener) return m } -// 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" }`)) - } +//---------------------------------------------------------------------- + +// Filter returns the event filter for the service +func (m *Module) Filter() *core.EventFilter { + f := core.NewEventFilter() + f.AddMsgType(message.REVOCATION_QUERY) + f.AddMsgType(message.REVOCATION_QUERY_RESPONSE) + f.AddMsgType(message.REVOCATION_REVOKE) + f.AddMsgType(message.REVOCATION_REVOKE_RESPONSE) + return f } +// Event handler +func (m *Module) event(ctx context.Context, ev *core.Event) { + +} + +//---------------------------------------------------------------------- + // Query return true if the pkey is valid (not revoked) and false // if the pkey has been revoked. -func (m *Module) Query(ctx *service.SessionContext, zkey *crypto.ZoneKey) (valid bool, err error) { +func (m *Module) Query(ctx context.Context, zkey *crypto.ZoneKey) (valid bool, err error) { // fast check first: is the key in the bloomfilter? data := zkey.Bytes() if !m.bloomf.Contains(data) { @@ -100,9 +119,9 @@ func (m *Module) Query(ctx *service.SessionContext, zkey *crypto.ZoneKey) (valid } // Revoke a key with given revocation data -func (m *Module) Revoke(ctx *service.SessionContext, rd *RevData) (success bool, err error) { +func (m *Module) Revoke(ctx context.Context, rd *RevData) (success bool, err error) { // verify the revocation data - rc := rd.Verify(true) + diff, rc := rd.Verify(true) switch { case rc == -1: logger.Println(logger.WARN, "[revocation] Revoke: Missing/invalid signature") @@ -113,10 +132,12 @@ func (m *Module) Revoke(ctx *service.SessionContext, rd *RevData) (success bool, case rc == -3: logger.Println(logger.WARN, "[revocation] Revoke: Wrong PoW sequence order") return false, nil - case rc < 25: + } + if diff < float64(MinAvgDifficulty) { logger.Println(logger.WARN, "[revocation] Revoke: Difficulty to small") return false, nil } + // store the revocation data // (1) add it to the bloomfilter m.bloomf.Add(rd.ZoneKeySig.KeyData) @@ -129,3 +150,12 @@ func (m *Module) Revoke(ctx *service.SessionContext, rd *RevData) (success bool, err = m.kvs.Put(rd.ZoneKeySig.ID(), value) return true, 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" }`)) + } +} diff --git a/src/gnunet/service/revocation/pow.go b/src/gnunet/service/revocation/pow.go index 04bbb13..57ddad7 100644 --- a/src/gnunet/service/revocation/pow.go +++ b/src/gnunet/service/revocation/pow.go @@ -40,6 +40,11 @@ import ( // Proof-of-Work data //---------------------------------------------------------------------- +const ( + // MinDifficulty for revocations -> expires in ~1year + MinDifficulty = 23 +) + // PoWData is the proof-of-work data type PoWData struct { PoW uint64 `order:"big"` // start with this PoW value @@ -143,6 +148,11 @@ func NewRevDataFromMsg(m *message.RevocationRevokeMsg) *RevData { return rd } +// Size of a serialized RevData object. +func (rd *RevData) Size() int { + return 16 + 8*len(rd.PoWs) + int(rd.ZoneKeySig.SigSize()) +} + // Sign the revocation data func (rd *RevData) Sign(skey *crypto.ZonePrivate) (err error) { sigBlock := &SignedRevData{ @@ -160,12 +170,10 @@ func (rd *RevData) Sign(skey *crypto.ZonePrivate) (err error) { return } -// Verify a revocation object: returns the (smallest) number of leading -// zero-bits in the PoWs of this revocation; a number > 0, but smaller -// than the minimum (25) indicates invalid PoWs; a value of -1 indicates -// a failed signature; -2 indicates an expired revocation and -3 for a -// "out-of-order" PoW sequence. -func (rd *RevData) Verify(withSig bool) int { +// Verify a revocation object and return the average difficulty of the PoWs +// 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 { @@ -179,39 +187,36 @@ func (rd *RevData) Verify(withSig bool) int { } sigData, err := data.Marshal(sigBlock) if err != nil { - return -1 + return 0., -1 } valid, err := rd.ZoneKeySig.Verify(sigData) if err != nil || !valid { - return -1 + return 0., -1 } } // (2) check PoWs - var ( - zbits float64 = 0 - last uint64 = 0 - ) + var last uint64 = 0 for _, pow := range rd.PoWs { // check sequence order if pow <= last { - return -3 + return 0., -3 } last = pow // compute number of leading zero-bits work := NewPoWData(pow, rd.Timestamp, &rd.ZoneKeySig.ZoneKey) zbits += float64(512 - work.Compute().BitLen()) } - zbits /= 32.0 + zbits /= float64(len(rd.PoWs)) // (3) check expiration - if zbits > 24.0 { - ttl := time.Duration(int((zbits-24)*365*24)) * time.Hour + if zbits >= 23.0 { + ttl := time.Duration(int((zbits-22)*365*24*1.1)) * time.Hour if util.AbsoluteTimeNow().Add(ttl).Expired() { - return -2 + return zbits, -2 } } - return int(zbits) + return zbits, 0 } //---------------------------------------------------------------------- @@ -240,6 +245,11 @@ func NewRevDataCalc(zkey *crypto.ZoneKey) *RevDataCalc { return rd } +// Size of a serialized RevData object. +func (rdc *RevDataCalc) Size() int { + return rdc.RevData.Size() + 2*len(rdc.Bits) + 1 +} + // Average number of leading zero-bits in current list func (rdc *RevDataCalc) Average() float64 { var sum uint16 = 0 diff --git a/src/gnunet/service/revocation/pow_test.go b/src/gnunet/service/revocation/pow_test.go index 8280402..a59f92b 100644 --- a/src/gnunet/service/revocation/pow_test.go +++ b/src/gnunet/service/revocation/pow_test.go @@ -16,7 +16,6 @@ func TestRevocationRFC(t *testing.T) { var ( D = "6fea32c05af58bfa979553d188605fd57d8bf9cc263b78d5f7478c07b998ed70" ZKEY = "000100002ca223e879ecc4bbdeb5da17319281d63b2e3b6955f1c3775c804a98d5f8ddaa" - DIFF = 7 PROOF = "" + "0005d66da3598127" + "0000395d1827c000" + @@ -142,8 +141,11 @@ func TestRevocationRFC(t *testing.T) { } // verify revocation data object - rc := revData.Verify(true) - if rc != DIFF { + diff, rc := revData.Verify(true) + if testing.Verbose() { + t.Logf("Average difficulty of PoWs = %f\n", diff) + } + if rc != 0 { t.Fatalf("REV_Verify (pkey): %d\n", rc) } } diff --git a/src/gnunet/service/revocation/service.go b/src/gnunet/service/revocation/service.go index a82e582..4d48d40 100644 --- a/src/gnunet/service/revocation/service.go +++ b/src/gnunet/service/revocation/service.go @@ -19,11 +19,12 @@ package revocation import ( + "context" + "fmt" "io" "gnunet/message" "gnunet/service" - "gnunet/transport" "github.com/bfix/gospel/logger" ) @@ -44,114 +45,113 @@ func NewService() service.Service { return inst } -// Start the Revocation service -func (s *Service) Start(spec string) error { - return nil -} - -// Stop the Revocation service -func (s *Service) Stop() error { - return nil -} - // ServeClient processes a client channel. -func (s *Service) ServeClient(ctx *service.SessionContext, mc *transport.MsgChannel) { +func (s *Service) ServeClient(ctx context.Context, id int, mc *service.Connection) { reqID := 0 -loop: + var cancel context.CancelFunc + ctx, cancel = context.WithCancel(ctx) + for { // receive next message from client reqID++ - logger.Printf(logger.DBG, "[revocation:%d:%d] Waiting for client request...\n", ctx.ID, reqID) - msg, err := mc.Receive(ctx.Signaller()) + logger.Printf(logger.DBG, "[revocation:%d:%d] Waiting for client request...\n", id, reqID) + msg, err := mc.Receive(ctx) if err != nil { if err == io.EOF { - logger.Printf(logger.INFO, "[revocation:%d:%d] Client channel closed.\n", ctx.ID, reqID) - } else if err == transport.ErrChannelInterrupted { - logger.Printf(logger.INFO, "[revocation:%d:%d] Service operation interrupted.\n", ctx.ID, reqID) + logger.Printf(logger.INFO, "[revocation:%d:%d] Client channel closed.\n", id, reqID) + } else if err == service.ErrConnectionInterrupted { + logger.Printf(logger.INFO, "[revocation:%d:%d] Service operation interrupted.\n", id, reqID) } else { - logger.Printf(logger.ERROR, "[revocation:%d:%d] Message-receive failed: %s\n", ctx.ID, reqID, err.Error()) + logger.Printf(logger.ERROR, "[revocation:%d:%d] Message-receive failed: %s\n", id, reqID, err.Error()) } - break loop - } - logger.Printf(logger.INFO, "[revocation:%d:%d] Received request: %v\n", ctx.ID, reqID, msg) - - // handle request - switch m := msg.(type) { - case *message.RevocationQueryMsg: - //---------------------------------------------------------- - // REVOCATION_QUERY - //---------------------------------------------------------- - go func(id int, m *message.RevocationQueryMsg) { - logger.Printf(logger.INFO, "[revocation:%d:%d] Query request received.\n", ctx.ID, id) - var resp *message.RevocationQueryResponseMsg - ctx.Add() - defer func() { - // send response - if resp != nil { - if err := mc.Send(resp, ctx.Signaller()); err != nil { - logger.Printf(logger.ERROR, "[revocation:%d:%d] Failed to send response: %s\n", ctx.ID, id, err.Error()) - } - } - // go-routine finished - logger.Printf(logger.DBG, "[revocation:%d:%d] Query request finished.\n", ctx.ID, id) - ctx.Remove() - }() - - valid, err := s.Query(ctx, m.Zone) - if err != nil { - logger.Printf(logger.ERROR, "[revocation:%d:%d] Failed to query revocation status: %s\n", ctx.ID, id, err.Error()) - if err == transport.ErrChannelInterrupted { - resp = nil - } - return - } - resp = message.NewRevocationQueryResponseMsg(valid) - }(reqID, m) - - case *message.RevocationRevokeMsg: - //---------------------------------------------------------- - // REVOCATION_REVOKE - //---------------------------------------------------------- - go func(id int, m *message.RevocationRevokeMsg) { - logger.Printf(logger.INFO, "[revocation:%d:%d] Revoke request received.\n", ctx.ID, id) - var resp *message.RevocationRevokeResponseMsg - ctx.Add() - defer func() { - // send response - if resp != nil { - if err := mc.Send(resp, ctx.Signaller()); err != nil { - logger.Printf(logger.ERROR, "[revocation:%d:%d] Failed to send response: %s\n", ctx.ID, id, err.Error()) - } - } - // go-routine finished - logger.Printf(logger.DBG, "[revocation:%d:%d] Revoke request finished.\n", ctx.ID, id) - ctx.Remove() - }() - - rd := NewRevDataFromMsg(m) - valid, err := s.Revoke(ctx, rd) - if err != nil { - logger.Printf(logger.ERROR, "[revocation:%d:%d] Failed to revoke key: %s\n", ctx.ID, id, err.Error()) - if err == transport.ErrChannelInterrupted { - resp = nil - } - return - } - resp = message.NewRevocationRevokeResponseMsg(valid) - }(reqID, m) - - default: - //---------------------------------------------------------- - // UNKNOWN message type received - //---------------------------------------------------------- - logger.Printf(logger.ERROR, "[revocation:%d:%d] Unhandled message of type (%d)\n", ctx.ID, reqID, msg.Header().MsgType) - break loop + break } + 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) } // close client connection mc.Close() // cancel all tasks running for this session/connection - logger.Printf(logger.INFO, "[revocation:%d] Start closing session... [%d]\n", ctx.ID, ctx.Waiting()) - ctx.Cancel() + logger.Printf(logger.INFO, "[revocation:%d] Start closing session...\n", id) + cancel() +} + +// Handle a single incoming message +func (s *Service) HandleMessage(ctx context.Context, msg message.Message, back service.Responder) bool { + // assemble log label + label := "" + if v := ctx.Value("label"); v != nil { + label = v.(string) + } + switch m := msg.(type) { + case *message.RevocationQueryMsg: + //---------------------------------------------------------- + // REVOCATION_QUERY + //---------------------------------------------------------- + go func(m *message.RevocationQueryMsg) { + logger.Printf(logger.INFO, "[revocation%s] Query request received.\n", label) + var resp *message.RevocationQueryResponseMsg + defer func() { + // send response + if resp != nil { + if err := back.Send(ctx, resp); err != nil { + logger.Printf(logger.ERROR, "[revocation%s] Failed to send response: %s\n", label, err.Error()) + } + } + // go-routine finished + logger.Printf(logger.DBG, "[revocation%s] Query request finished.\n", label) + }() + + valid, err := s.Query(ctx, m.Zone) + if err != nil { + logger.Printf(logger.ERROR, "[revocation%s] Failed to query revocation status: %s\n", label, err.Error()) + if err == service.ErrConnectionInterrupted { + resp = nil + } + return + } + resp = message.NewRevocationQueryResponseMsg(valid) + }(m) + + case *message.RevocationRevokeMsg: + //---------------------------------------------------------- + // REVOCATION_REVOKE + //---------------------------------------------------------- + go func(m *message.RevocationRevokeMsg) { + logger.Printf(logger.INFO, "[revocation%s] Revoke request received.\n", label) + var resp *message.RevocationRevokeResponseMsg + defer func() { + // send response + if resp != nil { + if err := back.Send(ctx, resp); err != nil { + logger.Printf(logger.ERROR, "[revocation%s] Failed to send response: %s\n", label, err.Error()) + } + } + // go-routine finished + logger.Printf(logger.DBG, "[revocation%s] Revoke request finished.\n", label) + }() + + rd := NewRevDataFromMsg(m) + valid, err := s.Revoke(ctx, rd) + if err != nil { + logger.Printf(logger.ERROR, "[revocation%s] Failed to revoke key: %s\n", label, err.Error()) + if err == service.ErrConnectionInterrupted { + resp = nil + } + return + } + resp = message.NewRevocationRevokeResponseMsg(valid) + }(m) + + default: + //---------------------------------------------------------- + // UNKNOWN message type received + //---------------------------------------------------------- + logger.Printf(logger.ERROR, "[revocation%s] Unhandled message of type (%d)\n", label, msg.Header().MsgType) + return false + } + return true } diff --git a/src/gnunet/service/service.go b/src/gnunet/service/service.go index c996e0c..32ccf67 100644 --- a/src/gnunet/service/service.go +++ b/src/gnunet/service/service.go @@ -1,5 +1,5 @@ // This file is part of gnunet-go, a GNUnet-implementation in Golang. -// Copyright (C) 2019, 2020 Bernd Fix >Y< +// 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 @@ -19,145 +19,133 @@ package service import ( + "context" + "errors" "fmt" - "net/http" - "sync" - - "gnunet/transport" + "gnunet/message" + "gnunet/util" "github.com/bfix/gospel/logger" ) -// Module is an interface for GNUnet service modules (workers). -type Module interface { - // RPC returns the route and handler for JSON-RPC requests - RPC() (string, func(http.ResponseWriter, *http.Request)) +//---------------------------------------------------------------------- + +// Responder is a back-channel for messages generated during +// message processing. The Connection type is a responder +// and used as such in ServeClient(). +type Responder interface { + // Handle outgoing message + Send(ctx context.Context, msg message.Message) error +} + +// TransportResponder is used as a responder in message handling for +// messages received from Transport. +type TransportResponder struct { + Peer *util.PeerID + SendFcn func(context.Context, *util.PeerID, message.Message) error } -// Service is an interface for GNUnet services. Every service has one channel -// end-point it listens to for incoming channel requests (network-based -// channels established by service clients). The end-point is specified in -// Channel semantics in the specification string. +// Send a message back to caller. +func (r *TransportResponder) Send(ctx context.Context, msg message.Message) error { + if r.SendFcn == nil { + return errors.New("no send function defined") + } + return r.SendFcn(ctx, r.Peer, msg) +} + +//---------------------------------------------------------------------- + +// Service is an interface for GNUnet services type Service interface { Module - // Start a service on the given endpoint - Start(spec string) error - // Serve a client session - ServeClient(ctx *SessionContext, ch *transport.MsgChannel) - // Stop the service - Stop() error + + // Serve a client session: A service has a socket it listens to for + // incoming connections (sessions) which are used for message exchange + // with local GNUnet services or clients. + ServeClient(ctx context.Context, id int, mc *Connection) + + // 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 Responder) bool } -// Impl is an implementation of generic service functionality. -type Impl struct { - impl Service // Specific service implementation - hdlr chan transport.Channel // Channel from listener - ctrl chan bool // Control channel - drop chan int // Channel to drop a session from pending list - srvc transport.ChannelServer // multi-user service - wg *sync.WaitGroup // wait group for go routine synchronization - name string // service name - running bool // service currently running? - pending map[int]*SessionContext // list of pending sessions +// SocketHandler handles incoming connections on the local service socket. +// It delegates calls to ServeClient() and HandleMessage() methods +// to a custom service 'srv'. +type SocketHandler struct { + srv Service // Specific service implementation + hdlr chan *Connection // handler for incoming connections + cmgr *ConnectionManager // manager for client connections + name string // service name } -// NewServiceImpl instantiates a new ServiceImpl object. -func NewServiceImpl(name string, srv Service) *Impl { - return &Impl{ - impl: srv, - hdlr: make(chan transport.Channel), - ctrl: make(chan bool), - drop: make(chan int), - srvc: nil, - wg: new(sync.WaitGroup), - name: name, - running: false, - pending: make(map[int]*SessionContext), +// NewSocketHandler instantiates a new socket handler. +func NewSocketHandler(name string, srv Service) *SocketHandler { + return &SocketHandler{ + srv: srv, + hdlr: make(chan *Connection), + cmgr: nil, + name: name, } } -// Start a service -func (si *Impl) Start(spec string) (err error) { +// Start the socket handler by listening on a Unix domain socket specified +// by its path and additional parameters. Incoming connections from clients +// are dispatched to 'hdlr'. Stopped socket handlers can be re-started. +func (h *SocketHandler) Start(ctx context.Context, path string, params map[string]string) (err error) { // check if we are already running - if si.running { - logger.Printf(logger.ERROR, "Service '%s' already running.\n", si.name) + if h.cmgr != nil { + logger.Printf(logger.ERROR, "Service '%s' already running.\n", h.name) return fmt.Errorf("service already running") } - - // start channel server - logger.Printf(logger.INFO, "[%s] Service starting.\n", si.name) - if si.srvc, err = transport.NewChannelServer(spec, si.hdlr); err != nil { + // start connection manager + logger.Printf(logger.INFO, "[%s] Service starting.\n", h.name) + if h.cmgr, err = NewConnectionManager(ctx, path, params, h.hdlr); err != nil { return } - si.running = true - // handle clients - si.wg.Add(1) + // handle client connections go func() { - defer si.wg.Done() loop: - for si.running { + for { select { - // handle incoming connections - case in := <-si.hdlr: - if in == nil { - logger.Printf(logger.INFO, "[%s] Listener terminated.\n", si.name) - break loop - } - switch ch := in.(type) { - case transport.Channel: - // run a new session with context - ctx := NewSessionContext() - sessID := ctx.ID - si.pending[sessID] = ctx - logger.Printf(logger.INFO, "[%s] Session '%d' started.\n", si.name, sessID) - - go func() { - // serve client on the message channel - si.impl.ServeClient(ctx, transport.NewMsgChannel(ch)) - // session is done now. - logger.Printf(logger.INFO, "[%s] Session with client '%d' ended.\n", si.name, sessID) - si.drop <- sessID - }() - } - - // handle session removal - case sessID := <-si.drop: - delete(si.pending, sessID) - - // handle cancelation signal on listener. - case <-si.ctrl: + // handle incoming connection + case conn := <-h.hdlr: + // run a new session with context + id := util.NextID() + logger.Printf(logger.INFO, "[%s] Session '%d' started.\n", h.name, id) + + go func() { + // serve client on the message channel + h.srv.ServeClient(ctx, id, conn) + // session is done now. + logger.Printf(logger.INFO, "[%s] Session with client '%d' ended.\n", h.name, id) + }() + + // handle termination + case <-ctx.Done(): + logger.Printf(logger.INFO, "[%s] Listener terminated.\n", h.name) break loop } } - // terminate pending sessions - for _, ctx := range si.pending { - logger.Printf(logger.DBG, "[%s] Session '%d' closing...\n", si.name, ctx.ID) - ctx.Cancel() - } - // close-down service - logger.Printf(logger.INFO, "[%s] Service closing.\n", si.name) - si.srvc.Close() - si.running = false + logger.Printf(logger.INFO, "[%s] Service closing.\n", h.name) + h.cmgr.Close() }() - - return si.impl.Start(spec) + return nil } -// Stop a service -func (si *Impl) Stop() error { - if !si.running { - logger.Printf(logger.WARN, "Service '%s' not running.\n", si.name) +// Stop socket handler. +func (h *SocketHandler) Stop() error { + if h.cmgr == nil { + logger.Printf(logger.WARN, "Service '%s' not running.\n", h.name) return fmt.Errorf("service not running") } - si.running = false - si.ctrl <- true - logger.Printf(logger.INFO, "[%s] Service terminating.\n", si.name) - - err := si.impl.Stop() - si.wg.Wait() - return err + logger.Printf(logger.INFO, "[%s] Service terminating.\n", h.name) + h.cmgr.Close() + h.cmgr = nil + return nil } diff --git a/src/gnunet/service/store.go b/src/gnunet/service/store.go new file mode 100644 index 0000000..1e5af8b --- /dev/null +++ b/src/gnunet/service/store.go @@ -0,0 +1,379 @@ +// 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 . +// +// SPDX-License-Identifier: AGPL3.0-or-later + +package service + +import ( + "context" + "database/sql" + "encoding/binary" + "encoding/hex" + "errors" + "fmt" + "gnunet/crypto" + "gnunet/service/dht/blocks" + "gnunet/util" + "io/ioutil" + "os" + "strconv" + "strings" + + 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. +// One set of methods (Get/Put) work on DHT queries and blocks, +// the other set (GetS, PutS) work on key/value strings. +// Each custom implementation can decide which sets to support. +//------------------------------------------------------------ + +// 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. +type Store[K, V any] interface { + // Put block into storage under given key + Put(key K, val V) error + + // Get block with given key from storage + Get(key K) (V, error) + + // List all store queries + 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 string) (DHTStore, error) { + specs := strings.SplitN(spec, ":", 2) + if len(specs) < 2 { + return nil, ErrStoreInvalidSpec + } + switch specs[0] { + //------------------------------------------------------------------ + // File-base storage + //------------------------------------------------------------------ + case "file_store": + return NewFileStore(specs[1]) + case "file_cache": + if len(specs) < 3 { + return nil, ErrStoreInvalidSpec + } + return NewFileCache(specs[1], specs[2]) + } + return nil, ErrStoreUnknown +} + +//------------------------------------------------------------ +// NewKVStore creates a new storage handler with given spec +// for use with key/value string pairs. +func NewKVStore(spec string) (KVStore, error) { + specs := strings.SplitN(spec, ":", 2) + if len(specs) < 2 { + return nil, ErrStoreInvalidSpec + } + switch specs[0] { + //-------------------------------------------------------------- + // Redis service + //-------------------------------------------------------------- + case "redis": + if len(specs) < 4 { + return nil, ErrStoreInvalidSpec + } + return NewRedisStore(specs[1], specs[2], specs[3]) + + //-------------------------------------------------------------- + // SQL database service + //-------------------------------------------------------------- + case "sql": + if len(specs) < 4 { + return nil, ErrStoreInvalidSpec + } + return NewSQLStore(specs[1]) + } + return nil, errors.New("unknown storage mechanism") +} + +//------------------------------------------------------------ +// Filesystem-based storage +//------------------------------------------------------------ + +// FileStore implements a filesystem-based storage mechanism for +// DHT queries and blocks. +type FileStore struct { + path string // storage path + cached []*crypto.HashCode // list of cached entries (key) + wrPos int // write position in cyclic list +} + +// NewFileStore instantiates a new file storage. +func NewFileStore(path string) (DHTStore, error) { + // create file store + return &FileStore{ + path: path, + }, nil +} + +// NewFileCache instantiates a new file-based cache. +func NewFileCache(path, param string) (DHTStore, error) { + // remove old cache content + os.RemoveAll(path) + + // get number of cache entries + num, err := strconv.ParseUint(param, 10, 32) + if err != nil { + return nil, err + } + // create file store + return &FileStore{ + path: path, + cached: make([]*crypto.HashCode, num), + wrPos: 0, + }, nil +} + +// Put block into storage under given key +func (s *FileStore) Put(query blocks.Query, block blocks.Block) (err error) { + // get query parameters for entry + var ( + btype uint16 // block type + expire util.AbsoluteTime // block expiration + ) + query.Get("blkType", &btype) + query.Get("expire", &expire) + + // are we in caching mode? + if s.cached != nil { + // release previous block if defined + if key := s.cached[s.wrPos]; key != nil { + // get path and filename from key + path, fname := s.expandPath(key) + if err = os.Remove(path + "/" + fname); err != nil { + return + } + // free slot + s.cached[s.wrPos] = nil + } + } + // 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 + 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()) + } + } + } + // update cache list + if s.cached != nil { + s.cached[s.wrPos] = query.Key() + s.wrPos = (s.wrPos + 1) % len(s.cached) + } + 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:] +} + +//------------------------------------------------------------ +// 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(addr, passwd, db string) (s KVStore, err error) { + kvs := new(RedisStore) + if kvs.db, err = strconv.Atoi(db); err != nil { + err = ErrStoreInvalidSpec + return + } + kvs.client = redis.NewClient(&redis.Options{ + Addr: addr, + Password: passwd, + DB: kvs.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 string) (s KVStore, err error) { + kvs := new(SQLStore) + + // connect to SQL database + kvs.db, err = util.DbPool.Connect(spec) + 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/test.sh b/src/gnunet/test.sh new file mode 100755 index 0000000..78495a8 --- /dev/null +++ b/src/gnunet/test.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +go test $* -gcflags "-N -l" ./... diff --git a/src/gnunet/transport/channel.go b/src/gnunet/transport/channel.go deleted file mode 100644 index 1632aab..0000000 --- a/src/gnunet/transport/channel.go +++ /dev/null @@ -1,213 +0,0 @@ -// This file is part of gnunet-go, a GNUnet-implementation in Golang. -// Copyright (C) 2019, 2020 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 . -// -// SPDX-License-Identifier: AGPL3.0-or-later - -package transport - -import ( - "encoding/hex" - "errors" - "fmt" - "path" - "strings" - - "gnunet/message" - "gnunet/util" - - "github.com/bfix/gospel/concurrent" - "github.com/bfix/gospel/data" - "github.com/bfix/gospel/logger" -) - -// Error codes -var ( - ErrChannelNotImplemented = fmt.Errorf("protocol not implemented") - ErrChannelNotOpened = fmt.Errorf("channel not opened") - ErrChannelInterrupted = fmt.Errorf("channel interrupted") -) - -//////////////////////////////////////////////////////////////////////// -// CHANNEL - -// Channel is an abstraction for exchanging arbitrary data over various -// transport protocols and mechanisms. They are created by clients via -// 'NewChannel()' or by services run via 'NewChannelServer()'. -// A string specifies the end-point of the channel: -// "unix+/tmp/test.sock" -- for UDS channels -// "tcp+1.2.3.4:5" -- for TCP channels -// "udp+1.2.3.4:5" -- for UDP channels -type Channel interface { - Open(spec string) error // open channel (for read/write) - Close() error // close open channel - IsOpen() bool // check if channel is open - Read([]byte, *concurrent.Signaller) (int, error) // read from channel - Write([]byte, *concurrent.Signaller) (int, error) // write to channel -} - -// ChannelFactory instantiates specific Channel imülementations. -type ChannelFactory func() Channel - -// Known channel implementations. -var channelImpl = map[string]ChannelFactory{ - "unix": NewSocketChannel, - "tcp": NewTCPChannel, - "udp": NewUDPChannel, -} - -// NewChannel creates a new channel to the specified endpoint. -// Called by a client to connect to a service. -func NewChannel(spec string) (Channel, error) { - parts := strings.Split(spec, "+") - if fac, ok := channelImpl[parts[0]]; ok { - inst := fac() - err := inst.Open(spec) - return inst, err - } - return nil, ErrChannelNotImplemented -} - -//////////////////////////////////////////////////////////////////////// -// CHANNEL SERVER - -// ChannelServer creates a listener for the specified endpoint. -// The specification string has the same format as for Channel with slightly -// different semantics (for TCP, and ICMP the address specifies is a mask -// for client addresses accepted for a channel request). -type ChannelServer interface { - Open(spec string, hdlr chan<- Channel) error - Close() error -} - -// ChannelServerFactory instantiates specific ChannelServer imülementations. -type ChannelServerFactory func() ChannelServer - -// Known channel server implementations. -var channelServerImpl = map[string]ChannelServerFactory{ - "unix": NewSocketChannelServer, - "tcp": NewTCPChannelServer, - "udp": NewUDPChannelServer, -} - -// NewChannelServer creates a new channel server instance -func NewChannelServer(spec string, hdlr chan<- Channel) (cs ChannelServer, err error) { - parts := strings.Split(spec, "+") - - if fac, ok := channelServerImpl[parts[0]]; ok { - // check if the basedir for the spec exists... - if err = util.EnforceDirExists(path.Dir(parts[1])); err != nil { - return - } - // instantiate server implementation - cs = fac() - // create the domain socket and listen to it. - err = cs.Open(spec, hdlr) - return - } - return nil, ErrChannelNotImplemented -} - -//////////////////////////////////////////////////////////////////////// -// MESSAGE CHANNEL - -// MsgChannel s a wrapper around a generic channel for GNUnet message exchange. -type MsgChannel struct { - ch Channel - buf []byte -} - -// NewMsgChannel wraps a plain Channel for GNUnet message exchange. -func NewMsgChannel(ch Channel) *MsgChannel { - return &MsgChannel{ - ch: ch, - buf: make([]byte, 65536), - } -} - -// Close a MsgChannel by closing the wrapped plain Channel. -func (c *MsgChannel) Close() error { - return c.ch.Close() -} - -// Send a GNUnet message over a channel. -func (c *MsgChannel) Send(msg message.Message, sig *concurrent.Signaller) error { - // convert message to binary data - data, err := data.Marshal(msg) - if err != nil { - return err - } - logger.Printf(logger.DBG, "==> %v\n", msg) - logger.Printf(logger.DBG, " [%s]\n", hex.EncodeToString(data)) - - // check message header size and packet size - mh, err := message.GetMsgHeader(data) - if err != nil { - return err - } - if len(data) != int(mh.MsgSize) { - return errors.New("send: message size mismatch") - } - - // send packet - n, err := c.ch.Write(data, sig) - if err != nil { - return err - } - if n != len(data) { - return errors.New("incomplete send") - } - return nil -} - -// Receive GNUnet messages over a plain Channel. -func (c *MsgChannel) Receive(sig *concurrent.Signaller) (message.Message, error) { - // get bytes from channel - get := func(pos, count int) error { - n, err := c.ch.Read(c.buf[pos:pos+count], sig) - if err != nil { - return err - } - if n != count { - return errors.New("not enough bytes on network") - } - return nil - } - - if err := get(0, 4); err != nil { - return nil, err - } - mh, err := message.GetMsgHeader(c.buf[:4]) - if err != nil { - return nil, err - } - - if err := get(4, int(mh.MsgSize)-4); err != nil { - return nil, err - } - msg, err := message.NewEmptyMessage(mh.MsgType) - if err != nil { - return nil, err - } - if msg == nil { - return nil, fmt.Errorf("message{%d} is nil", mh.MsgType) - } - if err = data.Unmarshal(msg, c.buf[:mh.MsgSize]); err != nil { - return nil, err - } - logger.Printf(logger.DBG, "<== %v\n", msg) - logger.Printf(logger.DBG, " [%s]\n", hex.EncodeToString(c.buf[:mh.MsgSize])) - return msg, nil -} diff --git a/src/gnunet/transport/channel_netw.go b/src/gnunet/transport/channel_netw.go deleted file mode 100644 index c0de978..0000000 --- a/src/gnunet/transport/channel_netw.go +++ /dev/null @@ -1,285 +0,0 @@ -// This file is part of gnunet-go, a GNUnet-implementation in Golang. -// Copyright (C) 2019, 2020 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 . -// -// SPDX-License-Identifier: AGPL3.0-or-later - -package transport - -import ( - "net" - "os" - "strconv" - "strings" - - "github.com/bfix/gospel/concurrent" - "github.com/bfix/gospel/logger" -) - -// ChannelResult for read/write operations on channels. -type ChannelResult struct { - count int // number of bytes read/written - err error // error (or nil) -} - -// NewChannelResult instanciates a new object with given attributes. -func NewChannelResult(n int, err error) *ChannelResult { - return &ChannelResult{ - count: n, - err: err, - } -} - -// Values returns the attributes of a result instance (for passing up the -// call stack). -func (cr *ChannelResult) Values() (int, error) { - return cr.count, cr.err -} - -//////////////////////////////////////////////////////////////////////// -// Generic network-based Channel - -// NetworkChannel represents a network-based channel -type NetworkChannel struct { - network string // network protocol identifier ("tcp", "unix", ...) - conn net.Conn // associated connection -} - -// NewNetworkChannel creates a new channel for a given network protocol. -// The channel is in pending state and need to be opened before use. -func NewNetworkChannel(netw string) Channel { - return &NetworkChannel{ - network: netw, - conn: nil, - } -} - -// Open a network channel based on specification: -// The specification is a string separated into parts by the '+' delimiter -// (e.g. "unix+/tmp/gnunet-service-gns-go.sock+perm=0770"). The network -// identifier (first part) must match the network specification of the -// underlaying NetworkChannel instance. -func (c *NetworkChannel) Open(spec string) (err error) { - parts := strings.Split(spec, "+") - // check for correct protocol - if parts[0] != c.network { - return ErrChannelNotImplemented - } - // open connection - c.conn, err = net.Dial(c.network, parts[1]) - return -} - -// Close a network channel -func (c *NetworkChannel) Close() error { - if c.conn != nil { - rc := c.conn.Close() - c.conn = nil - return rc - } - return ErrChannelNotOpened -} - -// IsOpen returns true if the channel is opened -func (c *NetworkChannel) IsOpen() bool { - return c.conn != nil -} - -// Read bytes from a network channel into buffer: Returns the number of read -// bytes and an error code. Only works on open channels ;) -// The read can be aborted by sending 'true' on the cmd interface; the -// channel is closed after such interruption. -func (c *NetworkChannel) Read(buf []byte, sig *concurrent.Signaller) (int, error) { - // check if the channel is open - if c.conn == nil { - return 0, ErrChannelNotOpened - } - // perform operation in go-routine - result := make(chan *ChannelResult) - go func() { - result <- NewChannelResult(c.conn.Read(buf)) - }() - - listener := sig.Listen() - defer sig.Drop(listener) - for { - select { - // handle terminate command - case x := <-listener: - switch val := x.(type) { - case bool: - if val { - return 0, ErrChannelInterrupted - } - } - // handle result of read operation - case res := <-result: - return res.Values() - } - } -} - -// Write buffer to a network channel: Returns the number of written bytes and -// an error code. The write operation can be aborted by sending 'true' on the -// command channel; the network channel is closed after such interrupt. -func (c *NetworkChannel) Write(buf []byte, sig *concurrent.Signaller) (int, error) { - // check if we have an open channel to write to. - if c.conn == nil { - return 0, ErrChannelNotOpened - } - // perform operation in go-routine - result := make(chan *ChannelResult) - go func() { - result <- NewChannelResult(c.conn.Write(buf)) - }() - - listener := sig.Listen() - defer sig.Drop(listener) - for { - select { - // handle terminate command - case x := <-listener: - switch val := x.(type) { - case bool: - if val { - return 0, ErrChannelInterrupted - } - } - // handle result of read operation - case res := <-result: - return res.Values() - } - } -} - -//////////////////////////////////////////////////////////////////////// -// Generic network-based ChannelServer - -// NetworkChannelServer represents a network-based channel server -type NetworkChannelServer struct { - network string // network protocol to listen on - listener net.Listener // reference to listener object -} - -// NewNetworkChannelServer creates a new network-based channel server -func NewNetworkChannelServer(netw string) ChannelServer { - return &NetworkChannelServer{ - network: netw, - listener: nil, - } -} - -// Open a network channel server (= start running it) based on the given -// specification. For every client connection to the server, the associated -// network channel for the connection is send via the hdlr channel. -func (s *NetworkChannelServer) Open(spec string, hdlr chan<- Channel) (err error) { - parts := strings.Split(spec, "+") - // check for correct protocol - if parts[0] != s.network { - return ErrChannelNotImplemented - } - // create listener - if s.listener, err = net.Listen(s.network, parts[1]); err != nil { - return - } - // handle additional parameters ('key[=value]') - for _, param := range parts[2:] { - frag := strings.Split(param, "=") - switch frag[0] { - case "perm": // set permissions on 'unix' - if s.network == "unix" { - if perm, err := strconv.ParseInt(frag[1], 8, 32); err == nil { - if err := os.Chmod(parts[1], os.FileMode(perm)); err != nil { - logger.Printf( - logger.ERROR, - "NetworkChannelServer: Failed to set permissions: %s\n", - err.Error()) - - } - } else { - logger.Printf( - logger.ERROR, - "NetworkChannelServer: Invalid permissions '%s'\n", - frag[1]) - } - } - } - } - // run go routine to handle channel requests from clients - go func() { - for { - conn, err := s.listener.Accept() - if err != nil { - // signal failure and terminate - hdlr <- nil - break - } - // send channel to handler - hdlr <- &NetworkChannel{ - network: s.network, - conn: conn, - } - } - if s.listener != nil { - s.listener.Close() - } - }() - - return nil -} - -// Close a network channel server (= stop the server) -func (s *NetworkChannelServer) Close() error { - if s.listener != nil { - err := s.listener.Close() - s.listener = nil - return err - } - return nil -} - -//////////////////////////////////////////////////////////////////////// -// helper functions to instantiate network channels and servers for -// common network protocols - -// NewSocketChannel implements a Unix Domain Socket connection -func NewSocketChannel() Channel { - return NewNetworkChannel("unix") -} - -// NewTCPChannel implements a: TCP connection -func NewTCPChannel() Channel { - return NewNetworkChannel("tcp") -} - -// NewUDPChannel implements an UDP connection -func NewUDPChannel() Channel { - return NewNetworkChannel("udp") -} - -// NewSocketChannelServer implements an Unix Domain Socket listener -func NewSocketChannelServer() ChannelServer { - return NewNetworkChannelServer("unix") -} - -// NewTCPChannelServer implements a TCP listener -func NewTCPChannelServer() ChannelServer { - return NewNetworkChannelServer("tcp") -} - -// NewUDPChannelServer implements an UDP listener -func NewUDPChannelServer() ChannelServer { - return NewNetworkChannelServer("udp") -} diff --git a/src/gnunet/transport/channel_test.go b/src/gnunet/transport/channel_test.go deleted file mode 100644 index f028171..0000000 --- a/src/gnunet/transport/channel_test.go +++ /dev/null @@ -1,232 +0,0 @@ -// This file is part of gnunet-go, a GNUnet-implementation in Golang. -// Copyright (C) 2019, 2020 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 . -// -// SPDX-License-Identifier: AGPL3.0-or-later - -package transport - -import ( - "bytes" - "fmt" - "testing" - "time" - - "github.com/bfix/gospel/concurrent" -) - -// TODO: These test cases fail from time to time for no obvious reason. -// This needs to be investigated. - -const ( - SockAddr = "/tmp/gnunet-go-test.sock" - TCPAddrClient = "gnunet.org:80" - TCPAddrServer = "127.0.0.1:9876" -) - -type TestChannelServer struct { - hdlr chan Channel - srvc ChannelServer - running bool -} - -func NewTestChannelServer() *TestChannelServer { - return &TestChannelServer{ - hdlr: make(chan Channel), - srvc: nil, - running: false, - } -} - -func (s *TestChannelServer) handle(ch Channel, sig *concurrent.Signaller) { - buf := make([]byte, 4096) - for { - n, err := ch.Read(buf, sig) - if err != nil { - break - } - _, err = ch.Write(buf[:n], sig) - if err != nil { - break - } - } - ch.Close() -} - -func (s *TestChannelServer) Start(spec string) (err error) { - // check if we are already running - if s.running { - return fmt.Errorf("Server already running") - } - - // start channel server - if s.srvc, err = NewChannelServer(spec, s.hdlr); err != nil { - return - } - s.running = true - - // handle clients - sig := concurrent.NewSignaller() - go func() { - for s.running { - in := <-s.hdlr - if in == nil { - break - } - switch x := in.(type) { - case Channel: - go s.handle(x, sig) - } - } - s.srvc.Close() - s.running = false - }() - return nil -} - -func (s *TestChannelServer) Stop() { - s.running = false -} - -func TestChannelServerTCPSingle(t *testing.T) { - time.Sleep(time.Second) - s := NewTestChannelServer() - err := s.Start("tcp+" + TCPAddrServer) - defer s.Stop() - if err != nil { - t.Fatal(err) - } -} - -func TestChannelServerTCPTwice(t *testing.T) { - time.Sleep(time.Second) - s1 := NewTestChannelServer() - err := s1.Start("tcp+" + TCPAddrServer) - defer s1.Stop() - if err != nil { - t.Fatal(err) - } - time.Sleep(time.Second) - s2 := NewTestChannelServer() - err = s2.Start("tcp+" + TCPAddrServer) - defer s2.Stop() - if err == nil { - t.Fatal("SocketServer started twice!!") - } -} - -func TestChannelClientTCP(t *testing.T) { - time.Sleep(time.Second) - ch, err := NewChannel("tcp+" + TCPAddrClient) - if err != nil { - t.Fatal(err) - } - defer ch.Close() - - sig := concurrent.NewSignaller() - msg := []byte("GET /\n\n") - n, err := ch.Write(msg, sig) - if err != nil { - t.Fatal(err) - } - if n != len(msg) { - t.Fatal("Send size mismatch") - } - buf := make([]byte, 4096) - n = 0 - start := time.Now().Unix() - for n == 0 && (time.Now().Unix()-start) < 3 { - if n, err = ch.Read(buf, sig); err != nil { - t.Fatal(err) - } - } - t.Logf("'%s' [%d]\n", string(buf[:n]), n) -} - -func TestChannelClientServerTCP(t *testing.T) { - time.Sleep(time.Second) - s := NewTestChannelServer() - err := s.Start("tcp+" + TCPAddrServer) - defer s.Stop() - if err != nil { - t.Fatal(err) - } - - ch, err := NewChannel("tcp+" + TCPAddrServer) - if err != nil { - t.Fatal(err) - } - sig := concurrent.NewSignaller() - msg := []byte("GET /\n\n") - n, err := ch.Write(msg, sig) - if err != nil { - t.Fatal(err) - } - if n != len(msg) { - t.Fatal("Send size mismatch") - } - buf := make([]byte, 4096) - n = 0 - start := time.Now().Unix() - for n == 0 && (time.Now().Unix()-start) < 3 { - if n, err = ch.Read(buf, sig); err != nil { - t.Fatal(err) - } - } - if err = ch.Close(); err != nil { - t.Fatal(err) - } - if !bytes.Equal(buf[:n], msg) { - t.Fatal("message send/receive mismatch") - } -} - -func TestChannelClientServerSock(t *testing.T) { - time.Sleep(time.Second) - s := NewTestChannelServer() - if err := s.Start("unix+" + SockAddr); err != nil { - t.Fatal(err) - } - - ch, err := NewChannel("unix+" + SockAddr) - if err != nil { - t.Fatal(err) - } - sig := concurrent.NewSignaller() - msg := []byte("This is just a test -- please ignore...") - n, err := ch.Write(msg, sig) - if err != nil { - t.Fatal(err) - } - if n != len(msg) { - t.Fatal("Send size mismatch") - } - buf := make([]byte, 4096) - n = 0 - start := time.Now().Unix() - for n == 0 && (time.Now().Unix()-start) < 3 { - if n, err = ch.Read(buf, sig); err != nil { - t.Fatal(err) - } - } - if err = ch.Close(); err != nil { - t.Fatal(err) - } - if !bytes.Equal(buf[:n], msg) { - t.Fatal("message send/receive mismatch") - } - - s.Stop() -} diff --git a/src/gnunet/transport/connection.go b/src/gnunet/transport/connection.go index cc2c909..8337260 100644 --- a/src/gnunet/transport/connection.go +++ b/src/gnunet/transport/connection.go @@ -1,5 +1,5 @@ // This file is part of gnunet-go, a GNUnet-implementation in Golang. -// Copyright (C) 2019, 2020 Bernd Fix >Y< +// 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 @@ -19,59 +19,97 @@ package transport import ( - "gnunet/core" + "context" + "errors" "gnunet/message" + "net" +) - "github.com/bfix/gospel/concurrent" +// Error codes +var ( + ErrConnectionNotOpened = errors.New("connection not opened") + ErrConnectionInterrupted = errors.New("connection interrupted") ) -// Connection for communicating peers +//---------------------------------------------------------------------- + +// Connection is a net.Conn for GNUnet message exchange (send/receive) type Connection struct { - from, to *core.Peer - ch *MsgChannel - bandwidth uint32 - state int - shared []byte + conn net.Conn // associated connection + buf []byte // read/write buffer } -// NewConnection instanciates a new connection between peers communicating -// over a message channel (Connections are authenticated and secured). -func NewConnection(ch *MsgChannel, from, to *core.Peer) *Connection { +// NewConnection creates a new connection from an existing net.Conn +// This is usually used by clients to connect to a service. +func NewConnection(ctx context.Context, conn net.Conn) *Connection { return &Connection{ - from: from, - to: to, - state: 1, - ch: ch, + conn: conn, + buf: make([]byte, 65536), } } -// SharedSecret computes the shared secret the two endpoints of a connection. -func (c *Connection) SharedSecret(secret []byte) { - c.shared = make([]byte, len(secret)) - copy(c.shared, secret) +// Close connection +func (s *Connection) Close() error { + if s.conn != nil { + rc := s.conn.Close() + s.conn = nil + return rc + } + return ErrConnectionNotOpened } -// GetState returns the current state of the connection. -func (c *Connection) GetState() int { - return c.state +// Send a GNUnet message over connection +func (s *Connection) Send(ctx context.Context, msg message.Message) error { + return WriteMessage(ctx, s.conn, msg) } -// SetBandwidth to control transfer rates on the connection -func (c *Connection) SetBandwidth(bw uint32) { - c.bandwidth = bw +// Receive GNUnet messages from socket. +func (s *Connection) Receive(ctx context.Context) (message.Message, error) { + return ReadMessage(ctx, s.conn, s.buf) } -// Close connection between two peers. -func (c *Connection) Close() error { - return c.ch.Close() +//---------------------------------------------------------------------- + +// ConnectionManager handles client connections on a net.Listener +type ConnectionManager struct { + listener net.Listener // reference to listener object } -// Send a message on the connection -func (c *Connection) Send(msg message.Message, sig *concurrent.Signaller) error { - return c.ch.Send(msg, sig) +// NewConnectionManager creates a new net.Listener connection manager. +// Incoming connections from clients are dispatched to a handler channel. +func NewConnectionManager(ctx context.Context, listener net.Listener, hdlr chan *Connection) (cs *ConnectionManager, err error) { + // instantiate connection manager + cs = &ConnectionManager{ + listener: listener, + } + // run watch dog for termination + go func() { + <-ctx.Done() + cs.listener.Close() + }() + // run go routine to handle channel requests from clients + go func() { + for { + conn, err := cs.listener.Accept() + if err != nil { + return + } + // handle connection + c := &Connection{ + conn: conn, + buf: make([]byte, 65536), + } + hdlr <- c + } + }() + return cs, nil } -// Receive a message on the connection -func (c *Connection) Receive(sig *concurrent.Signaller) (message.Message, error) { - return c.ch.Receive(sig) +// Close a connection manager (= stop the server) +func (s *ConnectionManager) Close() (err error) { + if s.listener != nil { + err = s.listener.Close() + s.listener = nil + } + return } diff --git a/src/gnunet/transport/endpoint.go b/src/gnunet/transport/endpoint.go new file mode 100644 index 0000000..b54ee4e --- /dev/null +++ b/src/gnunet/transport/endpoint.go @@ -0,0 +1,282 @@ +// 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 . +// +// SPDX-License-Identifier: AGPL3.0-or-later + +package transport + +import ( + "bytes" + "context" + "errors" + "gnunet/message" + "gnunet/util" + "net" +) + +var ( + ErrEndpNotAvailable = errors.New("no endpoint for address available") + ErrEndpProtocolMismatch = errors.New("transport protocol mismatch") +) + +// Endpoint represents a local endpoint that can send and receive messages. +// Implementations need to manage the relations between peer IDs and +// 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 + + // Send message on endpoint + Send(context.Context, net.Addr, *TransportMessage) error + + // Address returns the listening address for the endpoint + Address() net.Addr + + // CanSendTo returns true if the endpoint can sent to address + CanSendTo(net.Addr) bool + + // Return endpoint identifier + ID() int +} + +//---------------------------------------------------------------------- + +// NewEndpoint returns a suitable endpoint for the address. +func NewEndpoint(addr net.Addr) (ep Endpoint, err error) { + switch epMode(addr.Network()) { + case "packet": + ep, err = newPacketEndpoint(addr) + case "stream": + ep, err = newStreamEndpoint(addr) + default: + err = ErrEndpNotAvailable + } + return +} + +//---------------------------------------------------------------------- +// Packet-oriented endpoint +//---------------------------------------------------------------------- + +// PacketEndpoint for packet-oriented network protocols +type PaketEndpoint struct { + id int // endpoint identifier + addr net.Addr // endpoint address + conn net.PacketConn // packet connection + buf []byte // buffer for read/write operations +} + +// Run packet endpoint: send incoming messages to the handler. +func (ep *PaketEndpoint) Run(ctx context.Context, hdlr chan *TransportMessage) (err error) { + // create listener + var lc net.ListenConfig + if ep.conn, err = lc.ListenPacket(ctx, ep.addr.Network(), ep.addr.String()); err != nil { + return + } + + // run watch dog for termination + go func() { + <-ctx.Done() + ep.conn.Close() + }() + // run go routine to handle messages from clients + go func() { + for { + // read next message from packet + n, _, err := ep.conn.ReadFrom(ep.buf) + if err != nil { + break + } + rdr := bytes.NewBuffer(util.Clone(ep.buf[:n])) + msg, err := ReadMessageDirect(rdr, ep.buf) + if err != nil { + break + } + // check for transport message + if msg.Header().MsgType == message.DUMMY { + // set transient attributes + tm := msg.(*TransportMessage) + tm.endp = ep.id + tm.conn = 0 + // send to handler + go func() { + hdlr <- tm + }() + } + } + // connection ended. + ep.conn.Close() + }() + return +} + +// Send message to address from endpoint +func (ep *PaketEndpoint) Send(ctx context.Context, addr net.Addr, msg *TransportMessage) (err error) { + var a *net.UDPAddr + a, err = net.ResolveUDPAddr(addr.Network(), addr.String()) + var buf []byte + if buf, err = msg.Bytes(); err != nil { + return + } + _, err = ep.conn.WriteTo(buf, a) + return +} + +// Address returms the +func (ep *PaketEndpoint) Address() net.Addr { + if ep.conn != nil { + return ep.conn.LocalAddr() + } + return ep.addr +} + +// CanSendTo returns true if the endpoint can sent to address +func (ep *PaketEndpoint) CanSendTo(addr net.Addr) bool { + return epMode(addr.Network()) == "packet" +} + +// ID returns the endpoint identifier +func (ep *PaketEndpoint) ID() int { + return ep.id +} + +func newPacketEndpoint(addr net.Addr) (ep *PaketEndpoint, err error) { + // check for matching protocol + if epMode(addr.Network()) != "packet" { + err = ErrEndpProtocolMismatch + return + } + // create endpoint + ep = &PaketEndpoint{ + id: util.NextID(), + addr: addr, + buf: make([]byte, 65536), + } + return +} + +//---------------------------------------------------------------------- +// Stream-oriented endpoint +//---------------------------------------------------------------------- + +// StreamEndpoint for stream-oriented network protocols +type StreamEndpoint struct { + id int // endpoint identifier + addr net.Addr // listening address + listener net.Listener // listener instance + conns *util.Map[int, net.Conn] // active connections + buf []byte // read/write buffer +} + +// Run packet endpoint: send incoming messages to the handler. +func (ep *StreamEndpoint) Run(ctx context.Context, hdlr chan *TransportMessage) (err error) { + // create listener + var lc net.ListenConfig + if ep.listener, err = lc.Listen(ctx, ep.addr.Network(), ep.addr.String()); err != nil { + return + } + // run watch dog for termination + go func() { + <-ctx.Done() + ep.listener.Close() + }() + // run go routine to handle messages from clients + go func() { + for { + // get next client connection + conn, err := ep.listener.Accept() + if err != nil { + return + } + session := util.NextID() + ep.conns.Put(session, conn) + go func() { + for { + // read next message from connection + msg, err := ReadMessage(ctx, conn, ep.buf) + if err != nil { + break + } + // check for transport message + if msg.Header().MsgType == message.DUMMY { + // set transient attributes + tm := msg.(*TransportMessage) + tm.endp = ep.id + tm.conn = session + // send to handler + go func() { + hdlr <- tm + }() + } + } + // connection ended. + conn.Close() + ep.conns.Delete(session) + }() + } + }() + return +} + +// Send message to address from endpoint +func (ep *StreamEndpoint) Send(ctx context.Context, addr net.Addr, msg *TransportMessage) error { + return nil +} + +// Address returns the actual listening endpoint address +func (ep *StreamEndpoint) Address() net.Addr { + if ep.listener != nil { + return ep.listener.Addr() + } + return ep.addr +} + +// CanSendTo returns true if the endpoint can sent to address +func (ep *StreamEndpoint) CanSendTo(addr net.Addr) bool { + return epMode(addr.Network()) == "stream" +} + +// ID returns the endpoint identifier +func (ep *StreamEndpoint) ID() int { + return ep.id +} + +func newStreamEndpoint(addr net.Addr) (ep *StreamEndpoint, err error) { + // check for matching protocol + if epMode(addr.Network()) != "stream" { + err = ErrEndpProtocolMismatch + return + } + // create endpoint + ep = &StreamEndpoint{ + id: util.NextID(), + addr: addr, + conns: util.NewMap[int, net.Conn](), + buf: make([]byte, 65536), + } + return +} + +// epMode returns the endpoint mode (packet or stream) for a given network +func epMode(netw string) string { + switch netw { + case "udp", "udp4", "udp6", "r5n+ip+udp": + return "packet" + case "tcp", "unix": + return "stream" + } + return "" +} diff --git a/src/gnunet/transport/reader_writer.go b/src/gnunet/transport/reader_writer.go new file mode 100644 index 0000000..2e5f14a --- /dev/null +++ b/src/gnunet/transport/reader_writer.go @@ -0,0 +1,157 @@ +// 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 . +// +// SPDX-License-Identifier: AGPL3.0-or-later + +package transport + +import ( + "context" + "errors" + "fmt" + "gnunet/message" + "io" + + "github.com/bfix/gospel/data" +) + +// WriteMessageDirect writes directly to io.Writer +func WriteMessageDirect(wrt io.Writer, msg message.Message) error { + dwc := &directWriteCloser{wrt} + return WriteMessage(context.Background(), dwc, msg) +} + +// WriteMessage to io.WriteCloser +func WriteMessage(ctx context.Context, wrt io.WriteCloser, msg message.Message) (err error) { + // convert message to binary data + var buf []byte + if buf, err = data.Marshal(msg); err != nil { + return err + } + // check message header size and packet size + mh, err := message.GetMsgHeader(buf) + if err != nil { + return err + } + if len(buf) != int(mh.MsgSize) { + return errors.New("WriteMessage: message size mismatch") + } + // watch dog for write operation + go func() { + for { + select { + case <-ctx.Done(): + wrt.Close() + } + } + }() + // perform write operation + var n int + if n, err = wrt.Write(buf); err != nil { + return + } + if n != len(buf) { + err = fmt.Errorf("WriteMessage incomplete (%d of %d)", n, len(buf)) + } + return +} + +//---------------------------------------------------------------------- + +// ReadMessageDirect reads directly from io.Reader +func ReadMessageDirect(rdr io.Reader, buf []byte) (msg message.Message, err error) { + drc := &directReadCloser{ + rdr: rdr, + } + return ReadMessage(context.Background(), drc, buf) +} + +// ReadMessage from io.ReadCloser +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() + } + } + }() + // 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 { + 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 + } + var mh *message.Header + if mh, err = message.GetMsgHeader(buf[:4]); err != nil { + return nil, err + } + // get rest of message + if err = get(4, int(mh.MsgSize)-4); err != nil { + return nil, err + } + // handle transport message case + if mh.MsgType == message.DUMMY { + msg = NewTransportMessage(nil, nil) + } else if msg, err = message.NewEmptyMessage(mh.MsgType); err != nil { + return nil, err + } + 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 + } + return msg, nil +} + +//---------------------------------------------------------------------- +// helper for wrapped ReadCloser/WriteCloser (close is nop) +//---------------------------------------------------------------------- + +type directReadCloser struct { + rdr io.Reader +} + +func (drc *directReadCloser) Read(buf []byte) (int, error) { + return drc.rdr.Read(buf) +} + +func (drc *directReadCloser) Close() error { + return nil +} + +type directWriteCloser struct { + wrt io.Writer +} + +func (dwc *directWriteCloser) Write(buf []byte) (int, error) { + return dwc.wrt.Write(buf) +} + +func (dwc *directWriteCloser) Close() error { + return nil +} diff --git a/src/gnunet/transport/session.go b/src/gnunet/transport/session.go deleted file mode 100644 index f5a0787..0000000 --- a/src/gnunet/transport/session.go +++ /dev/null @@ -1,29 +0,0 @@ -// This file is part of gnunet-go, a GNUnet-implementation in Golang. -// Copyright (C) 2019, 2020 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 . -// -// SPDX-License-Identifier: AGPL3.0-or-later - -package transport - -// Session states -const ( - KxStateDown = iota // No handshake yet. - KxStateKeySent // We've sent our session key. - KxStateKeyReceived // We've received the other peers session key. - KxStateUp // Key exchange is done. - KxStateRekeySent // We're rekeying (or had a timeout). - KxPeerDisconnect // Last state of a KX (when it is being terminated). -) diff --git a/src/gnunet/transport/transport.go b/src/gnunet/transport/transport.go new file mode 100644 index 0000000..14def98 --- /dev/null +++ b/src/gnunet/transport/transport.go @@ -0,0 +1,151 @@ +// 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 . +// +// SPDX-License-Identifier: AGPL3.0-or-later + +package transport + +import ( + "bytes" + "context" + "errors" + "gnunet/message" + "gnunet/util" + "net" +) + +// Trnsport layer error codes +var ( + ErrTransNoEndpoint = errors.New("no matching endpoint found") +) + +//====================================================================== +// Network-oriented transport implementation +//====================================================================== + +// TransportMessage 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 { + Hdr *message.Header `` // message header + Peer *util.PeerID `` // remote peer + Payload []byte `size:"*"` // GNUnet message + + // package-local attributes (transient) + msg message.Message + endp int // id of endpoint (incoming message) + conn int // id of connection (optional, incoming message) +} + +func (msg *TransportMessage) Header() *message.Header { + return msg.Hdr +} + +func (msg *TransportMessage) Message() (m message.Message, err error) { + if m = msg.msg; m == nil { + rdr := bytes.NewBuffer(msg.Payload) + m, err = ReadMessageDirect(rdr, nil) + } + return +} + +// Bytes returns the binary representation of a transport message +func (msg *TransportMessage) Bytes() ([]byte, error) { + buf := new(bytes.Buffer) + err := WriteMessageDirect(buf, msg) + return buf.Bytes(), err +} + +// String returns the message in human-readable form +func (msg *TransportMessage) String() string { + return "TransportMessage{...}" +} + +// NewTransportMessage creates a message suitable for transfer +func NewTransportMessage(peer *util.PeerID, payload []byte) (tm *TransportMessage) { + if peer == nil { + peer = util.NewPeerID(nil) + } + msize := 0 + if payload != nil { + msize = len(payload) + } + tm = &TransportMessage{ + Hdr: &message.Header{ + MsgSize: uint16(36 + msize), + MsgType: message.DUMMY, + }, + Peer: peer, + Payload: payload, + } + return +} + +//---------------------------------------------------------------------- + +// 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 + endpoints map[int]Endpoint // list of available endpoints +} + +// NewTransport creates and runs a new transport layer implementation. +func NewTransport(ctx context.Context, ch chan *TransportMessage) (t *Transport) { + // create transport instance + return &Transport{ + incoming: ch, + endpoints: make(map[int]Endpoint), + } +} + +// Send a message over suitable endpoint +func (t *Transport) Send(ctx context.Context, addr net.Addr, msg *TransportMessage) (err error) { + for _, ep := range t.endpoints { + if ep.CanSendTo(addr) { + err = ep.Send(ctx, addr, msg) + break + } + } + return +} + +//---------------------------------------------------------------------- +// Endpoint handling +//---------------------------------------------------------------------- + +// AddEndpoint instantiates and run a new endpoint handler for the +// given address (must map to a network interface). +func (t *Transport) AddEndpoint(ctx context.Context, addr net.Addr) (a net.Addr, err error) { + // register endpoint + var ep Endpoint + if ep, err = NewEndpoint(addr); err != nil { + return + } + t.endpoints[ep.ID()] = ep + ep.Run(ctx, t.incoming) + return ep.Address(), nil +} + +// Endpoints returns a list of listening addresses managed by transport. +func (t *Transport) Endpoints() (list []net.Addr) { + list = make([]net.Addr, 0) + for _, ep := range t.endpoints { + list = append(list, ep.Address()) + } + return +} diff --git a/src/gnunet/util/address.go b/src/gnunet/util/address.go index d272742..106e671 100644 --- a/src/gnunet/util/address.go +++ b/src/gnunet/util/address.go @@ -19,44 +19,81 @@ package util import ( - "encoding/hex" + "bytes" "fmt" "net" + "strings" ) // Address specifies how a peer is reachable on the network. type Address struct { - Transport string // transport protocol - Options uint32 `order:"big"` // address options - Address []byte `size:"*"` // address data (protocol-dependent) + Netw string `` // network protocol + Options uint32 `order:"big"` // address options + Expires AbsoluteTime `` // expiration date for address + Address []byte `size:"*"` // address data (protocol-dependent) } // NewAddress returns a new Address for the given transport and specs func NewAddress(transport string, addr []byte) *Address { - a := &Address{ - Transport: transport, - Options: 0, - Address: make([]byte, len(addr)), + return &Address{ + Netw: transport, + Options: 0, + Address: Clone(addr), + Expires: AbsoluteTimeNever(), } - copy(a.Address, addr) - return a } +func NewAddressWrap(addr net.Addr) *Address { + return &Address{ + Netw: addr.Network(), + Options: 0, + Address: []byte(addr.String()), + Expires: AbsoluteTimeNever(), + } +} + +// ParseAddress translates a GNUnet address string like +// "r5n+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) + if len(p) != 2 { + err = fmt.Errorf("invalid address format: '%s'", s) + return + } + addr = NewAddress(p[0], []byte(strings.Trim(p[1], "/"))) + return +} + +// Equals return true if two addresses match. +func (a *Address) Equals(b *Address) bool { + return a.Netw == b.Netw && + a.Options == b.Options && + 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. func (a *Address) String() string { - return fmt.Sprintf("Address{%s}", AddressString(a.Transport, a.Address)) + return string(a.Address) +} + +// Network returns the protocol specifier. +func (a *Address) Network() string { + return a.Netw } //---------------------------------------------------------------------- // AddressString returns a string representaion of an address. -func AddressString(transport string, addr []byte) string { - if transport == "tcp" || transport == "udp" { - alen := len(addr) - port := uint(addr[alen-2])*256 + uint(addr[alen-1]) - return fmt.Sprintf("%s:%s:%d", transport, net.IP(addr[:alen-2]).String(), port) - } - return fmt.Sprintf("%s:%s", transport, hex.EncodeToString(addr)) +func AddressString(network string, addr []byte) string { + return network + "://" + string(addr) } //---------------------------------------------------------------------- @@ -76,3 +113,71 @@ func NewIPAddress(host []byte, port uint16) *IPAddress { copy(ip.Host, host) return ip } + +//---------------------------------------------------------------------- + +// PeerAddrList is a list of addresses per peer ID. +type PeerAddrList struct { + list *Map[string, []*Address] +} + +// NewPeerAddrList returns a new and empty address list. +func NewPeerAddrList() *PeerAddrList { + return &PeerAddrList{ + list: NewMap[string, []*Address](), + } +} + +// 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) { + // check for expired address. + mode = 0 + if !addr.Expires.Expired() { + // run add operation + a.list.Process(func() error { + list, ok := a.list.Get(id) + if !ok { + list = make([]*Address, 0) + mode = 1 + } else { + for _, a := range list { + if a.Equals(addr) { + return nil + } + } + mode = 2 + } + list = append(list, addr) + a.list.Put(id, list) + return nil + }) + } + return +} + +// Get address for peer +func (a *PeerAddrList) Get(id string, transport string) *Address { + list, ok := a.list.Get(id) + if ok { + for _, addr := range list { + // check for expired address. + if addr.Expires.Expired() { + // skip expired + continue + } + // check for matching protocol + if len(transport) > 0 && transport != addr.Netw { + // skip other transports + continue + } + return addr + } + } + return nil +} + +// Delete a list entry by key. +func (a *PeerAddrList) Delete(id string) { + a.list.Delete(id) +} diff --git a/src/gnunet/util/array.go b/src/gnunet/util/array.go index 254610b..c6d6371 100644 --- a/src/gnunet/util/array.go +++ b/src/gnunet/util/array.go @@ -24,44 +24,68 @@ import ( // Error variables var ( - ErrUtilArrayTooSmall = fmt.Errorf("Array to small") + ErrUtilArrayTooSmall = fmt.Errorf("array to small") ) //---------------------------------------------------------------------- -// Byte array helpers +// generic array helpers //---------------------------------------------------------------------- // Clone creates a new array of same content as the argument. -func Clone(d []byte) []byte { - r := make([]byte, len(d)) +func Clone[T []E, E any](d T) T { + r := make(T, len(d)) copy(r, d) return r } -// Reverse the content of a byte array -func Reverse(b []byte) []byte { +// Equals returns true if two arrays match. +func Equals[T []E, E comparable](a, b T) bool { + if len(a) != len(b) { + return false + } + for i, e := range a { + if e != b[i] { + return false + } + } + return true +} + +// Reverse the content of an array +func Reverse[T []E, E any](b T) T { bl := len(b) - r := make([]byte, bl) + r := make(T, bl) for i := 0; i < bl; i++ { r[bl-i-1] = b[i] } return r } -// IsNull returns true if all bytes in an array are set to 0. -func IsNull(b []byte) bool { +// IsAll returns true if all elements in an array are set to null. +func IsAll[T []E, E comparable](b T, null E) bool { for _, v := range b { - if v != 0 { + if v != null { return false } } return true } -// CopyBlock copies 'in' to 'out' so that 'out' is filled completely. +// Fill an array with a value +func Fill[T []E, E any](b T, val E) { + for i := range b { + b[i] = val + } +} + +//---------------------------------------------------------------------- +// byte array helpers +//---------------------------------------------------------------------- + +// CopyAlignedBlock copies 'in' to 'out' so that 'out' is filled completely. // - If 'in' is larger than 'out', it is left-truncated before copy // - If 'in' is smaller than 'out', it is left-padded with 0 before copy -func CopyBlock(out, in []byte) { +func CopyAlignedBlock(out, in []byte) { count := len(in) size := len(out) from, to := 0, 0 @@ -76,27 +100,10 @@ func CopyBlock(out, in []byte) { copy(out[to:], in[from:]) } -// Fill an array with a value -func Fill(b []byte, val byte) { - for i := 0; i < len(b); i++ { - b[i] = val - } -} - //---------------------------------------------------------------------- // String list helpers //---------------------------------------------------------------------- -// ReverseStringList reverse an array of strings -func ReverseStringList(s []string) []string { - sl := len(s) - r := make([]string, sl) - for i := 0; i < sl; i++ { - r[sl-i-1] = s[i] - } - return r -} - // StringList converts a binary representation of a string list. Each string // is '\0'-terminated. The whole byte array is parsed; if the final string is // not terminated, it is skipped. diff --git a/src/gnunet/util/database.go b/src/gnunet/util/database.go index 5805a8f..a1198fd 100644 --- a/src/gnunet/util/database.go +++ b/src/gnunet/util/database.go @@ -19,6 +19,7 @@ package util import ( + "context" "database/sql" "fmt" "os" @@ -34,7 +35,96 @@ var ( ErrSQLNoDatabase = fmt.Errorf("Database not found") ) -// ConnectSQLDatabase connects to an SQL database (various types and flavors): +//---------------------------------------------------------------------- +// 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 + }) +} + +// 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) @@ -46,27 +136,50 @@ var ( // * '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 ConnectSQLDatabase(spec string) (db *sql.DB, err error) { - // split spec string into segments - specs := strings.Split(spec, ":") - if len(specs) < 2 { - return nil, ErrSQLInvalidDatabaseSpec - } - 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 nil, ErrSQLNoDatabase - } - if fi.IsDir() { - return nil, ErrSQLNoDatabase +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 } - // open the database file - return sql.Open("sqlite3", specs[1]) - case "mysql": - // just connect to the database - return sql.Open("mysql", specs[1]) - } - return nil, ErrSQLInvalidDatabaseSpec + // 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 + }) + return } diff --git a/src/gnunet/util/fs.go b/src/gnunet/util/fs.go index 009ef62..b2a464e 100644 --- a/src/gnunet/util/fs.go +++ b/src/gnunet/util/fs.go @@ -25,7 +25,7 @@ import ( "github.com/bfix/gospel/logger" ) -// EnforceDirExists make sure that the path +// EnforceDirExists make sure that the path is created func EnforceDirExists(path string) error { logger.Printf(logger.DBG, "[util] Checking directory '%s'...\n", path) fi, err := os.Lstat(path) diff --git a/src/gnunet/util/key_value_store.go b/src/gnunet/util/key_value_store.go deleted file mode 100644 index 0658218..0000000 --- a/src/gnunet/util/key_value_store.go +++ /dev/null @@ -1,188 +0,0 @@ -// This file is part of gnunet-go, a GNUnet-implementation in Golang. -// Copyright (C) 2019, 2020 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 . -// -// SPDX-License-Identifier: AGPL3.0-or-later - -package util - -import ( - "context" - "database/sql" - "fmt" - "strconv" - "strings" - - redis "github.com/go-redis/redis/v8" -) - -// Error messages related to the key/value-store implementations -var ( - ErrKVSInvalidSpec = fmt.Errorf("Invalid KVStore specification") - ErrKVSNotAvailable = fmt.Errorf("KVStore not available") -) - -// KeyValueStore interface for implementations that store and retrieve -// key/value pairs. Keys and values are strings. -type KeyValueStore interface { - Put(key string, value string) error // put a key/value pair into store - Get(key string) (string, error) // retrieve a value for a key from store - List() ([]string, error) // get all keys from the store -} - -// OpenKVStore opens a key/value store for further put/get operations. -// The 'spec' option specifies the arguments required to connect to a specific -// persistence mechanism. The arguments in the 'spec' string are separated by -// the '+' character. -// The first argument specifies the type of key/value store to be used; the -// meaning and format of the following arguments depend on this type. -// -// Key/Value Store types defined: -// * 'redis': Use a Redis server for persistance; the specification is -// "redis+addr+[passwd]+db". 'db' must be an integer value. -// * 'mysql': MySQL-compatible database (see 'database.go' for details) -// * 'sqlite3': SQLite3-compatible database (see 'database.go' for details) -func OpenKVStore(spec string) (KeyValueStore, error) { - // check specification string - specs := strings.Split(spec, "+") - if len(specs) < 2 { - return nil, ErrKVSInvalidSpec - } - switch specs[0] { - case "redis": - //-------------------------------------------------------------- - // NoSQL-based persistance - //-------------------------------------------------------------- - if len(specs) < 4 { - return nil, ErrKVSInvalidSpec - } - db, err := strconv.Atoi(specs[3]) - if err != nil { - return nil, ErrKVSInvalidSpec - } - kvs := new(KvsRedis) - kvs.db = db - kvs.client = redis.NewClient(&redis.Options{ - Addr: specs[1], - Password: specs[2], - DB: db, - }) - if kvs.client == nil { - err = ErrKVSNotAvailable - } - return kvs, err - - case "sqlite3", "mysql": - //-------------------------------------------------------------- - // SQL-based persistance - //-------------------------------------------------------------- - kvs := new(KvsSQL) - var err error - - // connect to SQL database - kvs.db, err = ConnectSQLDatabase(spec) - 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, ErrKVSNotAvailable - } - return kvs, nil - } - return nil, ErrKVSInvalidSpec -} - -//====================================================================== -// NoSQL-based key-value-stores -//====================================================================== - -// KvsRedis represents a redis-based key/value store -type KvsRedis struct { - client *redis.Client // client connection - db int // index to database -} - -// Put a key/value pair into the store -func (kvs *KvsRedis) Put(key string, value string) error { - return kvs.client.Set(context.TODO(), key, value, 0).Err() -} - -// Get a value for a given key from store -func (kvs *KvsRedis) Get(key string) (value string, err error) { - return kvs.client.Get(context.TODO(), key).Result() -} - -// List all keys in store -func (kvs *KvsRedis) List() (keys []string, err error) { - var ( - crs uint64 - segm []string - ctx = context.TODO() - ) - for { - segm, crs, err = kvs.client.Scan(ctx, crs, "*", 10).Result() - if err != nil { - return nil, err - } - if crs == 0 { - break - } - keys = append(keys, segm...) - } - return -} - -//====================================================================== -// SQL-based key-value-store -//====================================================================== - -// KvsSQL represents a SQL-based key/value store -type KvsSQL struct { - db *sql.DB -} - -// Put a key/value pair into the store -func (kvs *KvsSQL) Put(key string, value string) error { - _, err := kvs.db.Exec("insert into store(key,value) values(?,?)", key, value) - return err -} - -// Get a value for a given key from store -func (kvs *KvsSQL) Get(key string) (value string, err error) { - row := kvs.db.QueryRow("select value from store where key=?", key) - err = row.Scan(&value) - return -} - -// List all keys in store -func (kvs *KvsSQL) List() (keys []string, err error) { - var ( - rows *sql.Rows - key string - ) - rows, err = kvs.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/util/misc.go b/src/gnunet/util/misc.go index 66744bd..7240757 100644 --- a/src/gnunet/util/misc.go +++ b/src/gnunet/util/misc.go @@ -20,13 +20,18 @@ package util import ( "strings" + "sync" ) -// CounterMap is a metric with single key -type CounterMap map[interface{}]int +//---------------------------------------------------------------------- +// Count occurence of multiple instance at the same time. +//---------------------------------------------------------------------- + +// Counter is a metric with single key +type Counter[T comparable] map[T]int // Add one to themetric for a given key and return current value -func (cm CounterMap) Add(i interface{}) int { +func (cm Counter[T]) Add(i T) int { count, ok := cm[i] if !ok { count = 1 @@ -38,7 +43,7 @@ func (cm CounterMap) Add(i interface{}) int { } // Num returns the metric for a given key -func (cm CounterMap) Num(i interface{}) int { +func (cm Counter[T]) Num(i T) int { count, ok := cm[i] if !ok { count = 0 @@ -46,6 +51,68 @@ func (cm CounterMap) Num(i interface{}) int { return count } +//---------------------------------------------------------------------- +// Thread-safe map implementation +//---------------------------------------------------------------------- + +// 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, + } +} + +// Process a function in the locked map context. Calls +// to other map functions in 'f' will use additional locks. +func (m *Map[K, V]) Process(f func() error) error { + m.mtx.Lock() + defer m.mtx.Unlock() + m.inProcess = true + err := f() + m.inProcess = false + return err +} + +// Put value into map under given key. +func (m *Map[K, V]) Put(key K, value V) { + if !m.inProcess { + m.mtx.Lock() + defer m.mtx.Unlock() + } + m.list[key] = value +} + +// Get value with iven key from map. +func (m *Map[K, V]) Get(key K) (value V, ok bool) { + if !m.inProcess { + m.mtx.RLock() + defer m.mtx.RUnlock() + } + value, ok = m.list[key] + return +} + +// Delete key/value pair from map. +func (m *Map[K, V]) Delete(key K) { + if !m.inProcess { + m.mtx.Lock() + defer m.mtx.Unlock() + } + delete(m.list, key) +} + +//---------------------------------------------------------------------- +// additional helpers +//---------------------------------------------------------------------- + // StripPathRight returns a dot-separated path without // its last (right-most) element. func StripPathRight(s string) string { diff --git a/src/gnunet/util/peer_id.go b/src/gnunet/util/peer_id.go index f4c14bd..a8202a1 100644 --- a/src/gnunet/util/peer_id.go +++ b/src/gnunet/util/peer_id.go @@ -1,5 +1,5 @@ // This file is part of gnunet-go, a GNUnet-implementation in Golang. -// Copyright (C) 2019, 2020 Bernd Fix >Y< +// 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 @@ -18,6 +18,8 @@ package util +import "bytes" + // PeerID is the 32-byte binary representation od a Ed25519 key type PeerID struct { Key []byte `size:"32"` @@ -33,7 +35,7 @@ func NewPeerID(data []byte) *PeerID { data = data[:32] } else if size < 32 { buf := make([]byte, 32) - CopyBlock(buf, data) + CopyAlignedBlock(buf, data) data = buf } } @@ -42,6 +44,11 @@ func NewPeerID(data []byte) *PeerID { } } +// 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) diff --git a/src/gnunet/util/time.go b/src/gnunet/util/time.go index e70635c..70b91e1 100644 --- a/src/gnunet/util/time.go +++ b/src/gnunet/util/time.go @@ -43,6 +43,13 @@ 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), + } +} + // AbsoluteTimeNow returns the current point in time. func AbsoluteTimeNow() AbsoluteTime { return NewAbsoluteTime(time.Now()) @@ -53,6 +60,11 @@ func AbsoluteTimeNever() AbsoluteTime { return AbsoluteTime{math.MaxUint64} } +// Epoch returns the seconds since Unix epoch. +func (t AbsoluteTime) Epoch() uint64 { + return t.Val / 1000000 +} + // String returns a human-readable notation of an absolute time. func (t AbsoluteTime) String() string { if t.Val == math.MaxUint64 { @@ -133,3 +145,8 @@ func (t RelativeTime) String() string { } return time.Duration(t.Val * 1000).String() } + +// Add two durations +func (t RelativeTime) Add(t2 RelativeTime) { + t.Val += t2.Val +} -- cgit v1.2.3