From 6ed1dfac9e0ac7a24114198ae8a364388cb55528 Mon Sep 17 00:00:00 2001 From: Bernd Fix Date: Sat, 22 Oct 2022 16:57:16 +0200 Subject: Initial revision of Zonemaster implementation. --- src/gnunet/build.sh | 7 +- src/gnunet/cmd/gnunet-service-gns-go/main.go | 13 +- src/gnunet/cmd/zonemaster-go/main.go | 144 +++++ src/gnunet/config/config.go | 56 +- src/gnunet/config/gnunet-config.json | 28 +- src/gnunet/crypto/gns.go | 126 ++-- src/gnunet/crypto/gns_edkey.go | 28 +- src/gnunet/crypto/gns_edkey_test.go | 56 ++ src/gnunet/crypto/gns_pkey.go | 15 +- src/gnunet/crypto/gns_pkey_test.go | 48 ++ src/gnunet/crypto/gns_test.go | 35 +- src/gnunet/enums/gns.go | 17 +- src/gnunet/enums/gns_type.go | 2 +- src/gnunet/enums/gnunet-gns.tpl | 2 +- src/gnunet/go.mod | 4 +- src/gnunet/go.sum | 4 +- src/gnunet/message/factory.go | 22 +- src/gnunet/message/msg_dht_p2p.go | 24 +- src/gnunet/message/msg_gns.go | 81 +-- src/gnunet/message/msg_gns_test.go | 353 ------------ src/gnunet/message/msg_namecache.go | 97 ++-- src/gnunet/message/msg_namestore.go | 199 +++++++ src/gnunet/service/client.go | 13 +- src/gnunet/service/dht/blocks/gns.go | 108 +++- src/gnunet/service/dht/blocks/gns_test.go | 444 +++++++++++++++ src/gnunet/service/dht/module.go | 17 +- src/gnunet/service/gns/block_handler.go | 134 +++-- src/gnunet/service/gns/box.go | 169 ------ src/gnunet/service/gns/dns.go | 16 +- src/gnunet/service/gns/module.go | 45 +- src/gnunet/service/gns/rr/coexist.go | 146 +++++ src/gnunet/service/gns/rr/dns.go | 119 ++++ src/gnunet/service/gns/rr/gns.go | 199 +++++++ src/gnunet/service/gns/rr/gns_box.go | 375 ++++++++++++ src/gnunet/service/gns/service.go | 12 +- src/gnunet/service/revocation/pow_test.go | 2 +- src/gnunet/service/store/store_dht_meta.go | 24 +- src/gnunet/service/store/store_zonemaster.go | 548 ++++++++++++++++++ src/gnunet/service/store/store_zonemaster.sql | 47 ++ src/gnunet/service/store/store_zonemaster_test.go | 105 ++++ src/gnunet/service/zonemaster/gui.go | 666 ++++++++++++++++++++++ src/gnunet/service/zonemaster/gui.htpl | 133 +++++ src/gnunet/service/zonemaster/gui_css.htpl | 253 ++++++++ src/gnunet/service/zonemaster/gui_debug.htpl | 14 + src/gnunet/service/zonemaster/gui_edit.htpl | 130 +++++ src/gnunet/service/zonemaster/gui_new.htpl | 114 ++++ src/gnunet/service/zonemaster/gui_rr.htpl | 420 ++++++++++++++ src/gnunet/service/zonemaster/module.go | 84 +++ src/gnunet/service/zonemaster/records.go | 348 +++++++++++ src/gnunet/service/zonemaster/rpc.go | 24 + src/gnunet/service/zonemaster/service.go | 150 +++++ src/gnunet/service/zonemaster/zonemaster.go | 179 ++++++ src/gnunet/util/array.go | 20 +- src/gnunet/util/misc.go | 16 + src/gnunet/util/time.go | 5 + 55 files changed, 5558 insertions(+), 882 deletions(-) create mode 100644 src/gnunet/cmd/zonemaster-go/main.go create mode 100644 src/gnunet/crypto/gns_edkey_test.go create mode 100644 src/gnunet/crypto/gns_pkey_test.go delete mode 100644 src/gnunet/message/msg_gns_test.go create mode 100644 src/gnunet/message/msg_namestore.go create mode 100644 src/gnunet/service/dht/blocks/gns_test.go delete mode 100644 src/gnunet/service/gns/box.go create mode 100644 src/gnunet/service/gns/rr/coexist.go create mode 100644 src/gnunet/service/gns/rr/dns.go create mode 100644 src/gnunet/service/gns/rr/gns.go create mode 100644 src/gnunet/service/gns/rr/gns_box.go create mode 100644 src/gnunet/service/store/store_zonemaster.go create mode 100644 src/gnunet/service/store/store_zonemaster.sql create mode 100644 src/gnunet/service/store/store_zonemaster_test.go create mode 100644 src/gnunet/service/zonemaster/gui.go create mode 100644 src/gnunet/service/zonemaster/gui.htpl create mode 100644 src/gnunet/service/zonemaster/gui_css.htpl create mode 100644 src/gnunet/service/zonemaster/gui_debug.htpl create mode 100644 src/gnunet/service/zonemaster/gui_edit.htpl create mode 100644 src/gnunet/service/zonemaster/gui_new.htpl create mode 100644 src/gnunet/service/zonemaster/gui_rr.htpl create mode 100644 src/gnunet/service/zonemaster/module.go create mode 100644 src/gnunet/service/zonemaster/records.go create mode 100644 src/gnunet/service/zonemaster/rpc.go create mode 100644 src/gnunet/service/zonemaster/service.go create mode 100644 src/gnunet/service/zonemaster/zonemaster.go (limited to 'src') diff --git a/src/gnunet/build.sh b/src/gnunet/build.sh index 20f6933..03899f7 100755 --- a/src/gnunet/build.sh +++ b/src/gnunet/build.sh @@ -1,4 +1,7 @@ #!/bin/bash -[ "$1" = "withgen" ] && go generate ./... -go install -v -gcflags "-N -l" ./... +if [ "$1" = "withgen" ]; then + go generate ./... + shift +fi +go install $* -gcflags "-N -l" ./... diff --git a/src/gnunet/cmd/gnunet-service-gns-go/main.go b/src/gnunet/cmd/gnunet-service-gns-go/main.go index ebf2151..2a4cae8 100644 --- a/src/gnunet/cmd/gnunet-service-gns-go/main.go +++ b/src/gnunet/cmd/gnunet-service-gns-go/main.go @@ -28,7 +28,6 @@ import ( "time" "gnunet/config" - "gnunet/core" "gnunet/service" "gnunet/service/gns" @@ -80,17 +79,9 @@ func main() { params = config.Cfg.GNS.Service.Params } - // instantiate core service - ctx, cancel := context.WithCancel(context.Background()) - var c *core.Core - if c, err = core.NewCore(ctx, config.Cfg.Local); err != nil { - logger.Printf(logger.ERROR, "[gns] core failed: %s\n", err.Error()) - return - } - defer c.Shutdown() - // start a new GNS service - gns := gns.NewService(ctx, c) + ctx, cancel := context.WithCancel(context.Background()) + gns := gns.NewService(ctx, nil) srv := service.NewSocketHandler("gns", gns) if err = srv.Start(ctx, socket, params); err != nil { logger.Printf(logger.ERROR, "[gns] Error: '%s'", err.Error()) diff --git a/src/gnunet/cmd/zonemaster-go/main.go b/src/gnunet/cmd/zonemaster-go/main.go new file mode 100644 index 0000000..1e4b985 --- /dev/null +++ b/src/gnunet/cmd/zonemaster-go/main.go @@ -0,0 +1,144 @@ +// 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/service" + "gnunet/service/zonemaster" + + "github.com/bfix/gospel/logger" +) + +func main() { + defer func() { + logger.Println(logger.INFO, "[zonemaster] Bye.") + // flush last messages + logger.Flush() + }() + // intro + logger.SetLogLevel(logger.DBG) + logger.Println(logger.INFO, "[zonemaster] Starting service...") + + var ( + cfgFile string + gui string + err error + logLevel int + rpcEndp string + ) + // handle command line arguments + flag.StringVar(&cfgFile, "c", "gnunet-config.json", "GNUnet configuration file") + flag.StringVar(&gui, "g", "", "GUI listen address") + flag.IntVar(&logLevel, "L", logger.INFO, "zonemaster 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, "[zonemaster] Invalid configuration file: %s\n", err.Error()) + return + } + + // apply configuration + if config.Cfg.Logging.Level > 0 { + logLevel = config.Cfg.Logging.Level + } + logger.SetLogLevel(logLevel) + if len(gui) > 0 { + config.Cfg.ZoneMaster.GUI = gui + } + + // start a new namestore service under zonemaster umbrella + ctx, cancel := context.WithCancel(context.Background()) + srv, ok := zonemaster.NewService(ctx, nil).(*zonemaster.Service) + if !ok { + logger.Println(logger.ERROR, "[zonemaster] Failed to create service") + return + } + // start UDS listener if service is specified + if config.Cfg.ZoneMaster.Service != nil { + sockHdlr := service.NewSocketHandler("zonemaster", srv) + if err = sockHdlr.Start(ctx, config.Cfg.ZoneMaster.Service.Socket, config.Cfg.ZoneMaster.Service.Params); err != nil { + logger.Printf(logger.ERROR, "[zonemaster] Error: '%s'", err.Error()) + return + } + } + + // start a new ZONEMASTER (background service with HTTPS backend) + zm := zonemaster.NewZoneMaster(config.Cfg, srv) + go zm.Run(ctx) + + // handle command-line arguments for RPC + if len(rpcEndp) > 0 { + parts := strings.Split(rpcEndp, ":") + if parts[0] != "tcp" { + logger.Println(logger.ERROR, "[zonemaster] RPC must have a TCP/IP endpoint") + return + } + config.Cfg.RPC.Endpoint = parts[1] + } + // start JSON-RPC server on request + if ep := config.Cfg.RPC.Endpoint; len(ep) > 0 { + var rpc *service.JRPCServer + if rpc, err = service.RunRPCServer(ctx, ep); err != nil { + logger.Printf(logger.ERROR, "[zonemaster] RPC failed to start: %s", err.Error()) + return + } + srv.InitRPC(rpc) + } + // 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, "[zonemaster] Terminating service (on signal '%s')\n", sig) + break loop + case syscall.SIGHUP: + logger.Println(logger.INFO, "[zonemaster] SIGHUP") + case syscall.SIGURG: + // TODO: https://github.com/golang/go/issues/37942 + default: + logger.Println(logger.INFO, "[zonemaster] Unhandled signal: "+sig.String()) + } + // handle heart beat + case now := <-tick.C: + logger.Println(logger.INFO, "[zonemaster] Heart beat at "+now.String()) + } + } + // terminating service + cancel() +} diff --git a/src/gnunet/config/config.go b/src/gnunet/config/config.go index 526b7b8..05f8cc9 100644 --- a/src/gnunet/config/config.go +++ b/src/gnunet/config/config.go @@ -95,6 +95,14 @@ type GNSConfig struct { MaxDepth int `json:"maxDepth"` // maximum recursion depth in resolution } +// ZoneMasterConfig contains parameters for the GNS ZoneMaster process +type ZoneMasterConfig struct { + Service *ServiceConfig `json:"service"` // socket for NameStore service + Period int `json:"period"` // cycle period + Storage util.ParameterSet `json:"storage"` // persistence mechanism for zone data + GUI string `json:"gui"` // listen address for HTTP GUI +} + //---------------------------------------------------------------------- // DHT configuration //---------------------------------------------------------------------- @@ -159,6 +167,7 @@ type Config struct { DHT *DHTConfig `json:"dht"` GNS *GNSConfig `json:"gns"` Namecache *NamecacheConfig `json:"namecache"` + ZoneMaster *ZoneMasterConfig `json:"zonemaster"` Revocation *RevocationConfig `json:"revocation"` Logging *LoggingConfig `json:"logging"` } @@ -200,14 +209,19 @@ var ( // substString is a helper function to substitute environment variables // with actual values. func substString(s string, env map[string]string) string { - matches := rx.FindAllStringSubmatch(s, -1) - for _, m := range matches { - if len(m[1]) != 0 { - subst, ok := env[m[1]] - if !ok { - continue + changed := true + for changed { + changed = false + matches := rx.FindAllStringSubmatch(s, -1) + for _, m := range matches { + if len(m[1]) != 0 { + subst, ok := env[m[1]] + if !ok { + continue + } + s = strings.Replace(s, "${"+m[1]+"}", subst, -1) + changed = true } - s = strings.Replace(s, "${"+m[1]+"}", subst, -1) } } return s @@ -225,14 +239,26 @@ func applySubstitutions(x interface{}, env map[string]string) { case reflect.String: // check for substitution if s, ok := fld.Interface().(string); ok { - for { - s1 := substString(s, env) - if s1 == s { - break + sOut := substString(s, env) + if sOut != s { + logger.Printf(logger.DBG, "[config] %s --> %s\n", s, sOut) + fld.SetString(sOut) + } + } + + case reflect.Map: + // substitute values + if s, ok := fld.Interface().(util.ParameterSet); ok { + for k, v := range s { + v1, ok := v.(string) + if !ok { + continue + } + sOut := substString(v1, env) + if sOut != v1 { + logger.Printf(logger.DBG, "[config] %s --> %s\n", v1, sOut) + s[k] = sOut } - logger.Printf(logger.DBG, "[config] %s --> %s\n", s, s1) - fld.SetString(s1) - s = s1 } } @@ -245,8 +271,6 @@ func applySubstitutions(x interface{}, env map[string]string) { e := fld.Elem() if e.IsValid() { process(fld.Elem()) - } else { - logger.Printf(logger.ERROR, "[config] 'nil' pointer encountered") } } } diff --git a/src/gnunet/config/gnunet-config.json b/src/gnunet/config/gnunet-config.json index f6823d7..5e9ec99 100644 --- a/src/gnunet/config/gnunet-config.json +++ b/src/gnunet/config/gnunet-config.json @@ -21,11 +21,13 @@ }, "environ": { "TMP": "/tmp", - "RT_SYS": "${TMP}/gnunet-system-runtime" + "RT_SYS": "${TMP}/gnunet-system-runtime", + "RT_USER": "${TMP}/gnunet-user-runtime", + "VAR_LIB": "/var/lib/gnunet" }, "dht": { "service": { - "socket": "${RT_SYS}/gnunet-service-dht.sock", + "socket": "${RT_SYS}/gnunet-service-dht-go.sock", "params": { "perm": "0770" } @@ -33,7 +35,7 @@ "storage": { "mode": "file", "cache": false, - "path": "/var/lib/gnunet/dht/store", + "path": "${VAR_LIB}/dht/store", "maxGB": 10 }, "routing": { @@ -54,7 +56,7 @@ }, "namecache": { "service": { - "socket": "${RT_SYS}/gnunet-service-namecache.sock", + "socket": "${RT_SYS}/gnunet-service-namecache-go.sock", "params": { "perm": "0770" } @@ -62,7 +64,7 @@ "storage": { "mode": "file", "cache": true, - "path": "/var/lib/gnunet/namecache", + "path": "${VAR_LIB}/namecache", "num": 1000, "expire": 43200 } @@ -81,11 +83,25 @@ "id": 15 } }, + "zonemaster": { + "period": 300, + "storage": { + "mode": "sqlite3", + "file": "${VAR_LIB}/gns/zonemaster.sqlite3" + }, + "gui": "127.0.0.1:8100", + "service": { + "socket": "${RT_USER}/gnunet-service-namestore-go.sock", + "params": { + "perm": "0770" + } + } + }, "rpc": { "endpoint": "tcp:127.0.0.1:80" }, "logging": { "level": 4, - "file": "/tmp/gnunet-go/run.log" + "file": "${TMP}/gnunet-go/run.log" } } \ No newline at end of file diff --git a/src/gnunet/crypto/gns.go b/src/gnunet/crypto/gns.go index 39eb8c5..301acb1 100644 --- a/src/gnunet/crypto/gns.go +++ b/src/gnunet/crypto/gns.go @@ -20,6 +20,7 @@ package crypto import ( "bytes" + "crypto/rand" "crypto/sha256" "crypto/sha512" "encoding/binary" @@ -119,6 +120,9 @@ type ZoneKeyImpl interface { // Verify a signature for binary data Verify(data []byte, sig *ZoneSignature) (bool, error) + + // ID returns the GNUnet identifier for a public zone key + ID() string } // ZonePrivateImpl defines the methods for a private zone key. @@ -134,6 +138,9 @@ type ZonePrivateImpl interface { // Public returns the associated public key Public() ZoneKeyImpl + + // ID returns the GNUnet identifier for a private zone key + ID() string } // ZoneSigImpl defines the methods for a signature object. @@ -147,8 +154,8 @@ type ZoneSigImpl interface { //nolint:stylecheck // allow non-camel-case in constants var ( - ZONE_PKEY = uint32(enums.GNS_TYPE_PKEY) - ZONE_EDKEY = uint32(enums.GNS_TYPE_EDKEY) + ZONE_EDKEY = enums.GNS_TYPE_EDKEY + ZONE_PKEY = enums.GNS_TYPE_PKEY ) var ( @@ -176,7 +183,7 @@ type ZoneImplementation struct { // keep a mapping of available implementations var ( - zoneImpl = make(map[uint32]*ZoneImplementation) + zoneImpl = make(map[enums.GNSType]*ZoneImplementation) ) // Error codes @@ -187,7 +194,7 @@ var ( // GetImplementation return the factory for a given zone type. // If zje zone type is unregistered, nil is returned. -func GetImplementation(ztype uint32) *ZoneImplementation { +func GetImplementation(ztype enums.GNSType) *ZoneImplementation { if impl, ok := zoneImpl[ztype]; ok { return impl } @@ -209,13 +216,22 @@ type ZonePrivate struct { impl ZonePrivateImpl // reference to implementation } -// NewZonePrivate returns a new initialized ZonePrivate instance -func NewZonePrivate(ztype uint32, d []byte) (zp *ZonePrivate, err error) { +// NewZonePrivate returns a new initialized ZonePrivate instance. If no data is +// provided, a new random key is created +func NewZonePrivate(ztype enums.GNSType, d []byte) (zp *ZonePrivate, err error) { // get factory for given zone type impl, ok := zoneImpl[ztype] if !ok { return nil, ErrNoImplementation } + // init data available? + if d == nil { + // no: create random seed + d = make([]byte, impl.PrivateSize) + if _, err = rand.Read(d); err != nil { + return + } + } // assemble private zone key zp = &ZonePrivate{ ZoneKey{ @@ -246,11 +262,9 @@ func (zp *ZonePrivate) KeySize() uint { // Derive key (key blinding) func (zp *ZonePrivate) Derive(label, context string) (dzp *ZonePrivate, h *math.Int, err error) { - // get factory for given zone type - impl := zoneImpl[zp.Type] - // calculate derived key - h = deriveH(zp.impl.Bytes(), label, context) + key := zp.Public().Bytes() + h = deriveH(key, label, context) var derived ZonePrivateImpl if derived, h, err = zp.impl.Derive(h); err != nil { return @@ -264,9 +278,8 @@ func (zp *ZonePrivate) Derive(label, context string) (dzp *ZonePrivate, h *math. }, derived, } - zp.ZoneKey.KeyData = derived.Public().Bytes() - zp.ZoneKey.impl = impl.NewPublic() - err = zp.ZoneKey.impl.Init(zp.ZoneKey.KeyData) + dzp.ZoneKey.KeyData = derived.Public().Bytes() + err = dzp.Init() return } @@ -280,33 +293,45 @@ func (zp *ZonePrivate) Public() *ZoneKey { return &zp.ZoneKey } +// ID returns the human-readable zone private key. +func (zp *ZonePrivate) ID() string { + return zp.impl.ID() +} + //---------------------------------------------------------------------- // Zone key (public) //---------------------------------------------------------------------- // ZoneKey represents the possible types of zone keys (PKEY, EDKEY,...) type ZoneKey struct { - Type uint32 `json:"type" order:"big"` - KeyData []byte `json:"key" size:"(KeySize)"` + Type enums.GNSType `json:"type" order:"big"` + KeyData []byte `json:"key" size:"(KeySize)"` impl ZoneKeyImpl // reference to implementation } +// Init a zone key where only the attributes have been read/deserialized. +func (zk *ZoneKey) Init() (err error) { + if zk.impl == nil { + // initialize implementation + impl, ok := zoneImpl[zk.Type] + if !ok { + err = ErrUnknownZoneType + return + } + zk.impl = impl.NewPublic() + err = zk.impl.Init(zk.KeyData) + } + return +} + // NewZoneKey returns a new initialized ZoneKey instance func NewZoneKey(d []byte) (zk *ZoneKey, err error) { // read zone key from data zk = new(ZoneKey) - if err = data.Unmarshal(zk, d); err != nil { - return + if err = data.Unmarshal(zk, d); err == nil { + err = zk.Init() } - // initialize implementation - impl, ok := zoneImpl[zk.Type] - if !ok { - err = ErrUnknownZoneType - return - } - zk.impl = impl.NewPublic() - err = zk.impl.Init(zk.KeyData) return } @@ -321,7 +346,8 @@ func (zk *ZoneKey) KeySize() uint { // Derive key (key blinding) func (zk *ZoneKey) Derive(label, context string) (dzk *ZoneKey, h *math.Int, err error) { - h = deriveH(zk.KeyData, label, context) + key := zk.Bytes() + h = deriveH(key, label, context) var derived ZoneKeyImpl if derived, h, err = zk.impl.Derive(h); err != nil { return @@ -359,15 +385,7 @@ func (zk *ZoneKey) Verify(data []byte, zs *ZoneSignature) (ok bool, err error) { // ID returns the human-readable zone identifier. func (zk *ZoneKey) ID() string { - buf := new(bytes.Buffer) - err := binary.Write(buf, binary.BigEndian, zk.Type) - if err == nil { - _, err = buf.Write(zk.KeyData) - } - if err != nil { - logger.Printf(logger.ERROR, "[ZoneKey.ID] failed: %s", err.Error()) - } - return util.EncodeBinaryToString(buf.Bytes()) + return zk.impl.ID() } // Bytes returns all bytes of a zone key @@ -402,31 +420,33 @@ type ZoneSignature struct { impl ZoneSigImpl // reference to implementation } -// NewZoneSignature returns a new initialized ZoneSignature instance -func NewZoneSignature(d []byte) (sig *ZoneSignature, err error) { - // read signature - sig = new(ZoneSignature) - if err = data.Unmarshal(sig, d); err != nil { - return - } +func (zs *ZoneSignature) Init() (err error) { // initialize implementations - impl, ok := zoneImpl[sig.Type] + impl, ok := zoneImpl[zs.Type] if !ok { err = ErrUnknownZoneType return } // set signature implementation - zs := impl.NewSignature() - if err = zs.Init(sig.Signature); err != nil { + sig := impl.NewSignature() + if err = sig.Init(zs.Signature); err != nil { return } - sig.impl = zs + zs.impl = sig // set public key implementation zk := impl.NewPublic() - if err = zk.Init(sig.KeyData); err != nil { - return + err = zk.Init(zs.KeyData) + zs.ZoneKey.impl = zk + return +} + +// NewZoneSignature returns a new initialized ZoneSignature instance +func NewZoneSignature(d []byte) (sig *ZoneSignature, err error) { + // read signature + sig = new(ZoneSignature) + if err = data.Unmarshal(sig, d); err == nil { + err = sig.Init() } - sig.ZoneKey.impl = zk return } @@ -465,3 +485,11 @@ func deriveH(key []byte, label, context string) *math.Int { } return math.NewIntFromBytes(b) } + +// convert (type|data) to GNUnet identifier +func asID(t enums.GNSType, data []byte) string { + buf := new(bytes.Buffer) + _ = binary.Write(buf, binary.BigEndian, t) + _, _ = buf.Write(data) + return util.EncodeBinaryToString(buf.Bytes()) +} diff --git a/src/gnunet/crypto/gns_edkey.go b/src/gnunet/crypto/gns_edkey.go index 7d4323a..08dbc55 100644 --- a/src/gnunet/crypto/gns_edkey.go +++ b/src/gnunet/crypto/gns_edkey.go @@ -22,6 +22,7 @@ import ( "crypto/sha256" "crypto/sha512" "errors" + "gnunet/enums" "gnunet/util" "github.com/bfix/gospel/crypto/ed25519" @@ -57,7 +58,7 @@ func init() { // EDKEYPublicImpl implements the public key scheme. type EDKEYPublicImpl struct { - ztype uint32 + ztype enums.GNSType pub *ed25519.PublicKey } @@ -159,6 +160,11 @@ func (pk *EDKEYPublicImpl) BlockKey(label string, expire util.AbsoluteTime) (ske return } +// ID returns the GNUnet identifier for a public zone key +func (pk *EDKEYPublicImpl) ID() string { + return asID(enums.GNS_TYPE_EDKEY, pk.pub.Bytes()) +} + //---------------------------------------------------------------------- // Private key //---------------------------------------------------------------------- @@ -167,12 +173,14 @@ func (pk *EDKEYPublicImpl) BlockKey(label string, expire util.AbsoluteTime) (ske type EDKEYPrivateImpl struct { EDKEYPublicImpl - prv *ed25519.PrivateKey + seed []byte // seed used to generate key + prv *ed25519.PrivateKey // private key } // Init instance from binary data. The data represents a big integer // (in big-endian notation) for the private scalar d. func (pk *EDKEYPrivateImpl) Init(data []byte) error { + pk.seed = util.Clone(data) pk.prv = ed25519.NewPrivateKeyFromSeed(data) pk.ztype = ZONE_EDKEY pk.pub = pk.prv.Public() @@ -194,14 +202,19 @@ func (pk *EDKEYPrivateImpl) Public() ZoneKeyImpl { // (key blinding). Returns the derived key and the blinding value. func (pk *EDKEYPrivateImpl) Derive(h *math.Int) (dPk ZonePrivateImpl, hOut *math.Int, err error) { // limit to allowed value range - hOut = h.Mod(ed25519.GetCurve().N) + hOut = h.SetBit(255, 0) + // derive private key derived := pk.prv.Mult(hOut) + // derive nonce + md := sha256.Sum256(append(pk.prv.Nonce, h.Bytes()...)) + derived.Nonce = md[:] + // assemble EDKEY private key implementation dPk = &EDKEYPrivateImpl{ - EDKEYPublicImpl{ + EDKEYPublicImpl: EDKEYPublicImpl{ pk.ztype, derived.Public(), }, - derived, + prv: derived, } return } @@ -228,6 +241,11 @@ func (pk *EDKEYPrivateImpl) Sign(data []byte) (sig *ZoneSignature, err error) { return } +// ID returns the GNUnet identifier for a private zone key +func (pk *EDKEYPrivateImpl) ID() string { + return asID(enums.GNS_TYPE_EDKEY, pk.seed) +} + //---------------------------------------------------------------------- // Signature //---------------------------------------------------------------------- diff --git a/src/gnunet/crypto/gns_edkey_test.go b/src/gnunet/crypto/gns_edkey_test.go new file mode 100644 index 0000000..aa9728f --- /dev/null +++ b/src/gnunet/crypto/gns_edkey_test.go @@ -0,0 +1,56 @@ +// 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 crypto + +import ( + "bytes" + "gnunet/enums" + "testing" +) + +func TestEdKeyCreate(t *testing.T) { + // create private key + zp, err := NewZonePrivate(enums.GNS_TYPE_EDKEY, nil) + if err != nil { + t.Fatal(err) + } + t.Log(zp.ID()) +} + +func TestDeriveEDKEY(t *testing.T) { + // create new key pair + zp, err := NewZonePrivate(enums.GNS_TYPE_EDKEY, nil) + if err != nil { + t.Fatal(err) + } + zk := zp.Public() + + // derive keys + dzp, _, err := zp.Derive("@", "gns") + if err != nil { + t.Fatal(err) + } + dzk, _, err := zk.Derive("@", "gns") + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(dzp.Public().Bytes(), dzk.Bytes()) { + t.Fatal("derive mismatch") + } +} diff --git a/src/gnunet/crypto/gns_pkey.go b/src/gnunet/crypto/gns_pkey.go index 13925d4..8184483 100644 --- a/src/gnunet/crypto/gns_pkey.go +++ b/src/gnunet/crypto/gns_pkey.go @@ -23,6 +23,7 @@ import ( "crypto/cipher" "crypto/sha256" "crypto/sha512" + "gnunet/enums" "gnunet/util" "github.com/bfix/gospel/crypto/ed25519" @@ -57,7 +58,7 @@ func init() { // PKEYPublicImpl implements the public key scheme. type PKEYPublicImpl struct { - ztype uint32 + ztype enums.GNSType pub *ed25519.PublicKey } @@ -155,6 +156,11 @@ func (pk *PKEYPublicImpl) cipher(encrypt bool, data []byte, label string, expire return } +// ID returns the GNUnet identifier for a public zone key +func (pk *PKEYPublicImpl) ID() string { + return asID(enums.GNS_TYPE_PKEY, pk.pub.Bytes()) +} + //---------------------------------------------------------------------- // Private key //---------------------------------------------------------------------- @@ -171,7 +177,7 @@ type PKEYPrivateImpl struct { func (pk *PKEYPrivateImpl) Init(data []byte) error { d := math.NewIntFromBytes(data) pk.prv = ed25519.NewPrivateKeyFromD(d) - pk.ztype = ZONE_PKEY + pk.ztype = enums.GNS_TYPE_PKEY pk.pub = pk.prv.Public() return nil } @@ -225,6 +231,11 @@ func (pk *PKEYPrivateImpl) Sign(data []byte) (sig *ZoneSignature, err error) { return } +// ID returns the GNUnet identifier for a private zone key +func (pk *PKEYPrivateImpl) ID() string { + return asID(enums.GNS_TYPE_PKEY, pk.prv.D.Bytes()) +} + //---------------------------------------------------------------------- // Signature //---------------------------------------------------------------------- diff --git a/src/gnunet/crypto/gns_pkey_test.go b/src/gnunet/crypto/gns_pkey_test.go new file mode 100644 index 0000000..0982227 --- /dev/null +++ b/src/gnunet/crypto/gns_pkey_test.go @@ -0,0 +1,48 @@ +// 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 crypto + +import ( + "bytes" + "gnunet/enums" + "testing" +) + +func TestDerivePKEY(t *testing.T) { + // create new key pair + zp, err := NewZonePrivate(enums.GNS_TYPE_PKEY, nil) + if err != nil { + t.Fatal(err) + } + zk := zp.Public() + + // derive keys + dzp, _, err := zp.Derive("@", "gns") + if err != nil { + t.Fatal(err) + } + dzk, _, err := zk.Derive("@", "gns") + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(dzp.Public().Bytes(), dzk.Bytes()) { + t.Fatal("derive mismatch") + } +} diff --git a/src/gnunet/crypto/gns_test.go b/src/gnunet/crypto/gns_test.go index 12ab603..2cccb4d 100644 --- a/src/gnunet/crypto/gns_test.go +++ b/src/gnunet/crypto/gns_test.go @@ -23,6 +23,7 @@ import ( "crypto/sha256" "crypto/sha512" "encoding/hex" + "gnunet/enums" "gnunet/util" "testing" "time" @@ -222,34 +223,34 @@ func TestDeriveH(t *testing.T) { CONTEXT = "gns" H = []byte{ - 0x07, 0x1e, 0xfc, 0xa7, 0xdb, 0x28, 0x50, 0xbd, - 0x6f, 0x35, 0x4e, 0xbf, 0xe3, 0x8c, 0x5b, 0xbf, - 0xd6, 0xba, 0x2f, 0x80, 0x5c, 0xd8, 0xd3, 0xb5, - 0x4e, 0xdd, 0x7f, 0x3d, 0xd0, 0x73, 0x0d, 0x1a, + 0x06, 0x5b, 0xb7, 0x42, 0x12, 0xa1, 0xae, 0xc3, + 0x59, 0x68, 0xdd, 0xdb, 0xca, 0xa3, 0x48, 0xfc, + 0xb0, 0xcd, 0x89, 0xd4, 0xcf, 0x9a, 0xe0, 0xfe, + 0xd1, 0xf9, 0xab, 0x6b, 0xd4, 0x28, 0xf4, 0x95, } Q = []byte{ // zone type 0x00, 0x01, 0x00, 0x00, // derived public key data - 0x9f, 0x27, 0xad, 0x25, 0xb5, 0x95, 0x4a, 0x46, - 0x7b, 0xc6, 0x5a, 0x67, 0x6b, 0x7a, 0x6d, 0x23, - 0xb2, 0xef, 0x30, 0x0f, 0x7f, 0xc7, 0x00, 0x58, - 0x05, 0x9e, 0x7f, 0x29, 0xe5, 0x94, 0xb5, 0xc1, + 0xb1, 0x0e, 0x88, 0xd5, 0x17, 0x02, 0xf3, 0x3d, + 0xc9, 0xcb, 0xa1, 0xe9, 0x16, 0x65, 0x9c, 0x44, + 0x47, 0x9c, 0xc8, 0xdb, 0x83, 0x32, 0xd1, 0xd1, + 0xc5, 0x03, 0xdb, 0x50, 0x0e, 0xbd, 0x2d, 0x67, } QUERY = []byte{ - 0xa9, 0x1a, 0x2c, 0x46, 0xf1, 0x98, 0x35, 0x50, - 0x4f, 0x4e, 0x96, 0x78, 0x2d, 0x77, 0xd1, 0x3b, - 0x9d, 0x4e, 0x61, 0xf3, 0x50, 0xe2, 0xe6, 0xa5, - 0xc2, 0xd1, 0x36, 0xc1, 0xf1, 0x37, 0x94, 0x79, - 0x19, 0xe9, 0xab, 0x2b, 0xae, 0xb5, 0xb9, 0x79, - 0xe9, 0x1e, 0xf2, 0x6a, 0xaa, 0x54, 0x81, 0x65, - 0xac, 0xb2, 0xec, 0xca, 0x8e, 0x30, 0x76, 0x1c, - 0xc2, 0x1b, 0xbe, 0x89, 0x0b, 0x34, 0x6d, 0xa1, + 0xa9, 0x47, 0x81, 0x8a, 0xaf, 0x45, 0x94, 0xda, + 0x89, 0x41, 0xfa, 0x29, 0x77, 0x53, 0x94, 0x9d, + 0xcb, 0xc5, 0xfb, 0x41, 0xea, 0x77, 0xc6, 0x25, + 0x11, 0x3a, 0x59, 0x09, 0x32, 0xfe, 0xeb, 0xb4, + 0x59, 0x98, 0x69, 0xe2, 0x83, 0xe9, 0xdb, 0xd9, + 0xc7, 0x24, 0xeb, 0xf2, 0xd5, 0x30, 0x3b, 0x73, + 0xd7, 0xda, 0x9a, 0x2c, 0xd1, 0xd7, 0x95, 0x70, + 0xc5, 0x9d, 0x71, 0xb8, 0x32, 0x68, 0xc9, 0xd1, } ) // create private key from scalar - prv, err := NewZonePrivate(ZONE_PKEY, D) + prv, err := NewZonePrivate(enums.GNS_TYPE_PKEY, D) if err != nil { t.Fatal(err) } diff --git a/src/gnunet/enums/gns.go b/src/gnunet/enums/gns.go index f6e58a2..8f786f8 100644 --- a/src/gnunet/enums/gns.go +++ b/src/gnunet/enums/gns.go @@ -19,12 +19,15 @@ //nolint:stylecheck // allow non-camel-case for constants package enums +// GNSFlag type +type GNSFlag uint32 + const ( // GNS record flags - GNS_FLAG_PRIVATE = 2 // Record is not shared on the DHT - GNS_FLAG_SUPPL = 4 // Supplemental records (e.g. NICK) in a block - GNS_FLAG_EXPREL = 8 // Expire time in record is in relative time. - GNS_FLAG_SHADOW = 16 // Record is ignored if non-expired records of same type exist in block + GNS_FLAG_PRIVATE GNSFlag = 2 // Record is not shared on the DHT + GNS_FLAG_SUPPL GNSFlag = 4 // Supplemental records (e.g. NICK) in a block + GNS_FLAG_EXPREL GNSFlag = 8 // Expire time in record is in relative time. + GNS_FLAG_SHADOW GNSFlag = 16 // Record is ignored if non-expired records of same type exist in block // GNS_LocalOptions GNS_LO_DEFAULT = 0 // Defaults, look in cache, then in DHT. @@ -39,3 +42,9 @@ const ( //go:generate go run generate.go gnunet-gns.rec gnunet-gns.tpl gns_type.go //go:generate stringer -type=GNSType gns_type.go + +// GNSSpec is the combination of type and flags +type GNSSpec struct { + Type GNSType + Flags GNSFlag +} diff --git a/src/gnunet/enums/gns_type.go b/src/gnunet/enums/gns_type.go index 5a13d67..d4cf6a6 100644 --- a/src/gnunet/enums/gns_type.go +++ b/src/gnunet/enums/gns_type.go @@ -3,7 +3,7 @@ //nolint:stylecheck // allow non-camel-case for constants package enums -type GNSType int +type GNSType uint32 // GNS constants const ( diff --git a/src/gnunet/enums/gnunet-gns.tpl b/src/gnunet/enums/gnunet-gns.tpl index 3249569..4f4f598 100644 --- a/src/gnunet/enums/gnunet-gns.tpl +++ b/src/gnunet/enums/gnunet-gns.tpl @@ -3,7 +3,7 @@ //nolint:stylecheck // allow non-camel-case for constants package enums -type GNSType int +type GNSType uint32 // GNS constants const ( diff --git a/src/gnunet/go.mod b/src/gnunet/go.mod index 26f0e38..185eaad 100644 --- a/src/gnunet/go.mod +++ b/src/gnunet/go.mod @@ -3,7 +3,7 @@ module gnunet go 1.18 require ( - github.com/bfix/gospel v1.2.19 + github.com/bfix/gospel v1.2.20 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 @@ -24,4 +24,4 @@ require ( golang.org/x/tools v0.1.11 // indirect ) -// replace github.com/bfix/gospel v1.2.19 => ../gospel +// replace github.com/bfix/gospel v1.2.20 => ../gospel diff --git a/src/gnunet/go.sum b/src/gnunet/go.sum index a1c014b..a8e3d7e 100644 --- a/src/gnunet/go.sum +++ b/src/gnunet/go.sum @@ -1,5 +1,5 @@ -github.com/bfix/gospel v1.2.19 h1:B57L5CMjKPeRPtVxt1JcSx42AKwD+SpN32QaF0DxXFM= -github.com/bfix/gospel v1.2.19/go.mod h1:cdu63bA9ZdfeDoqZ+vnWOcbY9Puwdzmf5DMxMGMznRI= +github.com/bfix/gospel v1.2.20 h1:e/IxmTiC579jIQlIxpMzCX/MIKHNsBzJ1WdMKheCgBw= +github.com/bfix/gospel v1.2.20/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= diff --git a/src/gnunet/message/factory.go b/src/gnunet/message/factory.go index 8f664b0..82af6e9 100644 --- a/src/gnunet/message/factory.go +++ b/src/gnunet/message/factory.go @@ -24,6 +24,8 @@ import ( ) // NewEmptyMessage creates a new empty message object for the given type. +// +//nolint:gocyclo // it's a long switch intentionally func NewEmptyMessage(msgType enums.MsgType) (Message, error) { switch msgType { //------------------------------------------------------------------ @@ -76,7 +78,7 @@ func NewEmptyMessage(msgType enums.MsgType) (Message, error) { case enums.MSG_DHT_P2P_GET: return NewDHTP2PGetMsg(), nil case enums.MSG_DHT_P2P_PUT: - return NewDHTP2PPutMsg(), nil + return NewDHTP2PPutMsg(nil), nil case enums.MSG_DHT_P2P_RESULT: return NewDHTP2PResultMsg(), nil @@ -111,6 +113,24 @@ func NewEmptyMessage(msgType enums.MsgType) (Message, error) { return NewRevocationRevokeMsg(nil), nil case enums.MSG_REVOCATION_REVOKE_RESPONSE: return NewRevocationRevokeResponseMsg(false), nil + + //------------------------------------------------------------------ + // Namestore service + //------------------------------------------------------------------ + case enums.MSG_NAMESTORE_ZONE_ITERATION_START: + return NewNamestoreZoneIterStartMsg(nil), nil + case enums.MSG_NAMESTORE_ZONE_ITERATION_NEXT: + case enums.MSG_NAMESTORE_ZONE_ITERATION_STOP: + case enums.MSG_NAMESTORE_RECORD_STORE: + case enums.MSG_NAMESTORE_RECORD_STORE_RESPONSE: + case enums.MSG_NAMESTORE_RECORD_LOOKUP: + case enums.MSG_NAMESTORE_RECORD_LOOKUP_RESPONSE: + case enums.MSG_NAMESTORE_ZONE_TO_NAME: + case enums.MSG_NAMESTORE_ZONE_TO_NAME_RESPONSE: + case enums.MSG_NAMESTORE_MONITOR_START: + case enums.MSG_NAMESTORE_MONITOR_SYNC: + case enums.MSG_NAMESTORE_RECORD_RESULT: + case enums.MSG_NAMESTORE_MONITOR_NEXT: } return nil, fmt.Errorf("unknown message type %d", msgType) } diff --git a/src/gnunet/message/msg_dht_p2p.go b/src/gnunet/message/msg_dht_p2p.go index 9d1d815..d4a474c 100644 --- a/src/gnunet/message/msg_dht_p2p.go +++ b/src/gnunet/message/msg_dht_p2p.go @@ -24,6 +24,7 @@ import ( "encoding/binary" "errors" "fmt" + "gnunet/config" "gnunet/crypto" "gnunet/enums" "gnunet/service/dht/blocks" @@ -122,9 +123,10 @@ type DHTP2PPutMsg struct { } // NewDHTP2PPutMsg creates an empty new DHTP2PPutMsg -func NewDHTP2PPutMsg() *DHTP2PPutMsg { - return &DHTP2PPutMsg{ - MsgHeader: MsgHeader{218, enums.MSG_DHT_P2P_PUT}, +func NewDHTP2PPutMsg(block blocks.Block) *DHTP2PPutMsg { + // create empty message + msg := &DHTP2PPutMsg{ + MsgHeader: MsgHeader{216, enums.MSG_DHT_P2P_PUT}, BType: enums.BLOCK_TYPE_ANY, // block type Flags: 0, // processing flags HopCount: 0, // message hops @@ -138,6 +140,20 @@ func NewDHTP2PPutMsg() *DHTP2PPutMsg { LastSig: nil, // no signature from last hop Block: nil, // no block data } + // initialize with block if available + if block != nil { + msg.BType = block.Type() + msg.HopCount = 0 + msg.PeerFilter = blocks.NewPeerFilter() + msg.ReplLvl = uint16(config.Cfg.GNS.ReplLevel) + msg.Expire = block.Expire() + msg.Block = block.Bytes() + msg.TruncOrigin = nil + msg.PutPath = nil + msg.LastSig = nil + msg.MsgSize += uint16(len(msg.Block)) + } + return msg } // IsUsed returns true if an optional field is used @@ -155,7 +171,7 @@ func (m *DHTP2PPutMsg) IsUsed(field string) bool { // Update message (forwarding) func (m *DHTP2PPutMsg) Update(p *path.Path, pf *blocks.PeerFilter, hop uint16) *DHTP2PPutMsg { - msg := NewDHTP2PPutMsg() + msg := NewDHTP2PPutMsg(nil) msg.Flags = m.Flags msg.HopCount = hop msg.PathL = p.NumList diff --git a/src/gnunet/message/msg_gns.go b/src/gnunet/message/msg_gns.go index 0b96e88..1c7f9af 100644 --- a/src/gnunet/message/msg_gns.go +++ b/src/gnunet/message/msg_gns.go @@ -23,6 +23,7 @@ import ( "gnunet/crypto" "gnunet/enums" + "gnunet/service/dht/blocks" "gnunet/util" "github.com/bfix/gospel/logger" @@ -39,7 +40,7 @@ type LookupMsg struct { Zone *crypto.ZoneKey `` // Zone that is to be used for lookup Options uint16 `order:"big"` // Local options for where to look for results Reserved uint16 `order:"big"` // Always 0 - RType uint32 `order:"big"` // the type of record to look up + RType enums.GNSType `order:"big"` // the type of record to look up Name []byte `size:"*"` // zero-terminated name to look up } @@ -51,7 +52,7 @@ func NewGNSLookupMsg() *LookupMsg { Zone: nil, Options: uint16(enums.GNS_LO_DEFAULT), Reserved: 0, - RType: uint32(enums.GNS_TYPE_ANY), + RType: enums.GNS_TYPE_ANY, Name: nil, } } @@ -84,78 +85,12 @@ func (m *LookupMsg) String() string { // GNS_LOOKUP_RESULT //---------------------------------------------------------------------- -// RecordSet ist the GNUnet data structure for a list of resource records -// in a GNSBlock. As part of GNUnet messages, the record set is padded so that -// the binary size of (records||padding) is the smallest power of two. -type RecordSet struct { - Count uint32 `order:"big"` // number of resource records - Records []*ResourceRecord `size:"Count"` // list of resource records - Padding []byte `size:"*"` // padding -} - -// NewRecordSet returns an empty resource record set. -func NewRecordSet() *RecordSet { - return &RecordSet{ - Count: 0, - Records: make([]*ResourceRecord, 0), - Padding: make([]byte, 0), - } -} - -// AddRecord to append a resource record to the set. -func (rs *RecordSet) AddRecord(rec *ResourceRecord) { - rs.Count++ - rs.Records = append(rs.Records, rec) -} - -// SetPadding (re-)calculates and allocates the padding. -func (rs *RecordSet) SetPadding() { - size := 0 - for _, rr := range rs.Records { - size += int(rr.Size) + 20 - } - n := 1 - for n < size { - n <<= 1 - } - rs.Padding = make([]byte, n-size) -} - -// Expire returns the earliest expiration timestamp for the records. -func (rs *RecordSet) Expire() util.AbsoluteTime { - var expires util.AbsoluteTime - for i, rr := range rs.Records { - if i == 0 { - expires = rr.Expire - } else if rr.Expire.Compare(expires) < 0 { - expires = rr.Expire - } - } - return expires -} - -// ResourceRecord is the GNUnet-specific representation of resource -// records (not to be confused with DNS resource records). -type ResourceRecord struct { - Expire util.AbsoluteTime // Expiration time for the record - Size uint32 `order:"big"` // Number of bytes in 'Data' - RType uint32 `order:"big"` // Type of the GNS/DNS record - Flags uint32 `order:"big"` // Flags for the record - Data []byte `size:"Size"` // Record data -} - -// String returns a human-readable representation of the message. -func (r *ResourceRecord) String() string { - return fmt.Sprintf("GNSResourceRecord{type=%s,expire=%s,flags=%d,size=%d}", - enums.GNSType(r.RType).String(), r.Expire, r.Flags, r.Size) -} - // LookupResultMsg is a response message for a GNS name lookup request type LookupResultMsg struct { MsgHeader - ID uint32 `order:"big"` // Unique identifier for this request (for key collisions). - Count uint32 `order:"big"` // The number of records contained in response - Records []*ResourceRecord `size:"Count"` // GNS resource records + ID uint32 `order:"big"` // Unique identifier for this request (for key collisions). + Count uint32 `order:"big"` // The number of records contained in response + Records []*blocks.ResourceRecord `size:"Count"` // GNS resource records } // NewGNSLookupResultMsg returns a new lookup result message @@ -164,12 +99,12 @@ func NewGNSLookupResultMsg(id uint32) *LookupResultMsg { MsgHeader: MsgHeader{12, enums.MSG_GNS_LOOKUP_RESULT}, ID: id, Count: 0, - Records: make([]*ResourceRecord, 0), + Records: make([]*blocks.ResourceRecord, 0), } } // AddRecord adds a GNS resource recordto the response message. -func (m *LookupResultMsg) AddRecord(rec *ResourceRecord) error { +func (m *LookupResultMsg) AddRecord(rec *blocks.ResourceRecord) error { recSize := 20 + int(rec.Size) if int(m.MsgSize)+recSize > enums.GNS_MAX_BLOCK_SIZE { return fmt.Errorf("gns.AddRecord(): MAX_BLOCK_SIZE reached") diff --git a/src/gnunet/message/msg_gns_test.go b/src/gnunet/message/msg_gns_test.go deleted file mode 100644 index 034e1a8..0000000 --- a/src/gnunet/message/msg_gns_test.go +++ /dev/null @@ -1,353 +0,0 @@ -// This file is part of gnunet-go, a GNUnet-implementation in Golang. -// Copyright (C) 2019-2022 Bernd Fix >Y< -// -// gnunet-go is free software: you can redistribute it and/or modify it -// under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, -// or (at your option) any later version. -// -// gnunet-go is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -// -// SPDX-License-Identifier: AGPL3.0-or-later - -package message - -import ( - "bytes" - "encoding/hex" - "gnunet/crypto" - "gnunet/enums" - "gnunet/util" - "testing" - - "github.com/bfix/gospel/data" -) - -// TestRecordsetPKEY implements the test case as defined in the GNS draft -// (see section 13. Test vectors, case "PKEY") -func TestRecordsetPKEY(t *testing.T) { - var ( - D = []byte{ - // PKEY private scalar - 0x50, 0xd7, 0xb6, 0x52, 0xa4, 0xef, 0xea, 0xdf, - 0xf3, 0x73, 0x96, 0x90, 0x97, 0x85, 0xe5, 0x95, - 0x21, 0x71, 0xa0, 0x21, 0x78, 0xc8, 0xe7, 0xd4, - 0x50, 0xfa, 0x90, 0x79, 0x25, 0xfa, 0xfd, 0x98, - } - ZKEY = []byte{ - // zone type - 0x00, 0x01, 0x00, 0x00, - // PKEY public key - 0x67, 0x7c, 0x47, 0x7d, 0x2d, 0x93, 0x09, 0x7c, - 0x85, 0xb1, 0x95, 0xc6, 0xf9, 0x6d, 0x84, 0xff, - 0x61, 0xf5, 0x98, 0x2c, 0x2c, 0x4f, 0xe0, 0x2d, - 0x5a, 0x11, 0xfe, 0xdf, 0xb0, 0xc2, 0x90, 0x1f, - } - ZID = "000G0037FH3QTBCK15Y8BCCNRVWPV17ZC7TSGB1C9ZG2TPGHZVFV1GMG3W" - RECSET = &RecordSet{ - Count: 2, - Records: []*ResourceRecord{ - { - Expire: util.AbsoluteTime{ - Val: uint64(14888744139323793), - }, - Size: 4, - RType: 1, - Flags: 0, - Data: []byte{ - 0x01, 0x02, 0x03, 0x04, - }, - }, - { - Expire: util.AbsoluteTime{ - Val: uint64(26147096139323793), - }, - Size: 36, - RType: crypto.ZONE_PKEY, - Flags: 2, - Data: []byte{ - 0x00, 0x01, 0x00, 0x00, - 0x0e, 0x60, 0x1b, 0xe4, 0x2e, 0xb5, 0x7f, 0xb4, - 0x69, 0x76, 0x10, 0xcf, 0x3a, 0x3b, 0x18, 0x34, - 0x7b, 0x65, 0xa3, 0x3f, 0x02, 0x5b, 0x5b, 0x17, - 0x4a, 0xbe, 0xfb, 0x30, 0x80, 0x7b, 0xfe, 0xcf, - }, - }, - }, - Padding: make([]byte, 0), - } - RDATA = []byte{ - 0x00, 0x00, 0x00, 0x02, 0x00, 0x34, 0xe5, 0x3b, - 0xe1, 0x93, 0x79, 0x91, 0x00, 0x00, 0x00, 0x04, - 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x02, 0x03, 0x04, 0x00, 0x5c, 0xe4, 0xa5, - 0x39, 0x4a, 0xd9, 0x91, 0x00, 0x00, 0x00, 0x24, - 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, - 0x00, 0x01, 0x00, 0x00, 0x0e, 0x60, 0x1b, 0xe4, - 0x2e, 0xb5, 0x7f, 0xb4, 0x69, 0x76, 0x10, 0xcf, - 0x3a, 0x3b, 0x18, 0x34, 0x7b, 0x65, 0xa3, 0x3f, - 0x02, 0x5b, 0x5b, 0x17, 0x4a, 0xbe, 0xfb, 0x30, - 0x80, 0x7b, 0xfe, 0xcf, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, - } - NONCE = []byte{ - 0x67, 0xeb, 0xda, 0x27, 0x00, 0x34, 0xe5, 0x3b, - 0xe1, 0x93, 0x79, 0x91, 0x00, 0x00, 0x00, 0x01, - } - SKEY = []byte{ - 0x55, 0x1f, 0x15, 0x7a, 0xcf, 0x2b, 0xf1, 0xd4, - 0xa9, 0x75, 0x03, 0x69, 0x99, 0xea, 0x7c, 0x82, - 0x86, 0xac, 0xb3, 0x18, 0xf1, 0x49, 0x3e, 0x63, - 0xb5, 0x00, 0x60, 0x3a, 0x9b, 0x02, 0xe3, 0xe4, - } - BDATA = []byte{ - 0x00, 0xe4, 0x83, 0x7e, 0xb5, 0xd0, 0x4f, 0x92, - 0x90, 0x3d, 0xe4, 0xb5, 0x23, 0x4e, 0x8c, 0xca, - 0xc5, 0x73, 0x6c, 0x97, 0x93, 0x37, 0x9a, 0x59, - 0xc3, 0x33, 0x75, 0xfc, 0x89, 0x51, 0xac, 0xa2, - 0xeb, 0x7a, 0xad, 0x06, 0x7b, 0xf9, 0xaf, 0x60, - 0xbf, 0x26, 0x75, 0x86, 0x46, 0xa1, 0x7f, 0x5e, - 0x5c, 0x3b, 0x62, 0x15, 0xf9, 0x40, 0x79, 0x54, - 0x5b, 0x1c, 0x4d, 0x4f, 0x1b, 0x2e, 0xbb, 0x22, - 0xc2, 0xb4, 0xda, 0xd4, 0x41, 0x26, 0x81, 0x7b, - 0x6f, 0x00, 0x15, 0x30, 0xd4, 0x76, 0x40, 0x1d, - 0xd6, 0x7a, 0xc0, 0x14, 0x85, 0x54, 0xe8, 0x06, - 0x35, 0x3d, 0xa9, 0xe4, 0x29, 0x80, 0x79, 0xf3, - 0xe1, 0xb1, 0x69, 0x42, 0xc4, 0x8d, 0x90, 0xc4, - 0x36, 0x0c, 0x61, 0x23, 0x8c, 0x40, 0xd9, 0xd5, - 0x29, 0x11, 0xae, 0xa5, 0x2c, 0xc0, 0x03, 0x7a, - 0xc7, 0x16, 0x0b, 0xb3, 0xcf, 0x5b, 0x2f, 0x4a, - 0x72, 0x2f, 0xd9, 0x6b, - } - LABEL = "test" - ) - - // check zone key pair - prv, err := crypto.NewZonePrivate(crypto.ZONE_PKEY, D) - if err != nil { - t.Fatal(err) - } - zk := prv.Public() - if !bytes.Equal(zk.Bytes(), ZKEY) { - t.Logf("pub = %s\n", hex.EncodeToString(zk.Bytes())) - t.Logf(" != %s\n", hex.EncodeToString(ZKEY)) - t.Fatal("zone key mismatch") - } - zid := zk.ID() - if zid != ZID { - t.Logf("id = %s\n", zid) - t.Logf("ID = %s\n", ZID) - t.Fatal("Zone ID mismatch") - } - - // assemble and check recordset - RECSET.SetPadding() - rdata, err := data.Marshal(RECSET) - if err != nil { - t.Fatal(err) - } - if !bytes.Equal(rdata, RDATA) { - t.Logf("rdata = %s\n", hex.EncodeToString(rdata)) - t.Logf("RDATA = %s\n", hex.EncodeToString(RDATA)) - t.Fatal("RDATA mismatch") - } - - // check symmetric keys and nonce - expires := RECSET.Expire() - skey := zk.BlockKey(LABEL, expires) - if !bytes.Equal(skey[32:], NONCE) { - t.Logf("nonce = %s\n", hex.EncodeToString(skey[32:])) - t.Logf("NONCE = %s\n", hex.EncodeToString(NONCE)) - t.Fatal("NONCE mismatch") - } - if !bytes.Equal(skey[:32], SKEY) { - t.Logf("skey = %s\n", hex.EncodeToString(skey[:32])) - t.Logf("SKEY = %s\n", hex.EncodeToString(SKEY)) - t.Fatal("SKEY mismatch") - } - - // check block encryption - bdata, err := zk.Encrypt(rdata, LABEL, expires) - if err != nil { - t.Fatal(err) - } - if !bytes.Equal(bdata, BDATA) { - t.Logf("bdata = %s\n", hex.EncodeToString(bdata)) - t.Logf("BDATA = %s\n", hex.EncodeToString(BDATA)) - t.Fatal("BDATA mismatch") - } -} - -// TestRecordsetEDKEY implements the test case as defined in the GNS draft -// (see section 13. Test vectors, case "EDKEY") -func TestRecordsetEDKEY(t *testing.T) { - var ( - SEED = []byte{ - // EDKEY private key (seed) - 0x5a, 0xf7, 0x02, 0x0e, 0xe1, 0x91, 0x60, 0x32, - 0x88, 0x32, 0x35, 0x2b, 0xbc, 0x6a, 0x68, 0xa8, - 0xd7, 0x1a, 0x7c, 0xbe, 0x1b, 0x92, 0x99, 0x69, - 0xa7, 0xc6, 0x6d, 0x41, 0x5a, 0x0d, 0x8f, 0x65, - } - ZKEY = []byte{ - // zone type - 0x00, 0x01, 0x00, 0x14, - // EDKEY public key data - 0x3c, 0xf4, 0xb9, 0x24, 0x03, 0x20, 0x22, 0xf0, - 0xdc, 0x50, 0x58, 0x14, 0x53, 0xb8, 0x5d, 0x93, - 0xb0, 0x47, 0xb6, 0x3d, 0x44, 0x6c, 0x58, 0x45, - 0xcb, 0x48, 0x44, 0x5d, 0xdb, 0x96, 0x68, 0x8f, - } - ZID = "000G051WYJWJ80S04BRDRM2R2H9VGQCKP13VCFA4DHC4BJT88HEXQ5K8HW" - RECSET = &RecordSet{ - Count: 2, - Records: []*ResourceRecord{ - { - Expire: util.AbsoluteTime{ - Val: uint64(2463385894000000), - }, - Size: 4, - RType: 1, - Flags: 0, - Data: []byte{ - 0x01, 0x02, 0x03, 0x04, - }, - }, - { - Expire: util.AbsoluteTime{ - Val: uint64(49556645701000000), - }, - Size: 36, - RType: uint32(enums.GNS_TYPE_NICK), - Flags: 2, - Data: []byte{ - 0x4d, 0x79, 0x20, 0x4e, 0x69, 0x63, 0x6b, 0x00, - 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, - 0x6f, 0x6e, 0x20, 0x4e, 0x4f, 0x4e, 0x43, 0x45, - 0x7c, 0x45, 0x58, 0x50, 0x49, 0x52, 0x41, 0x54, - 0x49, 0x4f, 0x4e, 0x3a, - }, - }, - }, - Padding: make([]byte, 0), - } - RDATA = []byte{ - 0x00, 0x00, 0x00, 0x02, 0x00, 0x08, 0xc0, 0x6f, - 0xb9, 0x28, 0x15, 0x80, 0x00, 0x00, 0x00, 0x04, - 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x02, 0x03, 0x04, 0x00, 0xb0, 0x0f, 0x81, - 0xb7, 0x44, 0x9b, 0x40, 0x00, 0x00, 0x00, 0x24, - 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, - 0x4d, 0x79, 0x20, 0x4e, 0x69, 0x63, 0x6b, 0x00, - 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, - 0x6f, 0x6e, 0x20, 0x4e, 0x4f, 0x4e, 0x43, 0x45, - 0x7c, 0x45, 0x58, 0x50, 0x49, 0x52, 0x41, 0x54, - 0x49, 0x4f, 0x4e, 0x3a, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, - } - NONCE = []byte{ - 0x95, 0x4c, 0xb5, 0xd6, 0x31, 0x9f, 0x9e, 0x31, - 0xff, 0x80, 0x4a, 0xe6, 0x83, 0xbc, 0x19, 0x37, - 0x00, 0x08, 0xc0, 0x6f, 0xb9, 0x28, 0x15, 0x80, - } - SKEY = []byte{ - 0x08, 0x34, 0xa3, 0xa3, 0xae, 0x09, 0xcb, 0x3b, - 0xd9, 0x8c, 0xec, 0xdb, 0x47, 0x7c, 0x3b, 0x32, - 0x45, 0xd0, 0xce, 0xda, 0x94, 0x8f, 0x9e, 0xbb, - 0xba, 0x3b, 0x17, 0x91, 0x61, 0x7b, 0xee, 0x69, - } - BDATA = []byte{ - 0x36, 0x07, 0xf8, 0x62, 0xfc, 0xf4, 0xc6, 0xd4, - 0x86, 0x1c, 0x7a, 0x06, 0x08, 0x81, 0x28, 0xbb, - 0x3d, 0x6c, 0xca, 0xe2, 0xb1, 0x4e, 0xf4, 0x25, - 0xe3, 0xd6, 0xbb, 0xd6, 0x27, 0x1a, 0x71, 0xe5, - 0x42, 0x1c, 0x25, 0x1c, 0xfb, 0x5e, 0xb6, 0xd7, - 0xbc, 0x9e, 0x74, 0xb2, 0xe8, 0xc8, 0xd8, 0x6c, - 0xe0, 0x65, 0x37, 0x12, 0x0c, 0x2e, 0xe2, 0x28, - 0x5b, 0x93, 0xc5, 0xaf, 0xb7, 0x79, 0xf9, 0xcf, - 0x50, 0x2e, 0x16, 0xa5, 0xad, 0x30, 0xe6, 0x22, - 0xed, 0x58, 0x92, 0xd2, 0x46, 0xc0, 0x34, 0x11, - 0x70, 0xf0, 0xc5, 0x1c, 0x39, 0x40, 0xab, 0x33, - 0x47, 0xdc, 0x91, 0x56, 0x5f, 0x36, 0x6d, 0xb6, - 0x23, 0x56, 0x73, 0x9a, 0xd8, 0xde, 0x68, 0x21, - 0x12, 0x68, 0xf0, 0xc0, 0x44, 0x00, 0x81, 0xd8, - 0xaf, 0x8a, 0x6e, 0x16, 0x45, 0xa6, 0x92, 0x46, - 0xb4, 0x34, 0xe2, 0xc8, 0x76, 0x9f, 0x00, 0x1b, - 0xd5, 0x1a, 0xb3, 0x73, 0x5e, 0x02, 0xb4, 0x81, - 0xa6, 0x83, 0x0f, 0x00, 0xd2, 0xf6, 0xf3, 0x15, - 0xdf, 0x54, 0x20, 0x90, - } - LABEL = "test" - ) - - // check zone key pair - prv, err := crypto.NewZonePrivate(crypto.ZONE_EDKEY, SEED) - if err != nil { - t.Fatal(err) - } - zk := prv.Public() - if !bytes.Equal(zk.Bytes(), ZKEY) { - t.Logf("zkey = %s\n", hex.EncodeToString(zk.Bytes())) - t.Logf(" != %s\n", hex.EncodeToString(ZKEY)) - t.Fatal("zone key mismatch") - } - zid := zk.ID() - if zid != ZID { - t.Logf("id = %s\n", zid) - t.Logf("ID = %s\n", ZID) - t.Fatal("Zone ID mismatch") - } - - // assemble and check recordset - RECSET.SetPadding() - rdata, err := data.Marshal(RECSET) - if err != nil { - t.Fatal(err) - } - if !bytes.Equal(rdata, RDATA) { - t.Logf("rdata = %s\n", hex.EncodeToString(rdata)) - t.Logf("RDATA = %s\n", hex.EncodeToString(RDATA)) - t.Fatal("RDATA mismatch") - } - - // check symmetric keys and nonce - expires := RECSET.Expire() - skey := zk.BlockKey(LABEL, expires) - if !bytes.Equal(skey[32:], NONCE) { - t.Logf("nonce = %s\n", hex.EncodeToString(skey[32:])) - t.Logf("NONCE = %s\n", hex.EncodeToString(NONCE)) - t.Fatal("NONCE mismatch") - } - if !bytes.Equal(skey[:32], SKEY) { - t.Logf("skey = %s\n", hex.EncodeToString(skey[:32])) - t.Logf("SKEY = %s\n", hex.EncodeToString(SKEY)) - t.Fatal("SKEY mismatch") - } - - // check block encryption - bdata, err := zk.Encrypt(rdata, LABEL, expires) - if err != nil { - t.Fatal(err) - } - if !bytes.Equal(bdata, BDATA) { - t.Logf("bdata = %s\n", hex.EncodeToString(bdata)) - t.Logf("BDATA = %s\n", hex.EncodeToString(BDATA)) - t.Fatal("BDATA mismatch") - } -} diff --git a/src/gnunet/message/msg_namecache.go b/src/gnunet/message/msg_namecache.go index ea413a7..53b2f4c 100644 --- a/src/gnunet/message/msg_namecache.go +++ b/src/gnunet/message/msg_namecache.go @@ -27,14 +27,32 @@ import ( "gnunet/util" ) +//---------------------------------------------------------------------- +// Generic Namecache message header +//---------------------------------------------------------------------- + +// GenericNamecacheMsg is the common header for Namestore messages +type GenericNamecacheMsg struct { + MsgHeader + ID uint32 `order:"big"` // unique reference ID +} + +// return initialized common message header +func newGenericNamecacheMsg(size uint16, mtype enums.MsgType) GenericNamecacheMsg { + return GenericNamecacheMsg{ + MsgHeader: MsgHeader{size, mtype}, + ID: uint32(util.NextID()), + } +} + //---------------------------------------------------------------------- // NAMECACHE_LOOKUP_BLOCK //---------------------------------------------------------------------- // NamecacheLookupMsg is request message for lookups in local namecache type NamecacheLookupMsg struct { - MsgHeader - ID uint32 `order:"big"` // Request Id + GenericNamecacheMsg + Query *crypto.HashCode // Query data } @@ -44,9 +62,8 @@ func NewNamecacheLookupMsg(query *crypto.HashCode) *NamecacheLookupMsg { query = crypto.NewHashCode(nil) } return &NamecacheLookupMsg{ - MsgHeader: MsgHeader{72, enums.MSG_NAMECACHE_LOOKUP_BLOCK}, - ID: 0, - Query: query, + GenericNamecacheMsg: newGenericNamecacheMsg(72, enums.MSG_NAMECACHE_LOOKUP_BLOCK), + Query: query, } } @@ -62,21 +79,20 @@ func (m *NamecacheLookupMsg) String() string { // NamecacheLookupResultMsg is the response message for namecache lookups. type NamecacheLookupResultMsg struct { - MsgHeader - ID uint32 `order:"big"` // Request Id - Expire util.AbsoluteTime `` // Expiration time - DerivedKeySig *crypto.ZoneSignature `` // Derived public key - EncData []byte `size:"*"` // Encrypted block data + GenericNamecacheMsg + + Expire util.AbsoluteTime `` // Expiration time + DerivedKeySig *crypto.ZoneSignature `` // Derived public key + EncData []byte `size:"*"` // Encrypted block data } // NewNamecacheLookupResultMsg creates a new default message. func NewNamecacheLookupResultMsg() *NamecacheLookupResultMsg { return &NamecacheLookupResultMsg{ - MsgHeader: MsgHeader{112, enums.MSG_NAMECACHE_LOOKUP_BLOCK_RESPONSE}, - ID: 0, - Expire: *new(util.AbsoluteTime), - DerivedKeySig: nil, - EncData: nil, + GenericNamecacheMsg: newGenericNamecacheMsg(112, enums.MSG_NAMECACHE_LOOKUP_BLOCK_RESPONSE), + Expire: util.AbsoluteTimeNever(), + DerivedKeySig: nil, + EncData: nil, } } @@ -92,24 +108,38 @@ func (m *NamecacheLookupResultMsg) String() string { // NamecacheCacheMsg is the request message to put a name into the local cache. type NamecacheCacheMsg struct { - MsgHeader - ID uint32 `order:"big"` // Request Id - Expire util.AbsoluteTime `` // Expiration time - DerivedKeySig *crypto.ZoneSignature `` // Derived public key and signature - EncData []byte `size:"*"` // Encrypted block data + GenericNamecacheMsg + + Expire util.AbsoluteTime `` // Expiration time + DerivedSig []byte `size:"(FldSize)"` // Derived signature + DerivedKey []byte `size:"(FldSize)"` // Derived public key + EncData []byte `size:"*"` // Encrypted block data +} + +// Size returns buffer sizes for fields +func (m *NamecacheCacheMsg) FldSize(field string) uint { + switch field { + case "DerivedSig": + return 64 + case "DerivedKey": + return 36 + } + // defaults to empty buffer + return 0 } // NewNamecacheCacheMsg creates a new default message. func NewNamecacheCacheMsg(block *blocks.GNSBlock) *NamecacheCacheMsg { msg := &NamecacheCacheMsg{ - MsgHeader: MsgHeader{108, enums.MSG_NAMECACHE_BLOCK_CACHE}, - ID: 0, - Expire: *new(util.AbsoluteTime), - DerivedKeySig: nil, - EncData: make([]byte, 0), + GenericNamecacheMsg: newGenericNamecacheMsg(116, enums.MSG_NAMECACHE_BLOCK_CACHE), + Expire: util.AbsoluteTimeNever(), + DerivedSig: nil, + DerivedKey: nil, + EncData: make([]byte, 0), } if block != nil { - msg.DerivedKeySig = block.DerivedKeySig + msg.DerivedKey = util.Clone(block.DerivedKeySig.ZoneKey.Bytes()) + msg.DerivedSig = util.Clone(block.DerivedKeySig.Signature) msg.Expire = block.Body.Expire size := len(block.Body.Data) msg.EncData = make([]byte, size) @@ -121,8 +151,8 @@ func NewNamecacheCacheMsg(block *blocks.GNSBlock) *NamecacheCacheMsg { // String returns a human-readable representation of the message. func (m *NamecacheCacheMsg) String() string { - return fmt.Sprintf("NewNamecacheCacheMsg{id=%d,expire=%s}", - m.ID, m.Expire) + return fmt.Sprintf("NamecacheCacheMsg{size=%d,id=%d,expire=%s}", + m.Size(), m.ID, m.Expire) } //---------------------------------------------------------------------- @@ -131,17 +161,16 @@ func (m *NamecacheCacheMsg) String() string { // NamecacheCacheResponseMsg is the response message for a put request type NamecacheCacheResponseMsg struct { - MsgHeader - ID uint32 `order:"big"` // Request Id - Result int32 `order:"big"` // Result code + GenericNamecacheMsg + + Result int32 `order:"big"` // Result code } // NewNamecacheCacheResponseMsg creates a new default message. func NewNamecacheCacheResponseMsg() *NamecacheCacheResponseMsg { return &NamecacheCacheResponseMsg{ - MsgHeader: MsgHeader{12, enums.MSG_NAMECACHE_BLOCK_CACHE_RESPONSE}, - ID: 0, - Result: 0, + GenericNamecacheMsg: newGenericNamecacheMsg(12, enums.MSG_NAMECACHE_BLOCK_CACHE_RESPONSE), + Result: 0, } } diff --git a/src/gnunet/message/msg_namestore.go b/src/gnunet/message/msg_namestore.go new file mode 100644 index 0000000..a682ef1 --- /dev/null +++ b/src/gnunet/message/msg_namestore.go @@ -0,0 +1,199 @@ +// 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/crypto" + "gnunet/enums" + "gnunet/service/dht/blocks" + "gnunet/util" +) + +//====================================================================== +// NameStore service messages +//====================================================================== + +// GenericNamestoreMsg is the common header for Namestore messages +type GenericNamestoreMsg struct { + MsgHeader + ID uint32 `order:"big"` // unique reference ID +} + +// return initialized common message header +func newGenericNamestoreMsg(size uint16, mtype enums.MsgType) GenericNamestoreMsg { + return GenericNamestoreMsg{ + MsgHeader: MsgHeader{size, mtype}, + ID: uint32(util.NextID()), + } +} + +//---------------------------------------------------------------------- +// MSG_NAMESTORE_ZONE_ITERATION_START +//---------------------------------------------------------------------- + +// NamestoreZoneIterStartMsg starts a new iteration over all zones +type NamestoreZoneIterStartMsg struct { + GenericNamestoreMsg + + ZoneKey *crypto.ZonePrivate // private zone key +} + +// NewNamecacheCacheMsg creates a new default message. +func NewNamestoreZoneIterStartMsg(zone *crypto.ZonePrivate) *NamestoreZoneIterStartMsg { + return &NamestoreZoneIterStartMsg{ + GenericNamestoreMsg: newGenericNamestoreMsg(100, enums.MSG_NAMESTORE_ZONE_ITERATION_START), + ZoneKey: zone, + } +} + +// String returns a human-readable representation of the message. +func (m *NamestoreZoneIterStartMsg) String() string { + return fmt.Sprintf("NamestoreZoneIterStartMsg{id=%d,zone=%s}", m.ID, m.ZoneKey.ID()) +} + +//---------------------------------------------------------------------- +// MSG_NAMESTORE_ZONE_ITERATION_NEXT +//---------------------------------------------------------------------- + +type NamestoreZoneIterNextMsg struct { + GenericNamestoreMsg + + Limit uint64 `order:"big"` // max. number of records in one go +} + +func NewNamestoreZoneIterNextMsg() *NamestoreZoneIterNextMsg { + return &NamestoreZoneIterNextMsg{} +} + +// String returns a human-readable representation of the message. +func (m *NamestoreZoneIterNextMsg) String() string { + return fmt.Sprintf("NamestoreZoneIterNextMsg{id=%d,limit=%d}", m.ID, m.Limit) +} + +//---------------------------------------------------------------------- +// MSG_NAMESTORE_ZONE_ITERATION_STOP +//---------------------------------------------------------------------- + +type NamestoreZoneIterStopMsg struct { + GenericNamestoreMsg +} + +//---------------------------------------------------------------------- +//---------------------------------------------------------------------- + +type NamestoreRecordStoreMsg struct { + GenericNamestoreMsg + + ZoneKey *crypto.ZonePrivate // private zone key + Records *blocks.RecordSet // list of records +} + +type NamestoreRecordStoreRespMsg struct { + GenericNamestoreMsg + + Status int32 `order:"big"` // result status + ErrLen uint16 `order:"big"` // length of error message + Reserved uint16 `order:"big"` // alignment + Error []byte `size:"ErrLen"` // error message +} + +type NamestoreLabelLookupMsg struct { + GenericNamestoreMsg + + LblLen uint32 `order:"big"` // length of label + IsEdit uint32 `order:"big"` // lookup corresponds to edit request + ZoneKey *crypto.ZonePrivate // private zone key + Label []byte `size:"LblLen"` // label string +} + +type NamestoreLabelLookupRespMsg struct { + GenericNamestoreMsg + + LblLen uint16 `order:"big"` // Length of label + RdLen uint16 `order:"big"` // size of record data + RdCount uint16 `order:"big"` // number of records + Found int16 `order:"big"` // label found? + ZoneKey *crypto.ZonePrivate // private zone key + Label []byte `size:"LblLen"` // label string + Records []byte `size:"RdLen"` // serialized record data +} + +type NamestoreZoneToNameMsg struct { + GenericNamestoreMsg + + ZoneKey *crypto.ZonePrivate // private zone key + ZonePublic *crypto.ZoneKey // public zone key +} + +type NamestoreZoneToNameRespMsg struct { + GenericNamestoreMsg + + NameLen uint16 `order:"big"` // length of name + RdLen uint16 `order:"big"` // size of record data + RdCount uint16 `order:"big"` // number of records + Status int16 `order:"big"` // result status + ZoneKey *crypto.ZonePrivate // private zone key + Name []byte `size:"NameLen"` // name string + Records []byte `size:"RdLen"` // serialized record data +} + +type NamestoreRecordResultMsg struct { + GenericNamestoreMsg + + Expire util.AbsoluteTime `` // expiration date + NameLen uint16 `order:"big"` // length of name + RdLen uint16 `order:"big"` // size of record data + RdCount uint16 `order:"big"` // number of records + Reserved uint16 `order:"big"` // alignment + ZoneKey *crypto.ZonePrivate // private zone key + Name []byte `size:"NameLen"` // name string + Records []byte `size:"RdLen"` // serialized record data +} + +type NamestoreTxControlMsg struct { + GenericNamestoreMsg + + Control uint16 `order:"big"` // type of control message + Reserved uint16 `order:"big"` // alignment +} + +type NamestoreTxControlResultMsg struct { + GenericNamestoreMsg + + Control uint16 `order:"big"` // type of control message + Status uint16 `order:"big"` // result status + Error []byte `size:"*"` // error message (on status != OK) +} + +type NamestoreZoneMonStartMsg struct { + GenericNamestoreMsg + + Iterate uint32 `order:"big"` // iterate over all records + Filter uint16 `order:"big"` // filter flags + Reserved uint16 `order:"big"` // alignment + ZoneKey *crypto.ZonePrivate // private zone key +} + +type NamestoreZoneMonNextMsg struct { + GenericNamestoreMsg + + Reserved uint32 `order:"big"` // alignment =0 + Limit uint64 `order:"big"` // max. number of records in one go +} diff --git a/src/gnunet/service/client.go b/src/gnunet/service/client.go index 5f7f8f0..7b5e29b 100644 --- a/src/gnunet/service/client.go +++ b/src/gnunet/service/client.go @@ -66,7 +66,8 @@ func RequestResponse( caller string, callee string, path string, - req message.Message) (message.Message, error) { + req message.Message, + withResponse bool) (message.Message, error) { // client-connect to the service logger.Printf(logger.DBG, "[%s] Connecting to %s service...\n", caller, callee) cl, err := NewClient(ctx, path) @@ -78,11 +79,13 @@ func RequestResponse( if err = cl.SendRequest(ctx, req); err != nil { return nil, err } - // wait for a single response, then close the connection - logger.Printf(logger.DBG, "[%s] Waiting for response from %s service\n", caller, callee) var resp message.Message - if resp, err = cl.ReceiveResponse(ctx); err != nil { - return nil, err + if withResponse { + // wait for a single response, then close the connection + logger.Printf(logger.DBG, "[%s] Waiting for response from %s service\n", caller, callee) + if resp, err = cl.ReceiveResponse(ctx); err != nil { + return nil, err + } } logger.Printf(logger.DBG, "[%s] Closing connection to %s service\n", caller, callee) cl.Close() diff --git a/src/gnunet/service/dht/blocks/gns.go b/src/gnunet/service/dht/blocks/gns.go index 6b41b0b..0c32085 100644 --- a/src/gnunet/service/dht/blocks/gns.go +++ b/src/gnunet/service/dht/blocks/gns.go @@ -47,7 +47,7 @@ type GNSQuery struct { GenericQuery Zone *crypto.ZoneKey // Public zone key Label string // Atomic label - derived *crypto.ZoneKey // Derived zone key from (pkey,label) + derived *crypto.ZoneKey // Derived zone key from (zone,label) } // Verify the integrity of the block data from a signature. @@ -103,6 +103,7 @@ func NewGNSQuery(zkey *crypto.ZoneKey, label string) *GNSQuery { pd, _, err := zkey.Derive(label, "gns") if err != nil { logger.Printf(logger.ERROR, "[NewGNSQuery] failed: %s", err.Error()) + return nil } gq := crypto.Hash(pd.Bytes()) return &GNSQuery{ @@ -140,6 +141,11 @@ type GNSBlock struct { data []byte // decrypted data } +// Payload returns the decrypted block data (or nil) +func (b *GNSBlock) Payload() []byte { + return util.Clone(b.data) +} + // Bytes return th binary representation of block func (b *GNSBlock) Bytes() []byte { buf, _ := data.Marshal(b) @@ -167,8 +173,12 @@ func NewGNSBlock() Block { return &GNSBlock{ DerivedKeySig: nil, Body: &SignedGNSBlockData{ - Purpose: new(crypto.SignaturePurpose), - Data: nil, + Purpose: &crypto.SignaturePurpose{ + Size: 16, + Purpose: enums.SIG_GNS_RECORD_SIGN, + }, + Expire: util.AbsoluteTimeNever(), + Data: nil, }, checked: false, verified: false, @@ -181,6 +191,23 @@ func NewGNSBlock() Block { // Not required for GNS blocks func (b *GNSBlock) Prepare(enums.BlockType, util.AbsoluteTime) {} +// SetData sets the data for the GNS block +func (b *GNSBlock) SetData(data []byte) { + b.Body.Data = data + b.Body.Purpose.Size = uint32(len(data) + 16) +} + +// Sign the block with a derived private key +func (b *GNSBlock) Sign(sk *crypto.ZonePrivate) error { + // get signed data + buf, err := data.Marshal(b.Body) + if err == nil { + // generate signature + b.DerivedKeySig, err = sk.Sign(buf) + } + return err +} + // 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 @@ -193,3 +220,78 @@ func (b *GNSBlock) Verify() (ok bool, err error) { } return b.DerivedKeySig.Verify(buf) } + +// RecordSet ist the GNUnet data structure for a list of resource records +// in a GNSBlock. As part of GNUnet messages, the record set is padded so that +// the binary size of (records||padding) is the smallest power of two. +type RecordSet struct { + Count uint32 `order:"big"` // number of resource records + Records []*ResourceRecord `size:"Count"` // list of resource records + Padding []byte `size:"*"` // padding +} + +// NewRecordSet returns an empty resource record set. +func NewRecordSet() *RecordSet { + return &RecordSet{ + Count: 0, + Records: make([]*ResourceRecord, 0), + Padding: make([]byte, 0), + } +} + +// AddRecord to append a resource record to the set. +func (rs *RecordSet) AddRecord(rec *ResourceRecord) { + rs.Count++ + rs.Records = append(rs.Records, rec) +} + +// SetPadding (re-)calculates and allocates the padding. +func (rs *RecordSet) SetPadding() { + size := 0 + for _, rr := range rs.Records { + size += int(rr.Size) + 20 + } + n := 1 + for n < size { + n <<= 1 + } + rs.Padding = make([]byte, n-size) +} + +// Expire returns the earliest expiration timestamp for the records. +func (rs *RecordSet) Expire() util.AbsoluteTime { + var expires util.AbsoluteTime + for i, rr := range rs.Records { + if i == 0 { + expires = rr.Expire + } else if rr.Expire.Compare(expires) < 0 { + expires = rr.Expire + } + } + return expires +} + +// Bytes returns the binary representation +func (rs *RecordSet) Bytes() []byte { + buf, err := data.Marshal(rs) + if err != nil { + return nil + } + return buf +} + +// ResourceRecord is the GNUnet-specific representation of resource +// records (not to be confused with DNS resource records). +type ResourceRecord struct { + Expire util.AbsoluteTime // Expiration time for the record + Size uint32 `order:"big"` // Number of bytes in 'Data' + RType enums.GNSType `order:"big"` // Type of the GNS/DNS record + Flags enums.GNSFlag `order:"big"` // Flags for the record + Data []byte `size:"Size"` // Record data +} + +// String returns a human-readable representation of the message. +func (r *ResourceRecord) String() string { + return fmt.Sprintf("GNSResourceRecord{type=%s,expire=%s,flags=%d,size=%d}", + r.RType.String(), r.Expire, r.Flags, r.Size) +} diff --git a/src/gnunet/service/dht/blocks/gns_test.go b/src/gnunet/service/dht/blocks/gns_test.go new file mode 100644 index 0000000..260d83b --- /dev/null +++ b/src/gnunet/service/dht/blocks/gns_test.go @@ -0,0 +1,444 @@ +// 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/hex" + "gnunet/crypto" + "gnunet/enums" + "gnunet/util" + "testing" + + "github.com/bfix/gospel/data" +) + +func TestGNSBlock(t *testing.T) { + var ( + ZONEKEY = "000G054G4G3HWZP2WFNVS1XJ4VXWY85G49AVYBZ7TV4EWP5J5V59H5QN40" + LABEL = "@" + + QKEY = []byte{ + 0xb6, 0x48, 0xfd, 0x0c, 0x4a, 0x6c, 0xaa, 0x87, + 0x33, 0x2f, 0xf5, 0x12, 0x90, 0xe4, 0xbd, 0x55, + 0x0f, 0x8c, 0xe7, 0x9b, 0xc9, 0x5b, 0x3a, 0xfb, + 0xbb, 0xe2, 0xd7, 0x33, 0xbc, 0x32, 0xc9, 0x7d, + 0xc5, 0x4a, 0x56, 0x22, 0xbf, 0xfa, 0x49, 0x1a, + 0x60, 0xd6, 0xdb, 0x77, 0x5d, 0x3d, 0x18, 0x99, + 0x5b, 0x4f, 0xc3, 0x7d, 0x86, 0x00, 0x15, 0x76, + 0x42, 0x03, 0x98, 0xcc, 0xdf, 0x83, 0x4d, 0x21, + } + BLK = []byte{ + 0x00, 0x01, 0x00, 0x14, 0xe0, 0x6b, 0xea, 0x2b, + 0x1b, 0xd6, 0xc6, 0x9a, 0xd4, 0x30, 0xa5, 0x0f, + 0x81, 0x16, 0x89, 0xe1, 0x9f, 0xca, 0x1f, 0x86, + 0x3f, 0x83, 0x6e, 0xe6, 0xa7, 0x54, 0x97, 0xde, + 0xf2, 0xc4, 0x2a, 0x84, 0xb6, 0x89, 0xe6, 0x7e, + 0xff, 0x0c, 0xae, 0x84, 0xe6, 0xb1, 0x6c, 0x72, + 0x83, 0x09, 0x68, 0x5b, 0x2f, 0xa2, 0x9f, 0xbe, + 0xfa, 0xef, 0x43, 0x52, 0x20, 0x48, 0xe5, 0x57, + 0x1e, 0x65, 0x21, 0x86, 0xd4, 0x9f, 0x96, 0x51, + 0x4f, 0xa9, 0x6d, 0xa9, 0x98, 0xaa, 0x2d, 0xf6, + 0x92, 0xd7, 0x86, 0x36, 0xc0, 0x84, 0x90, 0x00, + 0x42, 0x2e, 0x4e, 0xc1, 0xaf, 0x6f, 0xe0, 0x7e, + 0x71, 0xe3, 0xc4, 0x0d, 0x00, 0x00, 0x00, 0x10, + 0x00, 0x00, 0x00, 0x0f, 0x00, 0x06, 0x08, 0x00, + 0xb5, 0x99, 0x2a, 0x00, 0xd0, 0x7a, 0x2b, 0x9e, + 0x02, 0x45, 0x54, 0x0d, 0x65, 0x26, 0xa1, 0x05, + 0x80, 0x26, 0xce, 0xc2, 0x70, 0xd5, 0x22, 0x38, + 0x80, 0x9a, 0xed, 0x63, 0x2f, 0x96, 0x60, 0x4d, + 0x02, 0x59, 0xd0, 0x9a, 0x4e, 0x71, 0xfa, 0x30, + 0xd6, 0xf9, 0xf4, 0x84, 0x5d, 0xb8, 0x60, 0xa4, + 0xdf, 0xea, 0x34, 0x06, 0x3f, 0x6f, 0x76, 0x9e, + } + ) + // unmarshal block + blk := new(GNSBlock) + if err := data.Unmarshal(blk, BLK); err != nil { + t.Fatal(err) + } + // Initialize signature + if err := blk.DerivedKeySig.Init(); err != nil { + t.Fatal(err) + } + // assemble query from public zone key and label + zkData, err := util.DecodeStringToBinary(ZONEKEY, 36) + if err != nil { + t.Fatal(err) + } + zk, err := crypto.NewZoneKey(zkData) + if err != nil { + t.Fatal(err) + } + query := NewGNSQuery(zk, LABEL) + + // check query key + if !bytes.Equal(QKEY, query.Key().Data) { + t.Fatal("query key mismatch") + } + + // check derived public key (form zone key and label) + dkey2, _, err := zk.Derive(LABEL, "gns") + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(blk.DerivedKeySig.ZoneKey.Bytes(), dkey2.Bytes()) { + t.Logf("expected: %s\n", hex.EncodeToString(blk.DerivedKeySig.ZoneKey.Bytes())) + t.Logf("got: %s\n", hex.EncodeToString(dkey2.Bytes())) + t.Fatal("key mismatch") + } + + // verify signature + if err = query.Verify(blk); err != nil { + t.Fatal(err) + } + // decrypt payload + if err = query.Decrypt(blk); err != nil { + t.Fatal(err) + } + rrs := new(RecordSet) + if err = data.Unmarshal(rrs, blk.Payload()); err != nil { + t.Fatal(err) + } + t.Logf("RecordSet=%v\n", rrs) + +} + +// TestRecordsetPKEY implements the test case as defined in the GNS draft +// (see section 13. Test vectors, case "PKEY") +func TestRecordsetPKEY(t *testing.T) { + var ( + D = []byte{ + // PKEY private scalar + 0x50, 0xd7, 0xb6, 0x52, 0xa4, 0xef, 0xea, 0xdf, + 0xf3, 0x73, 0x96, 0x90, 0x97, 0x85, 0xe5, 0x95, + 0x21, 0x71, 0xa0, 0x21, 0x78, 0xc8, 0xe7, 0xd4, + 0x50, 0xfa, 0x90, 0x79, 0x25, 0xfa, 0xfd, 0x98, + } + ZKEY = []byte{ + // zone type + 0x00, 0x01, 0x00, 0x00, + // PKEY public key + 0x67, 0x7c, 0x47, 0x7d, 0x2d, 0x93, 0x09, 0x7c, + 0x85, 0xb1, 0x95, 0xc6, 0xf9, 0x6d, 0x84, 0xff, + 0x61, 0xf5, 0x98, 0x2c, 0x2c, 0x4f, 0xe0, 0x2d, + 0x5a, 0x11, 0xfe, 0xdf, 0xb0, 0xc2, 0x90, 0x1f, + } + ZID = "000G0037FH3QTBCK15Y8BCCNRVWPV17ZC7TSGB1C9ZG2TPGHZVFV1GMG3W" + RECSET = &RecordSet{ + Count: 2, + Records: []*ResourceRecord{ + { + Expire: util.AbsoluteTime{ + Val: uint64(14888744139323793), + }, + Size: 4, + RType: 1, + Flags: 0, + Data: []byte{ + 0x01, 0x02, 0x03, 0x04, + }, + }, + { + Expire: util.AbsoluteTime{ + Val: uint64(26147096139323793), + }, + Size: 36, + RType: enums.GNS_TYPE_PKEY, + Flags: 2, + Data: []byte{ + 0x00, 0x01, 0x00, 0x00, + 0x0e, 0x60, 0x1b, 0xe4, 0x2e, 0xb5, 0x7f, 0xb4, + 0x69, 0x76, 0x10, 0xcf, 0x3a, 0x3b, 0x18, 0x34, + 0x7b, 0x65, 0xa3, 0x3f, 0x02, 0x5b, 0x5b, 0x17, + 0x4a, 0xbe, 0xfb, 0x30, 0x80, 0x7b, 0xfe, 0xcf, + }, + }, + }, + Padding: make([]byte, 0), + } + RDATA = []byte{ + 0x00, 0x00, 0x00, 0x02, 0x00, 0x34, 0xe5, 0x3b, + 0xe1, 0x93, 0x79, 0x91, 0x00, 0x00, 0x00, 0x04, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x02, 0x03, 0x04, 0x00, 0x5c, 0xe4, 0xa5, + 0x39, 0x4a, 0xd9, 0x91, 0x00, 0x00, 0x00, 0x24, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, + 0x00, 0x01, 0x00, 0x00, 0x0e, 0x60, 0x1b, 0xe4, + 0x2e, 0xb5, 0x7f, 0xb4, 0x69, 0x76, 0x10, 0xcf, + 0x3a, 0x3b, 0x18, 0x34, 0x7b, 0x65, 0xa3, 0x3f, + 0x02, 0x5b, 0x5b, 0x17, 0x4a, 0xbe, 0xfb, 0x30, + 0x80, 0x7b, 0xfe, 0xcf, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + } + NONCE = []byte{ + 0x67, 0xeb, 0xda, 0x27, 0x00, 0x34, 0xe5, 0x3b, + 0xe1, 0x93, 0x79, 0x91, 0x00, 0x00, 0x00, 0x01, + } + SKEY = []byte{ + 0x55, 0x1f, 0x15, 0x7a, 0xcf, 0x2b, 0xf1, 0xd4, + 0xa9, 0x75, 0x03, 0x69, 0x99, 0xea, 0x7c, 0x82, + 0x86, 0xac, 0xb3, 0x18, 0xf1, 0x49, 0x3e, 0x63, + 0xb5, 0x00, 0x60, 0x3a, 0x9b, 0x02, 0xe3, 0xe4, + } + BDATA = []byte{ + 0x00, 0xe4, 0x83, 0x7e, 0xb5, 0xd0, 0x4f, 0x92, + 0x90, 0x3d, 0xe4, 0xb5, 0x23, 0x4e, 0x8c, 0xca, + 0xc5, 0x73, 0x6c, 0x97, 0x93, 0x37, 0x9a, 0x59, + 0xc3, 0x33, 0x75, 0xfc, 0x89, 0x51, 0xac, 0xa2, + 0xeb, 0x7a, 0xad, 0x06, 0x7b, 0xf9, 0xaf, 0x60, + 0xbf, 0x26, 0x75, 0x86, 0x46, 0xa1, 0x7f, 0x5e, + 0x5c, 0x3b, 0x62, 0x15, 0xf9, 0x40, 0x79, 0x54, + 0x5b, 0x1c, 0x4d, 0x4f, 0x1b, 0x2e, 0xbb, 0x22, + 0xc2, 0xb4, 0xda, 0xd4, 0x41, 0x26, 0x81, 0x7b, + 0x6f, 0x00, 0x15, 0x30, 0xd4, 0x76, 0x40, 0x1d, + 0xd6, 0x7a, 0xc0, 0x14, 0x85, 0x54, 0xe8, 0x06, + 0x35, 0x3d, 0xa9, 0xe4, 0x29, 0x80, 0x79, 0xf3, + 0xe1, 0xb1, 0x69, 0x42, 0xc4, 0x8d, 0x90, 0xc4, + 0x36, 0x0c, 0x61, 0x23, 0x8c, 0x40, 0xd9, 0xd5, + 0x29, 0x11, 0xae, 0xa5, 0x2c, 0xc0, 0x03, 0x7a, + 0xc7, 0x16, 0x0b, 0xb3, 0xcf, 0x5b, 0x2f, 0x4a, + 0x72, 0x2f, 0xd9, 0x6b, + } + LABEL = "test" + ) + + // check zone key pair + prv, err := crypto.NewZonePrivate(enums.GNS_TYPE_PKEY, D) + if err != nil { + t.Fatal(err) + } + zk := prv.Public() + if !bytes.Equal(zk.Bytes(), ZKEY) { + t.Logf("pub = %s\n", hex.EncodeToString(zk.Bytes())) + t.Logf(" != %s\n", hex.EncodeToString(ZKEY)) + t.Fatal("zone key mismatch") + } + zid := zk.ID() + if zid != ZID { + t.Logf("id = %s\n", zid) + t.Logf("ID = %s\n", ZID) + t.Fatal("Zone ID mismatch") + } + + // assemble and check recordset + RECSET.SetPadding() + rdata, err := data.Marshal(RECSET) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(rdata, RDATA) { + t.Logf("rdata = %s\n", hex.EncodeToString(rdata)) + t.Logf("RDATA = %s\n", hex.EncodeToString(RDATA)) + t.Fatal("RDATA mismatch") + } + + // check symmetric keys and nonce + expires := RECSET.Expire() + skey := zk.BlockKey(LABEL, expires) + if !bytes.Equal(skey[32:], NONCE) { + t.Logf("nonce = %s\n", hex.EncodeToString(skey[32:])) + t.Logf("NONCE = %s\n", hex.EncodeToString(NONCE)) + t.Fatal("NONCE mismatch") + } + if !bytes.Equal(skey[:32], SKEY) { + t.Logf("skey = %s\n", hex.EncodeToString(skey[:32])) + t.Logf("SKEY = %s\n", hex.EncodeToString(SKEY)) + t.Fatal("SKEY mismatch") + } + + // check block encryption + bdata, err := zk.Encrypt(rdata, LABEL, expires) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(bdata, BDATA) { + t.Logf("bdata = %s\n", hex.EncodeToString(bdata)) + t.Logf("BDATA = %s\n", hex.EncodeToString(BDATA)) + t.Fatal("BDATA mismatch") + } +} + +// TestRecordsetEDKEY implements the test case as defined in the GNS draft +// (see section 13. Test vectors, case "EDKEY") +func TestRecordsetEDKEY(t *testing.T) { + var ( + SEED = []byte{ + // EDKEY private key (seed) + 0x5a, 0xf7, 0x02, 0x0e, 0xe1, 0x91, 0x60, 0x32, + 0x88, 0x32, 0x35, 0x2b, 0xbc, 0x6a, 0x68, 0xa8, + 0xd7, 0x1a, 0x7c, 0xbe, 0x1b, 0x92, 0x99, 0x69, + 0xa7, 0xc6, 0x6d, 0x41, 0x5a, 0x0d, 0x8f, 0x65, + } + ZKEY = []byte{ + // zone type + 0x00, 0x01, 0x00, 0x14, + // EDKEY public key data + 0x3c, 0xf4, 0xb9, 0x24, 0x03, 0x20, 0x22, 0xf0, + 0xdc, 0x50, 0x58, 0x14, 0x53, 0xb8, 0x5d, 0x93, + 0xb0, 0x47, 0xb6, 0x3d, 0x44, 0x6c, 0x58, 0x45, + 0xcb, 0x48, 0x44, 0x5d, 0xdb, 0x96, 0x68, 0x8f, + } + ZID = "000G051WYJWJ80S04BRDRM2R2H9VGQCKP13VCFA4DHC4BJT88HEXQ5K8HW" + RECSET = &RecordSet{ + Count: 2, + Records: []*ResourceRecord{ + { + Expire: util.AbsoluteTime{ + Val: uint64(2463385894000000), + }, + Size: 4, + RType: 1, + Flags: 0, + Data: []byte{ + 0x01, 0x02, 0x03, 0x04, + }, + }, + { + Expire: util.AbsoluteTime{ + Val: uint64(49556645701000000), + }, + Size: 36, + RType: enums.GNS_TYPE_NICK, + Flags: 2, + Data: []byte{ + 0x4d, 0x79, 0x20, 0x4e, 0x69, 0x63, 0x6b, 0x00, + 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, + 0x6f, 0x6e, 0x20, 0x4e, 0x4f, 0x4e, 0x43, 0x45, + 0x7c, 0x45, 0x58, 0x50, 0x49, 0x52, 0x41, 0x54, + 0x49, 0x4f, 0x4e, 0x3a, + }, + }, + }, + Padding: make([]byte, 0), + } + RDATA = []byte{ + 0x00, 0x00, 0x00, 0x02, 0x00, 0x08, 0xc0, 0x6f, + 0xb9, 0x28, 0x15, 0x80, 0x00, 0x00, 0x00, 0x04, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x02, 0x03, 0x04, 0x00, 0xb0, 0x0f, 0x81, + 0xb7, 0x44, 0x9b, 0x40, 0x00, 0x00, 0x00, 0x24, + 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, + 0x4d, 0x79, 0x20, 0x4e, 0x69, 0x63, 0x6b, 0x00, + 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, + 0x6f, 0x6e, 0x20, 0x4e, 0x4f, 0x4e, 0x43, 0x45, + 0x7c, 0x45, 0x58, 0x50, 0x49, 0x52, 0x41, 0x54, + 0x49, 0x4f, 0x4e, 0x3a, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + } + NONCE = []byte{ + 0x95, 0x4c, 0xb5, 0xd6, 0x31, 0x9f, 0x9e, 0x31, + 0xff, 0x80, 0x4a, 0xe6, 0x83, 0xbc, 0x19, 0x37, + 0x00, 0x08, 0xc0, 0x6f, 0xb9, 0x28, 0x15, 0x80, + } + SKEY = []byte{ + 0x08, 0x34, 0xa3, 0xa3, 0xae, 0x09, 0xcb, 0x3b, + 0xd9, 0x8c, 0xec, 0xdb, 0x47, 0x7c, 0x3b, 0x32, + 0x45, 0xd0, 0xce, 0xda, 0x94, 0x8f, 0x9e, 0xbb, + 0xba, 0x3b, 0x17, 0x91, 0x61, 0x7b, 0xee, 0x69, + } + BDATA = []byte{ + 0x36, 0x07, 0xf8, 0x62, 0xfc, 0xf4, 0xc6, 0xd4, + 0x86, 0x1c, 0x7a, 0x06, 0x08, 0x81, 0x28, 0xbb, + 0x3d, 0x6c, 0xca, 0xe2, 0xb1, 0x4e, 0xf4, 0x25, + 0xe3, 0xd6, 0xbb, 0xd6, 0x27, 0x1a, 0x71, 0xe5, + 0x42, 0x1c, 0x25, 0x1c, 0xfb, 0x5e, 0xb6, 0xd7, + 0xbc, 0x9e, 0x74, 0xb2, 0xe8, 0xc8, 0xd8, 0x6c, + 0xe0, 0x65, 0x37, 0x12, 0x0c, 0x2e, 0xe2, 0x28, + 0x5b, 0x93, 0xc5, 0xaf, 0xb7, 0x79, 0xf9, 0xcf, + 0x50, 0x2e, 0x16, 0xa5, 0xad, 0x30, 0xe6, 0x22, + 0xed, 0x58, 0x92, 0xd2, 0x46, 0xc0, 0x34, 0x11, + 0x70, 0xf0, 0xc5, 0x1c, 0x39, 0x40, 0xab, 0x33, + 0x47, 0xdc, 0x91, 0x56, 0x5f, 0x36, 0x6d, 0xb6, + 0x23, 0x56, 0x73, 0x9a, 0xd8, 0xde, 0x68, 0x21, + 0x12, 0x68, 0xf0, 0xc0, 0x44, 0x00, 0x81, 0xd8, + 0xaf, 0x8a, 0x6e, 0x16, 0x45, 0xa6, 0x92, 0x46, + 0xb4, 0x34, 0xe2, 0xc8, 0x76, 0x9f, 0x00, 0x1b, + 0xd5, 0x1a, 0xb3, 0x73, 0x5e, 0x02, 0xb4, 0x81, + 0xa6, 0x83, 0x0f, 0x00, 0xd2, 0xf6, 0xf3, 0x15, + 0xdf, 0x54, 0x20, 0x90, + } + LABEL = "test" + ) + + // check zone key pair + prv, err := crypto.NewZonePrivate(crypto.ZONE_EDKEY, SEED) + if err != nil { + t.Fatal(err) + } + zk := prv.Public() + if !bytes.Equal(zk.Bytes(), ZKEY) { + t.Logf("zkey = %s\n", hex.EncodeToString(zk.Bytes())) + t.Logf(" != %s\n", hex.EncodeToString(ZKEY)) + t.Fatal("zone key mismatch") + } + zid := zk.ID() + if zid != ZID { + t.Logf("id = %s\n", zid) + t.Logf("ID = %s\n", ZID) + t.Fatal("Zone ID mismatch") + } + + // assemble and check recordset + RECSET.SetPadding() + rdata, err := data.Marshal(RECSET) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(rdata, RDATA) { + t.Logf("rdata = %s\n", hex.EncodeToString(rdata)) + t.Logf("RDATA = %s\n", hex.EncodeToString(RDATA)) + t.Fatal("RDATA mismatch") + } + + // check symmetric keys and nonce + expires := RECSET.Expire() + skey := zk.BlockKey(LABEL, expires) + if !bytes.Equal(skey[32:], NONCE) { + t.Logf("nonce = %s\n", hex.EncodeToString(skey[32:])) + t.Logf("NONCE = %s\n", hex.EncodeToString(NONCE)) + t.Fatal("NONCE mismatch") + } + if !bytes.Equal(skey[:32], SKEY) { + t.Logf("skey = %s\n", hex.EncodeToString(skey[:32])) + t.Logf("SKEY = %s\n", hex.EncodeToString(SKEY)) + t.Fatal("SKEY mismatch") + } + + // check block encryption + bdata, err := zk.Encrypt(rdata, LABEL, expires) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(bdata, BDATA) { + t.Logf("bdata = %s\n", hex.EncodeToString(bdata)) + t.Logf("BDATA = %s\n", hex.EncodeToString(BDATA)) + t.Fatal("BDATA mismatch") + } +} diff --git a/src/gnunet/service/dht/module.go b/src/gnunet/service/dht/module.go index 718b730..9f3aaa0 100644 --- a/src/gnunet/service/dht/module.go +++ b/src/gnunet/service/dht/module.go @@ -248,25 +248,10 @@ func (m *Module) Get(ctx context.Context, query blocks.Query) <-chan blocks.Bloc // Put a block into the DHT ["dht:put"] func (m *Module) Put(ctx context.Context, query blocks.Query, block blocks.Block) error { - // get additional query parameters - expire, ok := util.GetParam[util.AbsoluteTime](query.Params(), "expire") - if !ok { - expire = util.AbsoluteTimeNever() - } // assemble a new PUT message - msg := message.NewDHTP2PPutMsg() - msg.BType = query.Type() + msg := message.NewDHTP2PPutMsg(block) msg.Flags = query.Flags() - msg.HopCount = 0 - msg.PeerFilter = blocks.NewPeerFilter() - msg.ReplLvl = uint16(m.cfg.Routing.ReplLevel) - msg.Expire = expire - msg.Block = block.Bytes() msg.Key = query.Key().Clone() - msg.TruncOrigin = nil - msg.PutPath = nil - msg.LastSig = nil - msg.MsgSize += uint16(len(msg.Block)) // send message self := m.core.PeerID() diff --git a/src/gnunet/service/gns/block_handler.go b/src/gnunet/service/gns/block_handler.go index a0e7874..b00fac2 100644 --- a/src/gnunet/service/gns/block_handler.go +++ b/src/gnunet/service/gns/block_handler.go @@ -19,19 +19,21 @@ package gns import ( + "crypto/sha256" "encoding/hex" "fmt" "gnunet/crypto" "gnunet/enums" - "gnunet/message" + "gnunet/service/dht/blocks" + "gnunet/service/gns/rr" "gnunet/util" "github.com/bfix/gospel/logger" ) // HdlrInst is the type for functions that instantiate custom block handlers. -type HdlrInst func(*message.ResourceRecord, []string) (BlockHandler, error) +type HdlrInst func(*blocks.ResourceRecord, []string) (BlockHandler, error) // Error codes var ( @@ -70,7 +72,7 @@ type BlockHandler interface { // processing. The handler can inspect the remaining labels in a path // if required. The method returns an error if a record is not accepted // by the block handler (RR not of required type). - AddRecord(rr *message.ResourceRecord, labels []string) error + AddRecord(rr *blocks.ResourceRecord, labels []string) error // Coexist checks if a custom block handler can co-exist with other // resource records in the same block. 'cm' maps the resource type @@ -80,7 +82,7 @@ type BlockHandler interface { // Records returns a list of RR of the given types associated with // the custom handler - Records(kind RRTypeList) *message.RecordSet + Records(kind RRTypeList) *blocks.RecordSet // Name returns the human-readable name of the handler Name() string @@ -108,7 +110,7 @@ type BlockHandlerList struct { // NewBlockHandlerList instantiates an a list of active block handlers // for a given set of records (GNS block). -func NewBlockHandlerList(records []*message.ResourceRecord, labels []string) (*BlockHandlerList, []*message.ResourceRecord, error) { +func NewBlockHandlerList(records []*blocks.ResourceRecord, labels []string) (*BlockHandlerList, []*blocks.ResourceRecord, error) { // initialize block handler list hl := &BlockHandlerList{ list: make(map[enums.GNSType]BlockHandler), @@ -116,19 +118,19 @@ func NewBlockHandlerList(records []*message.ResourceRecord, labels []string) (*B } // first pass: build list of shadow records in this block - shadows := make([]*message.ResourceRecord, 0) + shadows := make([]*blocks.ResourceRecord, 0) for _, rec := range records { // filter out shadow records... - if (int(rec.Flags) & enums.GNS_FLAG_SHADOW) != 0 { + if (rec.Flags & enums.GNS_FLAG_SHADOW) != 0 { shadows = append(shadows, rec) } } // second pass: normalize block by filtering out expired records (and // replacing them with shadow records if available - active := make([]*message.ResourceRecord, 0) + active := make([]*blocks.ResourceRecord, 0) for _, rec := range records { // don't process shadow records again - if (int(rec.Flags) & enums.GNS_FLAG_SHADOW) != 0 { + if (rec.Flags & enums.GNS_FLAG_SHADOW) != 0 { continue } // check for expired record @@ -137,7 +139,7 @@ func NewBlockHandlerList(records []*message.ResourceRecord, labels []string) (*B for _, shadow := range shadows { if shadow.RType == rec.RType && !shadow.Expire.Expired() { // deliver un-expired shadow record instead. - shadow.Flags &^= uint32(enums.GNS_FLAG_SHADOW) + shadow.Flags &^= enums.GNS_FLAG_SHADOW active = append(active, shadow) } } @@ -149,11 +151,11 @@ func NewBlockHandlerList(records []*message.ResourceRecord, labels []string) (*B // Third pass: Traverse active list and build list of handler instances. for _, rec := range active { // update counter map for non-supplemental records - if (int(rec.Flags) & enums.GNS_FLAG_SUPPL) != 0 { + if (rec.Flags & enums.GNS_FLAG_SUPPL) != 0 { logger.Printf(logger.DBG, "[gns] handler_list: skip %v\n", rec) continue } - rrType := enums.GNSType(rec.RType) + rrType := rec.RType hl.counts.Add(rrType) // check for custom handler type @@ -205,7 +207,7 @@ func (hl *BlockHandlerList) GetHandler(types ...enums.GNSType) BlockHandler { } // FinalizeRecord post-processes records -func (hl *BlockHandlerList) FinalizeRecord(rec *message.ResourceRecord) *message.ResourceRecord { +func (hl *BlockHandlerList) FinalizeRecord(rec *blocks.ResourceRecord) *blocks.ResourceRecord { // no implementation yet return rec } @@ -216,13 +218,13 @@ func (hl *BlockHandlerList) FinalizeRecord(rec *message.ResourceRecord) *message // ZoneKeyHandler implementing the BlockHandler interface type ZoneKeyHandler struct { - ztype uint32 // zone type - zkey *crypto.ZoneKey // Zone key - rec *message.ResourceRecord // associated recource record + ztype enums.GNSType // zone type + zkey *crypto.ZoneKey // Zone key + rec *blocks.ResourceRecord // associated recource record } // NewZoneHandler returns a new BlockHandler instance -func NewZoneHandler(rec *message.ResourceRecord, labels []string) (BlockHandler, error) { +func NewZoneHandler(rec *blocks.ResourceRecord, labels []string) (BlockHandler, error) { // check if we have an implementation for the zone type if crypto.GetImplementation(rec.RType) == nil { return nil, ErrInvalidRecordType @@ -240,7 +242,7 @@ func NewZoneHandler(rec *message.ResourceRecord, labels []string) (BlockHandler, } // AddRecord inserts a PKEY record into the handler. -func (h *ZoneKeyHandler) AddRecord(rec *message.ResourceRecord, labels []string) (err error) { +func (h *ZoneKeyHandler) AddRecord(rec *blocks.ResourceRecord, labels []string) (err error) { // check record type if rec.RType != h.ztype { return ErrInvalidRecordType @@ -266,8 +268,8 @@ func (h *ZoneKeyHandler) Coexist(cm util.Counter[enums.GNSType]) bool { } // Records returns a list of RR of the given type associated with this handler -func (h *ZoneKeyHandler) Records(kind RRTypeList) *message.RecordSet { - rs := message.NewRecordSet() +func (h *ZoneKeyHandler) Records(kind RRTypeList) *blocks.RecordSet { + rs := blocks.NewRecordSet() if kind.HasType(enums.GNS_TYPE_PKEY) { rs.AddRecord(h.rec) } @@ -285,20 +287,20 @@ func (h *ZoneKeyHandler) Name() string { // Gns2DnsHandler implementing the BlockHandler interface type Gns2DnsHandler struct { - Query string // DNS query name - Servers []string // DNS servers to ask - recs []*message.ResourceRecord // list of rersource records + Query string // DNS query name + Servers []string // DNS servers to ask + recs []*blocks.ResourceRecord // list of rersource records } // NewGns2DnsHandler returns a new BlockHandler instance -func NewGns2DnsHandler(rec *message.ResourceRecord, labels []string) (BlockHandler, error) { - if enums.GNSType(rec.RType) != enums.GNS_TYPE_GNS2DNS { +func NewGns2DnsHandler(rec *blocks.ResourceRecord, labels []string) (BlockHandler, error) { + if rec.RType != enums.GNS_TYPE_GNS2DNS { return nil, ErrInvalidRecordType } h := &Gns2DnsHandler{ Query: "", Servers: make([]string, 0), - recs: make([]*message.ResourceRecord, 0), + recs: make([]*blocks.ResourceRecord, 0), } if err := h.AddRecord(rec, labels); err != nil { return nil, err @@ -307,8 +309,8 @@ func NewGns2DnsHandler(rec *message.ResourceRecord, labels []string) (BlockHandl } // AddRecord inserts a GNS2DNS record into the handler. -func (h *Gns2DnsHandler) AddRecord(rec *message.ResourceRecord, labels []string) error { - if enums.GNSType(rec.RType) != enums.GNS_TYPE_GNS2DNS { +func (h *Gns2DnsHandler) AddRecord(rec *blocks.ResourceRecord, labels []string) error { + if rec.RType != enums.GNS_TYPE_GNS2DNS { return ErrInvalidRecordType } logger.Printf(logger.DBG, "[gns] GNS2DNS data: %s\n", hex.EncodeToString(rec.Data)) @@ -341,8 +343,8 @@ func (h *Gns2DnsHandler) Coexist(cm util.Counter[enums.GNSType]) bool { } // Records returns a list of RR of the given type associated with this handler -func (h *Gns2DnsHandler) Records(kind RRTypeList) *message.RecordSet { - rs := message.NewRecordSet() +func (h *Gns2DnsHandler) Records(kind RRTypeList) *blocks.RecordSet { + rs := blocks.NewRecordSet() if kind.HasType(enums.GNS_TYPE_GNS2DNS) { for _, rec := range h.recs { rs.AddRecord(rec) @@ -360,14 +362,21 @@ func (h *Gns2DnsHandler) Name() string { // BOX handler //---------------------------------------------------------------------- +// Box record for handler logic +type Box struct { + rr.BOX + key string // map key for box instance + rec *blocks.ResourceRecord // originating RR +} + // BoxHandler implementing the BlockHandler interface type BoxHandler struct { boxes map[string]*Box // map of found boxes } // NewBoxHandler returns a new BlockHandler instance -func NewBoxHandler(rec *message.ResourceRecord, labels []string) (BlockHandler, error) { - if enums.GNSType(rec.RType) != enums.GNS_TYPE_BOX { +func NewBoxHandler(rec *blocks.ResourceRecord, labels []string) (BlockHandler, error) { + if rec.RType != enums.GNS_TYPE_BOX { return nil, ErrInvalidRecordType } h := &BoxHandler{ @@ -380,8 +389,8 @@ func NewBoxHandler(rec *message.ResourceRecord, labels []string) (BlockHandler, } // AddRecord inserts a BOX record into the handler. -func (h *BoxHandler) AddRecord(rec *message.ResourceRecord, labels []string) error { - if enums.GNSType(rec.RType) != enums.GNS_TYPE_BOX { +func (h *BoxHandler) AddRecord(rec *blocks.ResourceRecord, labels []string) error { + if rec.RType != enums.GNS_TYPE_BOX { return ErrInvalidRecordType } logger.Printf(logger.DBG, "[box-rr] for labels %v\n", labels) @@ -395,7 +404,12 @@ func (h *BoxHandler) AddRecord(rec *message.ResourceRecord, labels []string) err return nil } // (3) check of "svc" and "proto" match values in the BOX - box := NewBox(rec) + hsh := sha256.Sum256(rec.Data) + box := &Box{ + BOX: *rr.NewBOX(rec.Data), + key: hex.EncodeToString(hsh[:8]), + rec: rec, + } if box.Matches(labels) { logger.Println(logger.DBG, "[box-rr] MATCH -- adding record") h.boxes[box.key] = box @@ -411,12 +425,12 @@ func (h *BoxHandler) Coexist(cm util.Counter[enums.GNSType]) bool { } // Records returns a list of RR of the given type associated with this handler -func (h *BoxHandler) Records(kind RRTypeList) *message.RecordSet { - rs := message.NewRecordSet() +func (h *BoxHandler) Records(kind RRTypeList) *blocks.RecordSet { + rs := blocks.NewRecordSet() for _, box := range h.boxes { - if kind.HasType(enums.GNSType(box.Type)) { + if kind.HasType(box.Type) { // valid box found: assemble new resource record. - rr := new(message.ResourceRecord) + rr := new(blocks.ResourceRecord) rr.Expire = box.rec.Expire rr.Flags = box.rec.Flags rr.RType = box.Type @@ -440,12 +454,12 @@ func (h *BoxHandler) Name() string { // LehoHandler implementing the BlockHandler interface type LehoHandler struct { name string - rec *message.ResourceRecord + rec *blocks.ResourceRecord } // NewLehoHandler returns a new BlockHandler instance -func NewLehoHandler(rec *message.ResourceRecord, labels []string) (BlockHandler, error) { - if enums.GNSType(rec.RType) != enums.GNS_TYPE_LEHO { +func NewLehoHandler(rec *blocks.ResourceRecord, labels []string) (BlockHandler, error) { + if rec.RType != enums.GNS_TYPE_LEHO { return nil, ErrInvalidRecordType } h := &LehoHandler{ @@ -458,8 +472,8 @@ func NewLehoHandler(rec *message.ResourceRecord, labels []string) (BlockHandler, } // AddRecord inserts a LEHO record into the handler. -func (h *LehoHandler) AddRecord(rec *message.ResourceRecord, labels []string) error { - if enums.GNSType(rec.RType) != enums.GNS_TYPE_LEHO { +func (h *LehoHandler) AddRecord(rec *blocks.ResourceRecord, labels []string) error { + if rec.RType != enums.GNS_TYPE_LEHO { return ErrInvalidRecordType } h.name = string(rec.Data) @@ -475,8 +489,8 @@ func (h *LehoHandler) Coexist(cm util.Counter[enums.GNSType]) bool { } // Records returns a list of RR of the given type associated with this handler -func (h *LehoHandler) Records(kind RRTypeList) *message.RecordSet { - rs := message.NewRecordSet() +func (h *LehoHandler) Records(kind RRTypeList) *blocks.RecordSet { + rs := blocks.NewRecordSet() if kind.HasType(enums.GNS_TYPE_LEHO) { rs.AddRecord(h.rec) } @@ -495,12 +509,12 @@ func (h *LehoHandler) Name() string { // CnameHandler implementing the BlockHandler interface type CnameHandler struct { name string - rec *message.ResourceRecord + rec *blocks.ResourceRecord } // NewCnameHandler returns a new BlockHandler instance -func NewCnameHandler(rec *message.ResourceRecord, labels []string) (BlockHandler, error) { - if enums.GNSType(rec.RType) != enums.GNS_TYPE_DNS_CNAME { +func NewCnameHandler(rec *blocks.ResourceRecord, labels []string) (BlockHandler, error) { + if rec.RType != enums.GNS_TYPE_DNS_CNAME { return nil, ErrInvalidRecordType } h := &CnameHandler{ @@ -513,8 +527,8 @@ func NewCnameHandler(rec *message.ResourceRecord, labels []string) (BlockHandler } // AddRecord inserts a CNAME record into the handler. -func (h *CnameHandler) AddRecord(rec *message.ResourceRecord, labels []string) error { - if enums.GNSType(rec.RType) != enums.GNS_TYPE_DNS_CNAME { +func (h *CnameHandler) AddRecord(rec *blocks.ResourceRecord, labels []string) error { + if rec.RType != enums.GNS_TYPE_DNS_CNAME { return ErrInvalidRecordType } if h.rec != nil { @@ -533,8 +547,8 @@ func (h *CnameHandler) Coexist(cm util.Counter[enums.GNSType]) bool { } // Records returns a list of RR of the given type associated with this handler -func (h *CnameHandler) Records(kind RRTypeList) *message.RecordSet { - rs := message.NewRecordSet() +func (h *CnameHandler) Records(kind RRTypeList) *blocks.RecordSet { + rs := blocks.NewRecordSet() if kind.HasType(enums.GNS_TYPE_DNS_CNAME) { rs.AddRecord(h.rec) } @@ -552,12 +566,12 @@ func (h *CnameHandler) Name() string { // VpnHandler implementing the BlockHandler interface type VpnHandler struct { - rec *message.ResourceRecord + rec *blocks.ResourceRecord } // NewVpnHandler returns a new BlockHandler instance -func NewVpnHandler(rec *message.ResourceRecord, labels []string) (BlockHandler, error) { - if enums.GNSType(rec.RType) != enums.GNS_TYPE_VPN { +func NewVpnHandler(rec *blocks.ResourceRecord, labels []string) (BlockHandler, error) { + if rec.RType != enums.GNS_TYPE_VPN { return nil, ErrInvalidRecordType } h := &VpnHandler{} @@ -568,8 +582,8 @@ func NewVpnHandler(rec *message.ResourceRecord, labels []string) (BlockHandler, } // AddRecord inserts a VPN record into the handler. -func (h *VpnHandler) AddRecord(rec *message.ResourceRecord, labels []string) error { - if enums.GNSType(rec.RType) != enums.GNS_TYPE_VPN { +func (h *VpnHandler) AddRecord(rec *blocks.ResourceRecord, labels []string) error { + if rec.RType != enums.GNS_TYPE_VPN { return ErrInvalidRecordType } if h.rec != nil { @@ -587,8 +601,8 @@ func (h *VpnHandler) Coexist(cm util.Counter[enums.GNSType]) bool { } // Records returns a list of RR of the given type associated with this handler -func (h *VpnHandler) Records(kind RRTypeList) *message.RecordSet { - rs := message.NewRecordSet() +func (h *VpnHandler) Records(kind RRTypeList) *blocks.RecordSet { + rs := blocks.NewRecordSet() if kind.HasType(enums.GNS_TYPE_VPN) { rs.AddRecord(h.rec) } diff --git a/src/gnunet/service/gns/box.go b/src/gnunet/service/gns/box.go deleted file mode 100644 index f97471e..0000000 --- a/src/gnunet/service/gns/box.go +++ /dev/null @@ -1,169 +0,0 @@ -// This file is part of gnunet-go, a GNUnet-implementation in Golang. -// Copyright (C) 2019-2022 Bernd Fix >Y< -// -// gnunet-go is free software: you can redistribute it and/or modify it -// under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, -// or (at your option) any later version. -// -// gnunet-go is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -// -// SPDX-License-Identifier: AGPL3.0-or-later - -package gns - -import ( - "encoding/hex" - "strconv" - "strings" - - "gnunet/message" - - "github.com/bfix/gospel/data" - "github.com/bfix/gospel/logger" -) - -// Box is an encapsulated RR for special names -type Box struct { - Proto uint16 `order:"big"` // Protcol identifier - Svc uint16 `order:"big"` // Service identifier - Type uint32 `order:"big"` // Type of embedded RR - RR []byte `size:"*"` // embedded RR - - // transient attributes (not serialized) - key string // map key for box instance - rec *message.ResourceRecord // originating RR -} - -// NewBox creates a new box instance from a BOX resource record. -func NewBox(rec *message.ResourceRecord) *Box { - b := new(Box) - if err := data.Unmarshal(b, rec.Data); err != nil { - logger.Printf(logger.ERROR, "[gns] Can't unmarshal BOX") - return nil - } - b.key = hex.EncodeToString(rec.Data[:8]) - b.rec = rec - return b -} - -// Matches verifies that the remaining labels comply with the values -// in the BOX record. -func (b *Box) Matches(labels []string) bool { - // resolve protocol and service names - proto, protoName := GetProtocol(labels[0]) - svc, _ := GetService(labels[1], protoName) - // no match on invalid resolution - if proto == 0 || svc == 0 { - return false - } - // check for matching values in box - return proto == b.Proto && svc == b.Svc -} - -//---------------------------------------------------------------------- -// helper functions - -// list of handled protocols in BOX records -var protocols = map[string]int{ - "icmp": 1, - "igmp": 2, - "tcp": 6, - "udp": 17, - "ipv6-icmp": 58, -} - -// GetProtocol returns the protocol number and name for a given name. The -// name can be an integer value (e.g. "_6" for "tcp") or a mnemonic name -// (e.g. like "_tcp"). -func GetProtocol(name string) (uint16, string) { - // check for required prefix - if name[0] != '_' { - return 0, "" - } - name = strings.ToLower(name[1:]) - - // if label is an integer value it is the protocol number - if val, err := strconv.Atoi(name); err == nil { - // check for valid number (reverse protocol lookup) - for label, id := range protocols { - if id == val { - // return found entry - return uint16(val), label - } - } - // number out of range - return 0, "" - } - // try to resolve via protocol map - if id, ok := protocols[name]; ok { - return uint16(id), name - } - // resolution failed - return 0, "" -} - -// list of services (per protocol) handled in BOX records -var services = map[string]map[string]int{ - "udp": { - "domain": 53, - }, - "tcp": { - "ftp": 21, - "ftps": 990, - "gopher": 70, - "http": 80, - "https": 443, - "imap2": 143, - "imap3": 220, - "imaps": 993, - "pop3": 110, - "pop3s": 995, - "smtp": 25, - "ssh": 22, - "telnet": 23, - }, -} - -// GetService returns the port number and the name of a service (with given -// protocol). The name can be an integer value (e.g. "_443" for "https") or -// a mnemonic name (e.g. like "_https"). -func GetService(name, proto string) (uint16, string) { - // check for required prefix - if name[0] != '_' { - return 0, "" - } - name = strings.ToLower(name[1:]) - - // get list of services for given protocol - svcs, ok := services[proto] - if !ok { - // no services available for this protocol - return 0, "" - } - - // if label is an integer value it is the port number - if val, err := strconv.Atoi(name); err == nil { - // check for valid number (reverse service lookup) - for label, id := range svcs { - if id == val { - // return found entry - return uint16(val), label - } - } - // number out of range - return 0, "" - } - // try to resolve via services map - if id, ok := svcs[name]; ok { - return uint16(id), name - } - // resolution failed - return 0, "" -} diff --git a/src/gnunet/service/gns/dns.go b/src/gnunet/service/gns/dns.go index 5942b94..32c71e9 100644 --- a/src/gnunet/service/gns/dns.go +++ b/src/gnunet/service/gns/dns.go @@ -27,7 +27,7 @@ import ( "gnunet/crypto" "gnunet/enums" - "gnunet/message" + "gnunet/service/dht/blocks" "gnunet/util" "github.com/bfix/gospel/logger" @@ -116,7 +116,7 @@ func DNSNameFromBytes(b []byte, offset int) (int, string) { } // QueryDNS queries the specified DNS server for a given name and expected result types. -func QueryDNS(id int, name string, server net.IP, kind RRTypeList) *message.RecordSet { +func QueryDNS(id int, name string, server net.IP, kind RRTypeList) *blocks.RecordSet { // get default nameserver if not defined. if server == nil { server = net.IPv4(8, 8, 8, 8) @@ -161,7 +161,7 @@ func QueryDNS(id int, name string, server net.IP, kind RRTypeList) *message.Reco logger.Printf(logger.ERROR, "[dns][%d] No results\n", id) return nil } - set := message.NewRecordSet() + set := blocks.NewRecordSet() for _, record := range in.Answer { // check if answer record is of requested type if kind.HasType(enums.GNSType(record.Header().Rrtype)) { @@ -174,11 +174,11 @@ func QueryDNS(id int, name string, server net.IP, kind RRTypeList) *message.Reco } // create a new GNS resource record - rr := new(message.ResourceRecord) + rr := new(blocks.ResourceRecord) expires := time.Now().Add(time.Duration(record.Header().Ttl) * time.Second) rr.Expire = util.NewAbsoluteTime(expires) rr.Flags = 0 - rr.RType = uint32(record.Header().Rrtype) + rr.RType = enums.GNSType(record.Header().Rrtype) rr.Size = uint32(record.Header().Rdlength) rr.Data = make([]byte, rr.Size) @@ -210,11 +210,11 @@ func (m *Module) ResolveDNS( servers []string, kind RRTypeList, zkey *crypto.ZoneKey, - depth int) (set *message.RecordSet, err error) { + depth int) (set *blocks.RecordSet, err error) { // start DNS queries concurrently logger.Printf(logger.DBG, "[dns] Resolution of '%s' starting...\n", name) - res := make(chan *message.RecordSet) + res := make(chan *blocks.RecordSet) running := 0 for _, srv := range servers { // check if srv is an IPv4/IPv6 address @@ -230,7 +230,7 @@ func (m *Module) ResolveDNS( // traverse resource records for 'A' and 'AAAA' records. rec_loop: for _, rec := range set.Records { - switch enums.GNSType(rec.RType) { + switch rec.RType { case enums.GNS_TYPE_DNS_AAAA: addr = net.IP(rec.Data) // we prefer IPv6 diff --git a/src/gnunet/service/gns/module.go b/src/gnunet/service/gns/module.go index f12a89a..37bbfc5 100644 --- a/src/gnunet/service/gns/module.go +++ b/src/gnunet/service/gns/module.go @@ -27,7 +27,6 @@ import ( "gnunet/core" "gnunet/crypto" "gnunet/enums" - "gnunet/message" "gnunet/service" "gnunet/service/dht/blocks" "gnunet/service/revocation" @@ -102,9 +101,11 @@ func NewModule(ctx context.Context, c *core.Core) (m *Module) { m = &Module{ ModuleImpl: *service.NewModuleImpl(), } - // register as listener for core events - listener := m.ModuleImpl.Run(ctx, m.event, m.Filter(), 0, nil) - c.Register("gns", listener) + if c != nil { + // register as listener for core events + listener := m.ModuleImpl.Run(ctx, m.event, m.Filter(), 0, nil) + c.Register("gns", listener) + } return } @@ -152,7 +153,7 @@ func (m *Module) Resolve( zkey *crypto.ZoneKey, kind RRTypeList, mode int, - depth int) (set *message.RecordSet, err error) { + depth int) (set *blocks.RecordSet, err error) { // check for recursion depth if depth > config.Cfg.GNS.MaxDepth { @@ -178,7 +179,7 @@ func (m *Module) ResolveAbsolute( labels []string, kind RRTypeList, mode int, - depth int) (set *message.RecordSet, err error) { + depth int) (set *blocks.RecordSet, err error) { // get the zone key for the TLD zkey := m.GetZoneKey(labels[0]) @@ -189,7 +190,7 @@ func (m *Module) ResolveAbsolute( } // check if zone key has been revoked var valid bool - set = message.NewRecordSet() + set = blocks.NewRecordSet() if valid, err = m.RevocationQuery(ctx, zkey); err != nil || !valid { return } @@ -208,12 +209,12 @@ func (m *Module) ResolveRelative( zkey *crypto.ZoneKey, kind RRTypeList, mode int, - depth int) (set *message.RecordSet, err error) { + depth int) (set *blocks.RecordSet, err error) { // Process all names in sequence var ( - records []*message.ResourceRecord // final resource records from resolution - hdlrs *BlockHandlerList // list of block handlers in final step + records []*blocks.ResourceRecord // final resource records from resolution + hdlrs *BlockHandlerList // list of block handlers in final step ) for ; len(labels) > 0; labels = labels[1:] { logger.Printf(logger.DBG, "[gns] ResolveRelative '%s' in '%s'\n", labels[0], util.EncodeBinaryToString(zkey.Bytes())) @@ -229,7 +230,7 @@ func (m *Module) ResolveRelative( // if we have no results at this point, return NXDOMAIN if block == nil { // return record set with no entries as signal for NXDOMAIN - set = message.NewRecordSet() + set = blocks.NewRecordSet() return } mode = enums.GNS_LO_DEFAULT @@ -270,7 +271,7 @@ func (m *Module) ResolveRelative( var valid bool if valid, err = m.RevocationQuery(ctx, inst.zkey); err != nil || !valid { // revoked key -> no results! - records = make([]*message.ResourceRecord, 0) + records = make([]*blocks.ResourceRecord, 0) break } } else if hdlr := hdlrs.GetHandler(enums.GNS_TYPE_GNS2DNS); hdlr != nil { @@ -338,10 +339,10 @@ func (m *Module) ResolveRelative( } // Assemble resulting resource record set by filtering for requested types. // Records might get transformed by active block handlers. - set = message.NewRecordSet() + set = blocks.NewRecordSet() for _, rec := range records { // is this the record type we are looking for? - if kind.HasType(enums.GNSType(rec.RType)) { + if kind.HasType(rec.RType) { // add it to the result if rec = hdlrs.FinalizeRecord(rec); rec != nil { set.AddRecord(rec) @@ -364,7 +365,7 @@ func (m *Module) ResolveRelative( // asking for explicitly. if set.Count > 0 { for _, rec := range records { - if !kind.HasType(enums.GNSType(rec.RType)) && (int(rec.Flags)&enums.GNS_FLAG_SUPPL) != 0 { + if !kind.HasType(rec.RType) && (rec.Flags&enums.GNS_FLAG_SUPPL) != 0 { set.AddRecord(rec) } } @@ -383,7 +384,7 @@ func (m *Module) ResolveUnknown( labels []string, zkey *crypto.ZoneKey, kind RRTypeList, - depth int) (set *message.RecordSet, err error) { + depth int) (set *blocks.RecordSet, err error) { // relative GNS-based server name? if strings.HasSuffix(name, ".+") { @@ -471,11 +472,11 @@ func (m *Module) Lookup( } // newLEHORecord creates a new supplemental GNS record of type LEHO. -func (m *Module) newLEHORecord(name string, expires util.AbsoluteTime) *message.ResourceRecord { - rr := new(message.ResourceRecord) +func (m *Module) newLEHORecord(name string, expires util.AbsoluteTime) *blocks.ResourceRecord { + rr := new(blocks.ResourceRecord) rr.Expire = expires - rr.Flags = uint32(enums.GNS_FLAG_SUPPL) - rr.RType = uint32(enums.GNS_TYPE_LEHO) + rr.Flags = enums.GNS_FLAG_SUPPL + rr.RType = enums.GNS_TYPE_LEHO rr.Size = uint32(len(name) + 1) rr.Data = make([]byte, rr.Size) copy(rr.Data, []byte(name)) @@ -484,9 +485,9 @@ func (m *Module) newLEHORecord(name string, expires util.AbsoluteTime) *message. } // Records returns the list of resource records from binary data. -func (m *Module) records(buf []byte) ([]*message.ResourceRecord, error) { +func (m *Module) records(buf []byte) ([]*blocks.ResourceRecord, error) { // parse data into record set - rs := message.NewRecordSet() + rs := blocks.NewRecordSet() if err := data.Unmarshal(rs, buf); err != nil { return nil, err } diff --git a/src/gnunet/service/gns/rr/coexist.go b/src/gnunet/service/gns/rr/coexist.go new file mode 100644 index 0000000..1037b11 --- /dev/null +++ b/src/gnunet/service/gns/rr/coexist.go @@ -0,0 +1,146 @@ +// 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 rr + +import ( + "errors" + "gnunet/enums" + "gnunet/util" + + "github.com/bfix/gospel/data" +) + +// RR interface for resource records +type RR interface { + // Coexist checks if a new resource record could coexist with given set + // of records under a label (can be called with a nil receiver) + Coexist(list []*enums.GNSSpec, label string) (bool, enums.GNSFlag) + + // ToMap adds the RR attributes to a stringed map + ToMap(map[string]string, string) +} + +// CanCoexist checks if a (new) resource record of type 't' can coexist +// with a given set of resource records. If ok is true, it can enforce +// flags for the new record. +func CanCoexist(t enums.GNSType, list []*enums.GNSSpec, label string) (ok bool, forced enums.GNSFlag) { + rr := NilRR(t) + if rr == nil { + return true, 0 + } + // check if new record against list + if ok, forced = rr.Coexist(list, label); !ok { + return + } + // now check if each existing record can coexists with a modified list + // swpping new record and tested record. + testList := util.Clone(list) + eNew := &enums.GNSSpec{ + Type: t, + Flags: forced, + } + for i, e := range testList { + testList[i] = eNew + ok, forced = NilRR(e.Type).Coexist(testList, label) + if !ok { + return + } + eNew.Flags |= forced + testList[i] = e + } + // all checks passed + forced = eNew.Flags + return +} + +// ParseRR returns a RR instance from data for given type +func ParseRR(t enums.GNSType, buf []byte) (rr RR, err error) { + // get record instance + if rr = NewRR(t); rr == nil { + err = errors.New("parse RR failed") + return + } + // reconstruct record + err = data.Unmarshal(rr, buf) + return +} + +// NewRR returns a new RR instance of given type +func NewRR(t enums.GNSType) RR { + switch t { + case enums.GNS_TYPE_PKEY: + return new(PKEY) + case enums.GNS_TYPE_EDKEY: + return new(EDKEY) + case enums.GNS_TYPE_REDIRECT: + return (*REDIRECT)(nil) + case enums.GNS_TYPE_NICK: + return new(NICK) + case enums.GNS_TYPE_LEHO: + return new(LEHO) + case enums.GNS_TYPE_GNS2DNS: + return new(GNS2DNS) + case enums.GNS_TYPE_BOX: + return new(BOX) + case enums.GNS_TYPE_DNS_CNAME: + return new(CNAME) + case enums.GNS_TYPE_DNS_A: + return new(DNSA) + case enums.GNS_TYPE_DNS_AAAA: + return new(DNSAAAA) + case enums.GNS_TYPE_DNS_MX: + return new(MX) + case enums.GNS_TYPE_DNS_TXT: + return new(TXT) + } + return nil +} + +// NilRR returns a typed nil reference to a RR that can be used to +// call type methods that allow a nil receiver. +func NilRR(t enums.GNSType) RR { + switch t { + case enums.GNS_TYPE_PKEY: + return (*PKEY)(nil) + case enums.GNS_TYPE_EDKEY: + return (*EDKEY)(nil) + case enums.GNS_TYPE_REDIRECT: + return (*REDIRECT)(nil) + case enums.GNS_TYPE_NICK: + return (*NICK)(nil) + case enums.GNS_TYPE_LEHO: + return (*LEHO)(nil) + case enums.GNS_TYPE_GNS2DNS: + return (*GNS2DNS)(nil) + case enums.GNS_TYPE_BOX: + return (*BOX)(nil) + case enums.GNS_TYPE_DNS_CNAME: + return (*CNAME)(nil) + case enums.GNS_TYPE_DNS_A: + return (*DNSA)(nil) + case enums.GNS_TYPE_DNS_AAAA: + return (*DNSAAAA)(nil) + case enums.GNS_TYPE_DNS_MX: + return (*MX)(nil) + case enums.GNS_TYPE_DNS_TXT: + return (*TXT)(nil) + } + // return untyped nil + return nil +} diff --git a/src/gnunet/service/gns/rr/dns.go b/src/gnunet/service/gns/rr/dns.go new file mode 100644 index 0000000..a98b529 --- /dev/null +++ b/src/gnunet/service/gns/rr/dns.go @@ -0,0 +1,119 @@ +// 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 rr + +import ( + "fmt" + "gnunet/enums" + "net" +) + +//---------------------------------------------------------------------- +// DNS-related resource records +//---------------------------------------------------------------------- + +// DNS CNAME record +type CNAME struct { + Name string +} + +// Coexist checks if a new resource record could coexist with given set +// of records under a label (can be called with a nil receiver) +func (rr *CNAME) Coexist([]*enums.GNSSpec, string) (bool, enums.GNSFlag) { + return true, 0 +} + +// ToMap adds the RR attributes to a stringed map +func (rr *CNAME) ToMap(params map[string]string, prefix string) { + params[prefix+"name"] = rr.Name +} + +//---------------------------------------------------------------------- + +// DNS TXT record +type TXT struct { + Text string +} + +// Coexist checks if a new resource record could coexist with given set +// of records under a label (can be called with a nil receiver) +func (rr *TXT) Coexist([]*enums.GNSSpec, string) (bool, enums.GNSFlag) { + return true, 0 +} + +// ToMap adds the RR attributes to a stringed map +func (rr *TXT) ToMap(params map[string]string, prefix string) { + params[prefix+"text"] = rr.Text +} + +//---------------------------------------------------------------------- + +// DNS IPv4 address +type DNSA struct { + Addr net.IP `size:"16"` +} + +// Coexist checks if a new resource record could coexist with given set +// of records under a label (can be called with a nil receiver) +func (rr *DNSA) Coexist([]*enums.GNSSpec, string) (bool, enums.GNSFlag) { + return true, 0 +} + +// ToMap adds the RR attributes to a stringed map +func (rr *DNSA) ToMap(params map[string]string, prefix string) { + params[prefix+"addr"] = rr.Addr.String() +} + +//---------------------------------------------------------------------- + +// DNS IPv6 address +type DNSAAAA struct { + Addr net.IP `size:"16"` +} + +// Coexist checks if a new resource record could coexist with given set +// of records under a label (can be called with a nil receiver) +func (rr *DNSAAAA) Coexist([]*enums.GNSSpec, string) (bool, enums.GNSFlag) { + return true, 0 +} + +// ToMap adds the RR attributes to a stringed map +func (rr *DNSAAAA) ToMap(params map[string]string, prefix string) { + params[prefix+"addr"] = rr.Addr.String() +} + +//---------------------------------------------------------------------- + +// MX is a DNS MX record +type MX struct { + Prio uint16 `order:"big"` + Server string +} + +// Coexist checks if a new resource record could coexist with given set +// of records under a label (can be called with a nil receiver) +func (rr *MX) Coexist([]*enums.GNSSpec, string) (bool, enums.GNSFlag) { + return true, 0 +} + +// ToMap adds the RR attributes to a stringed map +func (rr *MX) ToMap(params map[string]string, prefix string) { + params[prefix+"prio"] = fmt.Sprintf("%d", rr.Prio) + params[prefix+"host"] = rr.Server +} diff --git a/src/gnunet/service/gns/rr/gns.go b/src/gnunet/service/gns/rr/gns.go new file mode 100644 index 0000000..1926a4a --- /dev/null +++ b/src/gnunet/service/gns/rr/gns.go @@ -0,0 +1,199 @@ +// 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 rr + +import ( + "gnunet/crypto" + "gnunet/enums" +) + +//---------------------------------------------------------------------- +// GNS resource records +//---------------------------------------------------------------------- + +// PKEY (Ed25519+EcDSA) zone key +type PKEY struct { + *crypto.ZoneKey +} + +// Coexist checks if a new resource record could coexist with given set +// of records under a label (can be called with a nil receiver) +func (rr *PKEY) Coexist(list []*enums.GNSSpec, label string) (ok bool, forced enums.GNSFlag) { + // can't add PKEY to apex label + if label == "@" { + return + } + // make sure all existing records are PKEYs too + for _, e := range list { + if e.Type != enums.GNS_TYPE_PKEY && e.Type != enums.GNS_TYPE_EDKEY { + // check failed on non-PKEY + return + } + // check for active PKEY + if e.Flags&enums.GNS_FLAG_SHADOW == 0 { + // only additional shaow records allowed + forced = enums.GNS_FLAG_SHADOW + } + } + ok = true + return +} + +// ToMap adds the RR attributes to a stringed map +func (rr *PKEY) ToMap(params map[string]string, prefix string) { + params[prefix+"data"] = rr.ID() +} + +//---------------------------------------------------------------------- + +// EDKEY (EdDSA) zone key +type EDKEY struct { + *crypto.ZoneKey +} + +// Coexist checks if a new resource record could coexist with given set +// of records under a label (can be called with a nil receiver) +func (rr *EDKEY) Coexist(list []*enums.GNSSpec, label string) (ok bool, forced enums.GNSFlag) { + // can't add EDKEY to apex label + if label == "@" { + return + } + // make sure all existing records are EDKEYs too + for _, e := range list { + if e.Type != enums.GNS_TYPE_EDKEY && e.Type != enums.GNS_TYPE_PKEY { + // check failed on non-EDKEY + return + } + // check for active PKEY + if e.Flags&enums.GNS_FLAG_SHADOW == 0 { + // only additional shaow records allowed + forced = enums.GNS_FLAG_SHADOW + } + } + ok = true + return +} + +// ToMap adds the RR attributes to a stringed map +func (rr *EDKEY) ToMap(params map[string]string, prefix string) { + params[prefix+"data"] = rr.ID() +} + +//---------------------------------------------------------------------- + +// REDIRECT to name +type REDIRECT struct { + Name string +} + +// Coexist checks if a new resource record could coexist with given set +// of records under a label (can be called with a nil receiver) +func (rr *REDIRECT) Coexist(list []*enums.GNSSpec, label string) (ok bool, forced enums.GNSFlag) { + // no REDIRECT in apex zone + if label == "@" { + return + } + // make sure all existing records are supplemental EDKEYs too + for _, e := range list { + if e.Type != enums.GNS_TYPE_REDIRECT && e.Flags&enums.GNS_FLAG_SUPPL == 0 { + // check failed on non-supplemental non-REDIRECT record + return + } + // check for active REDIRECT + if e.Flags&enums.GNS_FLAG_SHADOW == 0 { + // only additional shaow records allowed + forced = enums.GNS_FLAG_SHADOW + } + } + ok = true + return +} + +// ToMap adds the RR attributes to a stringed map +func (rr *REDIRECT) ToMap(params map[string]string, prefix string) { + params[prefix+"name"] = rr.Name +} + +//---------------------------------------------------------------------- + +// GNS NICK record +type NICK struct { + Name string +} + +// Coexist checks if a new resource record could coexist with given set +// of records under a label (can be called with a nil receiver) +func (rr *NICK) Coexist(list []*enums.GNSSpec, label string) (ok bool, forced enums.GNSFlag) { + // can only be added to the apex label + if label != "@" { + return + } + // only one un-shadowed NICK allowed + for _, e := range list { + if e.Type == enums.GNS_TYPE_NICK && e.Flags&enums.GNS_FLAG_SHADOW == 0 { + // only additional shadow records allowed + forced = enums.GNS_FLAG_SHADOW + } + } + ok = true + return +} + +// ToMap adds the RR attributes to a stringed map +func (rr *NICK) ToMap(params map[string]string, prefix string) { + params[prefix+"name"] = rr.Name +} + +//---------------------------------------------------------------------- + +// LEHO record +type LEHO struct { + Name string +} + +// Coexist checks if a new resource record could coexist with given set +// of records under a label (can be called with a nil receiver) +func (rr *LEHO) Coexist([]*enums.GNSSpec, string) (bool, enums.GNSFlag) { + return true, 0 +} + +// ToMap adds the RR attributes to a stringed map +func (rr *LEHO) ToMap(params map[string]string, prefix string) { + params[prefix+"name"] = rr.Name +} + +//---------------------------------------------------------------------- + +// GNS2DNS delegation +type GNS2DNS struct { + Name string + Server string +} + +// Coexist checks if a new resource record could coexist with given set +// of records under a label (can be called with a nil receiver) +func (rr *GNS2DNS) Coexist([]*enums.GNSSpec, string) (bool, enums.GNSFlag) { + return true, 0 +} + +// ToMap adds the RR attributes to a stringed map +func (rr *GNS2DNS) ToMap(params map[string]string, prefix string) { + params[prefix+"name"] = rr.Name + params[prefix+"server"] = rr.Server +} diff --git a/src/gnunet/service/gns/rr/gns_box.go b/src/gnunet/service/gns/rr/gns_box.go new file mode 100644 index 0000000..5123beb --- /dev/null +++ b/src/gnunet/service/gns/rr/gns_box.go @@ -0,0 +1,375 @@ +// 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 rr + +import ( + "encoding/hex" + "strconv" + "strings" + + "gnunet/enums" + "gnunet/util" + + "github.com/bfix/gospel/data" + "github.com/bfix/gospel/logger" +) + +//---------------------------------------------------------------------- +// GNS box record that embeds either a TLSA or SRV record +//---------------------------------------------------------------------- + +// BOX is an encapsulated RR for special names +type BOX struct { + Proto uint16 `order:"big"` // Protcol identifier + Svc uint16 `order:"big"` // Service identifier + Type enums.GNSType `order:"big"` // Type of embedded RR + RR []byte `size:"*"` // embedded RR +} + +// NewBOX creates a new box instance from a BOX resource record data. +func NewBOX(buf []byte) *BOX { + b := new(BOX) + if err := data.Unmarshal(b, buf); err != nil { + logger.Printf(logger.ERROR, "[gns] Can't unmarshal BOX") + return nil + } + return b +} + +// Matches verifies that the remaining labels comply with the values +// in the BOX record. +func (b *BOX) Matches(labels []string) bool { + // resolve protocol and service names + proto, protoName := GetProtocol(labels[0]) + svc, _ := GetService(labels[1], protoName) + // no match on invalid resolution + if proto == 0 || svc == 0 { + return false + } + // check for matching values in box + return proto == b.Proto && svc == b.Svc +} + +// Coexist checks if a new resource record could coexist with given set +// of records under a label (can be called with a nil receiver) +func (b *BOX) Coexist([]*enums.GNSSpec, string) (bool, enums.GNSFlag) { + return true, 0 +} + +// ToMap adds the RR attributes to a stringed map +func (b *BOX) ToMap(params map[string]string, prefix string) { + // shared attributes + protoS := util.CastToString(b.Proto) + if pn := GetProtocolName(b.Proto); len(pn) > 0 { + protoS += " (" + pn + ")" + } + params[prefix+"proto"] = protoS + svcS := util.CastToString(b.Svc) + if sn := GetServiceName(b.Svc, b.Proto); len(sn) > 0 { + svcS += " " + sn + } + params[prefix+"svc"] = svcS + params[prefix+"type"] = util.CastToString(int(b.Type)) + // attributes of embedded record + if rr, err := b.EmbeddedRR(); err == nil && rr != nil { + rr.ToMap(params, prefix) + } +} + +// EmbeddedRR returns the embedded RR as an instance +func (b *BOX) EmbeddedRR() (rr RR, err error) { + switch b.Type { + case enums.GNS_TYPE_DNS_TLSA: + rr = new(TLSA) + case enums.GNS_TYPE_DNS_SRV: + rr = new(SRV) + } + err = data.Unmarshal(rr, b.RR) + return +} + +//---------------------------------------------------------------------- +// embedded resource records +//---------------------------------------------------------------------- + +var ( + // TLSAUsage for defined usage values + TLSAUsage = map[uint8]string{ + 0: "CA certificate", + 1: "Service certificate constraint", + 2: "Trust anchor assertion", + 3: "Domain-issued certificate", + 255: "Private use", + } + // TLSASelector for defined selector values + TLSASelector = map[uint8]string{ + 0: "Full certificate", + 1: "SubjectPublicKeyInfo", + 255: "Private use", + } + // TLSAMatch for defined match values + TLSAMatch = map[uint8]string{ + 0: "No hash", + 1: "SHA-256", + 2: "SHA-512", + 255: "Private use", + } +) + +// TLSA is a DNSSEC TLS asscoication +type TLSA struct { + Usage uint8 + Selector uint8 + Match uint8 + Cert []byte `size:"*"` +} + +// Coexist checks if a new resource record could coexist with given set +// of records under a label (can be called with a nil receiver) +func (rr *TLSA) Coexist([]*enums.GNSSpec, string) (bool, enums.GNSFlag) { + return true, 0 +} + +// ToMap adds the RR attributes to a stringed map +func (rr *TLSA) ToMap(params map[string]string, prefix string) { + params[prefix+"tlsa_usage"] = strconv.Itoa(int(rr.Usage)) + params[prefix+"tlsa_selector"] = strconv.Itoa(int(rr.Selector)) + params[prefix+"tlsa_match"] = strconv.Itoa(int(rr.Match)) + params[prefix+"tlsa_cert"] = hex.EncodeToString(rr.Cert) +} + +//---------------------------------------------------------------------- + +// SRV for service definitions +type SRV struct { + Host string +} + +// Coexist checks if a new resource record could coexist with given set +// of records under a label (can be called with a nil receiver) +func (rr *SRV) Coexist([]*enums.GNSSpec, string) (bool, enums.GNSFlag) { + return true, 0 +} + +// ToMap adds the RR attributes to a stringed map +func (rr *SRV) ToMap(params map[string]string, prefix string) { + params[prefix+"srv_host"] = rr.Host +} + +//---------------------------------------------------------------------- +// BOX protocols +//---------------------------------------------------------------------- + +// list of handled protocols in BOX records +var protocols = map[string]uint16{ + "icmp": 1, + "igmp": 2, + "tcp": 6, + "udp": 17, + "ipv6-icmp": 58, +} + +// GetProtocolName returns the name of a protocol for given nu,ber +func GetProtocolName(proto uint16) string { + // check for valid number (reverse protocol lookup) + for label, id := range protocols { + if id == proto { + // return found entry + return label + } + } + return util.CastToString(proto) +} + +// GetProtocols returns a list of supported protocols for use +// by caller (e.g. UI handling) +func GetProtocols() (protos map[uint16]string) { + protos = make(map[uint16]string) + for name, id := range protocols { + protos[id] = name + } + return +} + +// GetProtocol returns the protocol number and name for a given name. The +// name can be an integer value (e.g. "_6" for "tcp") or a mnemonic name +// (e.g. like "_tcp"). +func GetProtocol(name string) (uint16, string) { + // check for required prefix + if name[0] != '_' { + return 0, "" + } + name = strings.ToLower(name[1:]) + + // if label is an integer value it is the protocol number + if val, err := strconv.Atoi(name); err == nil { + proto := uint16(val) + label := GetProtocolName(proto) + if len(label) == 0 { + proto = 0 + } + return proto, label + } + // try to resolve via protocol map + if id, ok := protocols[name]; ok { + return id, name + } + // resolution failed + return 0, "" +} + +//---------------------------------------------------------------------- +// BOX services +//---------------------------------------------------------------------- + +// list of services (per protocol) handled in BOX records +var services = map[string]map[string]uint16{ + "udp": { + "bootpc": 68, + "bootps": 67, + "domain": 53, + "gnunet": 2086, + "https": 443, + "isakmp": 500, + "kerberos4": 750, + "kerberos": 88, + "ldap": 389, + "ldaps": 636, + "ntp": 123, + "openvpn": 1194, + "radius": 1812, + "rtsp": 554, + "sip": 5060, + "sip-tls": 5061, + "snmp": 161, + "syslog": 514, + "tftp": 69, + "who": 513, + }, + "tcp": { + "domain": 53, + "finger": 79, + "ftp": 21, + "ftp-data": 20, + "ftps": 990, + "ftps-data": 989, + "git": 9418, + "gnunet": 2086, + "gopher": 70, + "http": 80, + "https": 443, + "imap2": 143, + "imaps": 993, + "kerberos4": 750, + "kerberos": 88, + "kermit": 1649, + "ldap": 389, + "ldaps": 636, + "login": 513, + "mysql": 3306, + "openvpn": 1194, + "pop3": 110, + "pop3s": 995, + "printer": 515, + "radius": 1812, + "redis": 6379, + "rsync": 873, + "rtsp": 554, + "shell": 514, + "sip": 5060, + "sip-tls": 5061, + "smtp": 25, + "snmp": 161, + "ssh": 22, + "telnet": 23, + "telnets": 992, + "uucp": 540, + "webmin": 10000, + "x11": 6000, + }, +} + +// GetServiceName returns the service spec on given port +func GetServiceName(svc, proto uint16) string { + for n, id := range services[GetProtocolName(proto)] { + if id == svc { + return n + } + } + return util.CastToString(svc) +} + +// GetServices returns a list of supported services for use +// by caller (e.g. UI handling) +func GetServices() (svcs map[uint16]string) { + svcs = make(map[uint16]string) + for n, id := range services["tcp"] { + svcs[id] = n + " (tcp" + } + for n, id := range services["udp"] { + nn, ok := svcs[id] + if ok { + svcs[id] = nn + "/udp" + } else { + svcs[id] = n + " (udp" + } + } + for id, n := range svcs { + svcs[id] = n + ")" + } + return +} + +// GetService returns the port number and the name of a service (with given +// protocol). The name can be an integer value (e.g. "_443" for "https") or +// a mnemonic name (e.g. like "_https"). +func GetService(name, proto string) (uint16, string) { + // check for required prefix + if name[0] != '_' { + return 0, "" + } + name = strings.ToLower(name[1:]) + + // get list of services for given protocol + svcs, ok := services[proto] + if !ok { + // no services available for this protocol + return 0, "" + } + + // if label is an integer value it is the port number + if val, err := strconv.Atoi(name); err == nil { + svc := uint16(val) + // check for valid number (reverse service lookup) + for label, id := range svcs { + if id == svc { + // return found entry + return svc, label + } + } + // number out of range + return 0, "" + } + // try to resolve via services map + if id, ok := svcs[name]; ok { + return id, name + } + // resolution failed + return 0, "" +} diff --git a/src/gnunet/service/gns/service.go b/src/gnunet/service/gns/service.go index 010c460..dbbd425 100644 --- a/src/gnunet/service/gns/service.go +++ b/src/gnunet/service/gns/service.go @@ -136,7 +136,7 @@ func (s *Service) HandleMessage(ctx context.Context, sender *util.PeerID, msg me logger.Printf(logger.DBG, "[gns%s] Lookup request finished.\n", label) }() - kind := NewRRTypeList(enums.GNSType(m.RType)) + kind := NewRRTypeList(m.RType) 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()) @@ -159,7 +159,7 @@ func (s *Service) HandleMessage(ctx context.Context, sender *util.PeerID, msg me logger.Printf(logger.DBG, "[gns%s] Record #%d: %v\n", label, i, rec) // is this the record type we are looking for? - if rec.RType == m.RType || enums.GNSType(m.RType) == enums.GNS_TYPE_ANY { + if rec.RType == m.RType || m.RType == enums.GNS_TYPE_ANY { // add it to the response message if err := resp.AddRecord(rec); err != nil { logger.Printf(logger.ERROR, "[gns%s] failed: %sv", label, err.Error()) @@ -192,7 +192,7 @@ func (s *Service) QueryKeyRevocation(ctx context.Context, zkey *crypto.ZoneKey) // get response from Revocation service var resp message.Message - if resp, err = service.RequestResponse(ctx, "gns", "Revocation", config.Cfg.Revocation.Service.Socket, req); err != nil { + if resp, err = service.RequestResponse(ctx, "gns", "Revocation", config.Cfg.Revocation.Service.Socket, req, true); err != nil { return } @@ -218,7 +218,7 @@ func (s *Service) RevokeKey(ctx context.Context, rd *revocation.RevData) (succes // get response from Revocation service var resp message.Message - if resp, err = service.RequestResponse(ctx, "gns", "Revocation", config.Cfg.Revocation.Service.Socket, req); err != nil { + if resp, err = service.RequestResponse(ctx, "gns", "Revocation", config.Cfg.Revocation.Service.Socket, req, true); err != nil { return } @@ -247,7 +247,7 @@ func (s *Service) LookupNamecache(ctx context.Context, query *blocks.GNSQuery) ( // get response from Namecache service var resp message.Message - if resp, err = service.RequestResponse(ctx, "gns", "Namecache", config.Cfg.Namecache.Service.Socket, req); err != nil { + if resp, err = service.RequestResponse(ctx, "gns", "Namecache", config.Cfg.Namecache.Service.Socket, req, true); err != nil { return } @@ -308,7 +308,7 @@ func (s *Service) StoreNamecache(ctx context.Context, query *blocks.GNSQuery, bl // get response from Namecache service var resp message.Message - if resp, err = service.RequestResponse(ctx, "gns", "Namecache", config.Cfg.Namecache.Service.Socket, req); err != nil { + if resp, err = service.RequestResponse(ctx, "gns", "Namecache", config.Cfg.Namecache.Service.Socket, req, true); err != nil { return } diff --git a/src/gnunet/service/revocation/pow_test.go b/src/gnunet/service/revocation/pow_test.go index 0747d72..41c17b4 100644 --- a/src/gnunet/service/revocation/pow_test.go +++ b/src/gnunet/service/revocation/pow_test.go @@ -60,7 +60,7 @@ func TestRevocationRFC(t *testing.T) { if err != nil { t.Fatal(err) } - prv, err := crypto.NewZonePrivate(crypto.ZONE_PKEY, d) + prv, err := crypto.NewZonePrivate(enums.GNS_TYPE_PKEY, d) if err != nil { t.Fatal(err) } diff --git a/src/gnunet/service/store/store_dht_meta.go b/src/gnunet/service/store/store_dht_meta.go index 64f658b..d1c4cb7 100644 --- a/src/gnunet/service/store/store_dht_meta.go +++ b/src/gnunet/service/store/store_dht_meta.go @@ -97,10 +97,16 @@ func OpenMetaDB(path string) (db *FileMetaDB, err error) { // Store metadata in database: creates or updates a record for the metadata // in the database; primary key is the query key func (db *FileMetaDB) Store(md *FileMetadata) (err error) { + // work around a SQLite3 bug when storing uint64 with high bit set + var exp *uint64 + if !md.expires.IsNever() { + exp = new(uint64) + *exp = md.expires.Val + } sql := "replace into meta(qkey,btype,bhash,size,stored,expires,lastUsed,usedCount) values(?,?,?,?,?,?,?,?)" _, err = db.conn.Exec(sql, md.key.Data, md.btype, md.bhash.Data, md.size, md.stored.Epoch(), - md.expires.Val, md.lastUsed.Epoch(), md.usedCount) + exp, md.lastUsed.Epoch(), md.usedCount) return } @@ -124,13 +130,19 @@ func (db *FileMetaDB) Get(query blocks.Query) (mds []*FileMetadata, err error) { md.key = query.Key() md.btype = btype var st, lu uint64 - if err = rows.Scan(&md.size, &md.bhash.Data, &st, &md.expires.Val, &lu, &md.usedCount); err != nil { + var exp *uint64 + if err = rows.Scan(&md.size, &md.bhash.Data, &st, &exp, &lu, &md.usedCount); err != nil { if err == sql.ErrNoRows { md = nil err = nil } return } + if exp != nil { + md.expires.Val = *exp + } else { + md.expires = util.AbsoluteTimeNever() + } md.stored.Val = st * 1000000 md.lastUsed.Val = lu * 1000000 mds = append(mds, md) @@ -192,10 +204,16 @@ func (db *FileMetaDB) Traverse(f func(*FileMetadata)) error { md := NewFileMetadata() for rows.Next() { var st, lu uint64 - err = rows.Scan(&md.key.Data, &md.btype, &md.bhash.Data, &md.size, &st, &md.expires.Val, &lu, &md.usedCount) + var exp *uint64 + err = rows.Scan(&md.key.Data, &md.btype, &md.bhash.Data, &md.size, &st, &exp, &lu, &md.usedCount) if err != nil { return err } + if exp != nil { + md.expires.Val = *exp + } else { + md.expires = util.AbsoluteTimeNever() + } md.stored.Val = st * 1000000 md.lastUsed.Val = lu * 1000000 // call process function diff --git a/src/gnunet/service/store/store_zonemaster.go b/src/gnunet/service/store/store_zonemaster.go new file mode 100644 index 0000000..44b1a68 --- /dev/null +++ b/src/gnunet/service/store/store_zonemaster.go @@ -0,0 +1,548 @@ +// 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 store + +import ( + "database/sql" + _ "embed" + "errors" + "fmt" + "gnunet/crypto" + "gnunet/enums" + "gnunet/service/dht/blocks" + "gnunet/util" + "os" + // "https://github.com/go-zeromq/zmq4" +) + +//============================================================ +// Local zone records stored in SQLite3 database +//============================================================ + +// Zone is the definition of a local GNS zone +// and is stored in a SQL database for faster access. +type Zone struct { + ID int64 // database identifier + Name string // zone name + Created util.AbsoluteTime // date of creation + Modified util.AbsoluteTime // date of last modification + Key *crypto.ZonePrivate // private zone key (ztype|zdata) +} + +// NewZone creates a new zone for the given private key. The zone is not stored +// in the database automatically. +func NewZone(name string, sk *crypto.ZonePrivate) *Zone { + // create zone instance + return &Zone{ + Name: name, + Created: util.AbsoluteTimeNow(), + Modified: util.AbsoluteTimeNow(), + Key: sk, + } +} + +//---------------------------------------------------------------------- + +type Label struct { + ID int64 // database id of label + Zone int64 // database ID of parent zone + Name string // label name + Created util.AbsoluteTime // date of creation + Modified util.AbsoluteTime // date of last modification +} + +func NewLabel(label string) *Label { + lbl := new(Label) + lbl.ID = 0 + lbl.Zone = 0 + lbl.Name = label + lbl.Created = util.AbsoluteTimeNow() + lbl.Modified = util.AbsoluteTimeNow() + return lbl +} + +//---------------------------------------------------------------------- + +// Record for GNS resource in a zone (generic). It is the responsibility +// of the caller to provide valid resource data in binary form. +type Record struct { + ID int64 // database id of record + Label int64 // database ID of parent label + Created util.AbsoluteTime // date of creation + Modified util.AbsoluteTime // date of last modification + + blocks.ResourceRecord +} + +// NewRecord creates a new record for given data. The record is not +// automatically added to the database. +func NewRecord(expire util.AbsoluteTime, rtype enums.GNSType, flags enums.GNSFlag, data []byte) *Record { + rec := new(Record) + rec.ID = 0 + rec.Label = 0 + rec.Expire = expire + rec.RType = rtype + rec.Flags = flags + rec.Data = data + rec.Size = uint32(len(rec.Data)) + rec.Created = util.AbsoluteTimeNow() + rec.Modified = util.AbsoluteTimeNow() + return rec +} + +//====================================================================== +// Zone database: A SQLite3 database to hold metadata about +// managed local zones (see "namestore" in gnunet). +//====================================================================== + +//go:embed store_zonemaster.sql +var initScriptZM []byte + +// ZoneDB is a SQLite3 database for locally managed zones +type ZoneDB struct { + conn *DBConn // database connection +} + +// OpenZoneDB opens a zone database in the given filename (including +// path). If the database file does not exist, it is created and +// set up with empty tables. +func OpenZoneDB(fname string) (db *ZoneDB, err error) { + // connect to database + if _, err = os.Stat(fname); err != nil { + var file *os.File + if file, err = os.Create(fname); err != nil { + return + } + file.Close() + } + db = new(ZoneDB) + if db.conn, err = DBPool.Connect("sqlite3:" + fname); err != nil { + return + } + // check for initialized database + res := db.conn.QueryRow("select name from sqlite_master where type='table' and name='zones'") + var s string + if res.Scan(&s) != nil { + // initialize database + if _, err = db.conn.Exec(string(initScriptZM)); err != nil { + return + } + } + return +} + +// Close zone database +func (db *ZoneDB) Close() error { + return db.conn.Close() +} + +//---------------------------------------------------------------------- +// Zone handling +//---------------------------------------------------------------------- + +// SetZone inserts, updates or deletes a zone in the database. +// The function does not change timestamps which are in the +// responsibility of the caller. +// - insert: Zone.ID is nil (0) +// - update: Zone.Name is set +// - remove: otherwise +func (db *ZoneDB) SetZone(z *Zone) error { + // check for zone insert + if z.ID == 0 { + stmt := "insert into zones(name,created,modified,ztype,zdata) values(?,?,?,?,?)" + result, err := db.conn.Exec(stmt, z.Name, z.Created.Val, z.Modified.Val, z.Key.Type, z.Key.KeyData) + if err != nil { + return err + } + z.ID, err = result.LastInsertId() + return err + } + // check for zone update (name only) + if len(z.Name) > 0 { + stmt := "update zones set name=?,modified=? where id=?" + result, err := db.conn.Exec(stmt, z.Name, z.Modified.Val, z.ID) + if err != nil { + return err + } + var num int64 + if num, err = result.RowsAffected(); err == nil { + if num != 1 { + err = errors.New("update zone failed") + } + } + return err + } + // remove zone from database: also move all dependent labels to "trash bin" + // (parent zone reference is nil) + if _, err := db.conn.Exec("update labels set zid=null where zid=?", z.ID); err != nil { + return err + } + _, err := db.conn.Exec("delete from zones where id=?", z.ID) + return err +} + +// GetZone gets a zone with given identifier +func (db *ZoneDB) GetZone(id int64) (zone *Zone, err error) { + // assemble zone from database row + stmt := "select name,created,modified,ztype,zdata from zones where id=?" + zone = new(Zone) + var ztype enums.GNSType + var zdata []byte + row := db.conn.QueryRow(stmt, id) + if err = row.Scan(&zone.Name, &zone.Created.Val, &zone.Modified.Val, &ztype, &zdata); err == nil { + // reconstruct private zone key + zone.Key, err = crypto.NewZonePrivate(ztype, zdata) + } + return +} + +// GetZones retrieves zone instances from database matching a filter +// ("where" clause) +func (db *ZoneDB) GetZones(filter string, args ...any) (list []*Zone, err error) { + // assemble query + stmt := "select id,name,created,modified,ztype,zdata from zones" + if len(filter) > 0 { + stmt += " where " + fmt.Sprintf(filter, args...) + } + // select zones + var rows *sql.Rows + if rows, err = db.conn.Query(stmt); err != nil { + return + } + // process zones + defer rows.Close() + for rows.Next() { + // assemble zone from database row + zone := new(Zone) + var ztype enums.GNSType + var zdata []byte + if err = rows.Scan(&zone.ID, &zone.Name, &zone.Created.Val, &zone.Modified.Val, &ztype, &zdata); err != nil { + // terminate on error; return list so far + return + } + // reconstruct private zone key + if zone.Key, err = crypto.NewZonePrivate(ztype, zdata); err != nil { + return + } + // append to result list + list = append(list, zone) + } + return +} + +//---------------------------------------------------------------------- +// Label handling +//---------------------------------------------------------------------- + +// SetLabel inserts, updates or deletes a zone label in the database. +// The function does not change timestamps which are in the +// responsibility of the caller. +// - insert: Label.ID is nil (0) +// - update: Label.Name is set (eventually modified) +// - remove: otherwise +func (db *ZoneDB) SetLabel(l *Label) error { + // check for label insert + if l.ID == 0 { + stmt := "insert into labels(zid,name,created,modified) values(?,?,?,?)" + result, err := db.conn.Exec(stmt, l.Zone, l.Name, l.Created.Val, l.Modified.Val) + if err != nil { + return err + } + l.ID, err = result.LastInsertId() + return err + } + // check for label update (name only) + if len(l.Name) > 0 { + stmt := "update labels set name=?,modified=? where id=?" + result, err := db.conn.Exec(stmt, l.Name, l.Modified.Val, l.ID) + if err != nil { + return err + } + var num int64 + if num, err = result.RowsAffected(); err == nil { + if num != 1 { + err = errors.New("update label failed") + } + } + return err + } + // remove label from database; move dependent records to trash bin + // (label id set to nil) + if _, err := db.conn.Exec("update records set lid=null where lid=?", l.ID); err != nil { + return err + } + _, err := db.conn.Exec("delete from labels where id=?", l.ID) + return err +} + +// GetLabel gets a label with given identifier +func (db *ZoneDB) GetLabel(id int64) (label *Label, err error) { + // assemble label from database row + stmt := "select zid,name,created,modified from labels where id=?" + label = new(Label) + row := db.conn.QueryRow(stmt, id) + err = row.Scan(&label.Zone, &label.Name, &label.Created.Val, &label.Modified.Val) + return +} + +// GetLabels retrieves record instances from database matching a filter +// ("where" clause) +func (db *ZoneDB) GetLabels(filter string, args ...any) (list []*Label, err error) { + // assemble querey + stmt := "select id,zid,name,created,modified from labels" + if len(filter) > 0 { + stmt += " where " + fmt.Sprintf(filter, args...) + } + // select labels + var rows *sql.Rows + if rows, err = db.conn.Query(stmt); err != nil { + return + } + // process labels + defer rows.Close() + for rows.Next() { + // assemble label from database row + lbl := new(Label) + if err = rows.Scan(&lbl.ID, &lbl.Zone, &lbl.Name, &lbl.Created.Val, &lbl.Modified.Val); err != nil { + // terminate on error; return list so far + return + } + // append to result list + list = append(list, lbl) + } + return +} + +//---------------------------------------------------------------------- +// Record handling +//---------------------------------------------------------------------- + +// SetRecord inserts, updates or deletes a record in the database. +// The function does not change timestamps which are in the +// responsibility of the caller. +// - insert: Record.ID is nil (0) +// - update: Record.ZID is set (eventually modified) +// - remove: otherwise +func (db *ZoneDB) SetRecord(r *Record) error { + // work around a SQLite3 bug when storing uint64 with high bit set + var exp *uint64 + if !r.Expire.IsNever() { + exp = new(uint64) + *exp = r.Expire.Val + } + // check for record insert + if r.ID == 0 { + stmt := "insert into records(lid,expire,created,modified,flags,rtype,rdata) values(?,?,?,?,?,?,?)" + result, err := db.conn.Exec(stmt, r.Label, exp, r.Created.Val, r.Modified.Val, r.Flags, r.RType, r.Data) + if err != nil { + return err + } + r.ID, err = result.LastInsertId() + return err + } + // check for record update + if r.Label != 0 { + stmt := "update records set lid=?,expire=?,modified=?,flags=?,rtype=?,rdata=? where id=?" + result, err := db.conn.Exec(stmt, r.Label, exp, r.Modified.Val, r.Flags, r.RType, r.Data, r.ID) + if err != nil { + return err + } + var num int64 + if num, err = result.RowsAffected(); err == nil { + if num != 1 { + err = errors.New("update record failed") + } + } + return err + } + // remove record from database + _, err := db.conn.Exec("delete from records where id=?", r.ID) + return err +} + +// GetRecord gets a resource record with given identifier +func (db *ZoneDB) GetRecord(id int64) (rec *Record, err error) { + // assemble resource record from database row + stmt := "select lid,expire,created,modified,flags,rtype,rdata from records where id=?" + rec = new(Record) + row := db.conn.QueryRow(stmt, id) + var exp *uint64 + if err = row.Scan(&rec.Label, &exp, &rec.Created.Val, &rec.Modified.Val, &rec.Flags, &rec.RType, &rec.Data); err != nil { + // terminate on error + return + } + // setup missing fields + rec.Size = uint32(len(rec.Data)) + if exp != nil { + rec.Expire.Val = *exp + } else { + rec.Expire = util.AbsoluteTimeNever() + } + return +} + +// GetRecords retrieves record instances from database matching a filter +// ("where" clause) +func (db *ZoneDB) GetRecords(filter string, args ...any) (list []*Record, err error) { + // assemble querey + stmt := "select id,lid,expire,created,modified,flags,rtype,rdata from records" + if len(filter) > 0 { + stmt += " where " + fmt.Sprintf(filter, args...) + } + // select records + var rows *sql.Rows + if rows, err = db.conn.Query(stmt); err != nil { + return + } + // process records + defer rows.Close() + for rows.Next() { + // assemble record from database row + rec := new(Record) + var exp *uint64 + if err = rows.Scan(&rec.ID, &rec.Label, &exp, &rec.Created.Val, &rec.Modified.Val, &rec.Flags, &rec.RType, &rec.Data); err != nil { + // terminate on error; return list so far + return + } + rec.Size = uint32(len(rec.Data)) + if exp != nil { + rec.Expire.Val = *exp + } else { + rec.Expire = util.AbsoluteTimeNever() + } + // append to result list + list = append(list, rec) + } + return +} + +//---------------------------------------------------------------------- +// Retrieve database content as a nested struct +//---------------------------------------------------------------------- + +// LabelGroup is a nested label entry (with records) +type LabelGroup struct { + Label *Label + Records []*Record +} + +// ZoneGroup is a nested zone entry (with labels) +type ZoneGroup struct { + Zone *Zone + PubID string + Labels []*LabelGroup +} + +// GetContent returns the database content as a nested list of zones, labels +// and records. Since the use-case for the ZoneManager is the management of +// local zones, the number of entries is limited. +func (db *ZoneDB) GetContent() (zg []*ZoneGroup, err error) { + // get all zones + var zones []*Zone + if zones, err = db.GetZones(""); err != nil { + return + } + for _, z := range zones { + // create group instance for zone + zGroup := &ZoneGroup{ + Zone: z, + PubID: z.Key.Public().ID(), + Labels: make([]*LabelGroup, 0), + } + zg = append(zg, zGroup) + + // get all labels for zone + var labels []*Label + if labels, err = db.GetLabels("zid=%d", z.ID); err != nil { + return + } + for _, l := range labels { + // create group instance for label + lGroup := &LabelGroup{ + Label: l, + Records: make([]*Record, 0), + } + // link to zone group + zGroup.Labels = append(zGroup.Labels, lGroup) + + // get all records for label + lGroup.Records, err = db.GetRecords("lid=%d", l.ID) + } + } + return +} + +//---------------------------------------------------------------------- +// Retrieve list of used names (Zone,Label) or RR types (Record) +//---------------------------------------------------------------------- + +// GetName returns an object name (zone,label) for given id +func (db *ZoneDB) GetName(tbl string, id int64) (name string, err error) { + row := db.conn.QueryRow("select name from "+tbl+" where id=?", id) + err = row.Scan(&name) + return +} + +// GetNames returns a list of used names (table "zones" and "labels") +func (db *ZoneDB) GetNames(tbl string) (names []string, err error) { + // select all table names + var rows *sql.Rows + if rows, err = db.conn.Query("select name from " + tbl); err != nil { + return + } + // process names + defer rows.Close() + var name string + for rows.Next() { + if err = rows.Scan(&name); err != nil { + // terminate on error; return list so far + return + } + // append to result list + names = append(names, name) + } + return +} + +// GetRRTypes returns a list record types stored under a label +func (db *ZoneDB) GetRRTypes(lid int64) (rrtypes []*enums.GNSSpec, label string, err error) { + // select label name + row := db.conn.QueryRow("select name from labels where id=?", lid) + if err = row.Scan(&label); err != nil { + return + } + // select all record types under label + stmt := "select rtype,flags from records where lid=?" + var rows *sql.Rows + if rows, err = db.conn.Query(stmt, lid); err != nil { + return + } + // process records + defer rows.Close() + for rows.Next() { + e := new(enums.GNSSpec) + if err = rows.Scan(&e.Type, &e.Flags); err != nil { + // terminate on error; return list so far + return + } + // append to result list + rrtypes = append(rrtypes, e) + } + return +} diff --git a/src/gnunet/service/store/store_zonemaster.sql b/src/gnunet/service/store/store_zonemaster.sql new file mode 100644 index 0000000..169fefd --- /dev/null +++ b/src/gnunet/service/store/store_zonemaster.sql @@ -0,0 +1,47 @@ +-- 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 + +create table zones ( + id integer primary key autoincrement, + name text unique, + created integer, + modified integer, + ztype integer, + zdata blob +); + + +create table labels ( + id integer primary key autoincrement, + zid integer references zones(id), + name text, + created integer, + modified integer, + unique (zid,name) +); + +create table records ( + id integer primary key autoincrement, + lid integer references labels(id), + expire integer, + created integer, + modified integer, + flags integer, + rtype integer, + rdata blob +); diff --git a/src/gnunet/service/store/store_zonemaster_test.go b/src/gnunet/service/store/store_zonemaster_test.go new file mode 100644 index 0000000..6f416b1 --- /dev/null +++ b/src/gnunet/service/store/store_zonemaster_test.go @@ -0,0 +1,105 @@ +// 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 store + +import ( + "crypto/rand" + "gnunet/crypto" + "gnunet/enums" + "gnunet/util" + "os" + "testing" + "time" +) + +func TestZoneMaster(t *testing.T) { + + //------------------------------------------------------------------ + // create database + _ = os.Remove("/tmp/zonemaster.db") + zdb, err := OpenZoneDB("/tmp/zonemaster.db") + if err != nil { + t.Fatal(err) + } + + //------------------------------------------------------------------ + // create zone and add zone to database + seed := make([]byte, 32) + if _, err = rand.Read(seed); err != nil { + t.Fatal(err) + } + zp, err := crypto.NewZonePrivate(enums.GNS_TYPE_PKEY, seed) + if err != nil { + t.Fatal(err) + } + zone := NewZone("foo", zp) + if err = zdb.SetZone(zone); err != nil { + t.Fatal(err) + } + + //------------------------------------------------------------------ + // create label and add to zone and database + label := NewLabel("bar") + label.Zone = zone.ID + if err = zdb.SetLabel(label); err != nil { + t.Fatal(err) + } + + //------------------------------------------------------------------ + // add record to label and database + rec := NewRecord(util.AbsoluteTimeNever().Add(time.Hour), enums.GNS_TYPE_DNS_TXT, 0, []byte("test entry")) + rec.Label = label.ID + if err = zdb.SetRecord(rec); err != nil { + t.Fatal(err) + } + + //------------------------------------------------------------------ + // search record in database + recs, err := zdb.GetRecords("rtype=%d", enums.GNS_TYPE_DNS_TXT) + if err != nil { + t.Fatal(err) + } + if len(recs) != 1 { + t.Fatalf("record: got %d records, expected 1", len(recs)) + } + + //------------------------------------------------------------------ + // rename zone + zone.Name = "MyZone" + zone.Modified = util.AbsoluteTimeNow() + if err = zdb.SetZone(zone); err != nil { + t.Fatal(err) + } + + //------------------------------------------------------------------ + // search zone in database + zones, err := zdb.GetZones("name like 'My%%'") + if err != nil { + t.Fatal(err) + } + if len(zones) != 1 { + t.Fatalf("zone: got %d records, expected 1", len(zones)) + } + + //------------------------------------------------------------------ + // close database + if err = zdb.Close(); err != nil { + t.Fatal(err) + } +} diff --git a/src/gnunet/service/zonemaster/gui.go b/src/gnunet/service/zonemaster/gui.go new file mode 100644 index 0000000..3885d01 --- /dev/null +++ b/src/gnunet/service/zonemaster/gui.go @@ -0,0 +1,666 @@ +// 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 zonemaster + +import ( + "bytes" + "context" + "crypto/rand" + "embed" + "errors" + "fmt" + "gnunet/crypto" + "gnunet/enums" + "gnunet/service/gns/rr" + "gnunet/service/store" + "gnunet/util" + "io" + "net" + "net/http" + "strings" + "text/template" + "time" + + "github.com/bfix/gospel/logger" + "github.com/gorilla/mux" +) + +//====================================================================== +// HTTP service +//====================================================================== + +//go:embed gui.htpl gui_css.htpl gui_rr.htpl gui_debug.htpl gui_edit.htpl gui_new.htpl +var fsys embed.FS + +var ( + tpl *template.Template // HTML templates + timeHTML = "2006-01-02T15:04" // time format (defined by HTML, don't change!) + timeGUI = "02.01.06 15:04" // time format for GUI +) + +// state-change constants +const ( + ChangeNew = iota + ChangeUpdate + ChangeDelete +) + +//---------------------------------------------------------------------- + +// Start HTTP server to provide GUI +func (zm *ZoneMaster) startGUI(ctx context.Context) { + logger.Println(logger.INFO, "[zonemaster] Starting HTTP GUI backend...") + + // read and prepare templates + tpl = template.New("gui") + tpl.Funcs(template.FuncMap{ + "date": func(ts util.AbsoluteTime) string { + return guiTime(ts) + }, + "keytype": func(t enums.GNSType) string { + return guiKeyType(t) + }, + "setspecs": func(data map[string]string, spec enums.GNSSpec) string { + pf := guiPrefix(spec.Type) + data["prefix"] = pf + if spec.Flags&enums.GNS_FLAG_PRIVATE != 0 { + data[pf+"private"] = "on" + } + if spec.Flags&enums.GNS_FLAG_SHADOW != 0 { + data[pf+"shadow"] = "on" + } + if spec.Flags&enums.GNS_FLAG_SUPPL != 0 { + data[pf+"suppl"] = "on" + } + return pf + }, + "boxprotos": func() map[uint16]string { + return rr.GetProtocols() + }, + "boxsvcs": func() map[uint16]string { + return rr.GetServices() + }, + "rrtype": func(t enums.GNSType) string { + return strings.Replace(t.String(), "GNS_TYPE_", "", -1) + }, + "rritype": func(ts string) string { + t, _ := util.CastFromString[enums.GNSType](ts) + return strings.Replace(t.String(), "GNS_TYPE_", "", -1) + }, + "rrflags": func(f enums.GNSFlag) string { + flags := make([]string, 0) + if f&enums.GNS_FLAG_PRIVATE != 0 { + flags = append(flags, "Private") + } + if f&enums.GNS_FLAG_SHADOW != 0 { + flags = append(flags, "Shadow") + } + if f&enums.GNS_FLAG_SUPPL != 0 { + flags = append(flags, "Suppl") + } + if len(flags) == 0 { + return "None" + } + return strings.Join(flags, ",
") + }, + "rrdata": func(t enums.GNSType, buf []byte) string { + return guiRRdata(t, buf) + }, + }) + if _, err := tpl.ParseFS(fsys, "*.htpl"); err != nil { + logger.Println(logger.ERROR, "[zonemaster] GUI templates failed: "+err.Error()) + return + } + + // start HTTP server + router := mux.NewRouter() + router.HandleFunc("/new/{mode}/{id}", zm.new) + router.HandleFunc("/edit/{mode}/{id}", zm.edit) + router.HandleFunc("/del/{mode}/{id}", zm.remove) + router.HandleFunc("/action/{cmd}/{mode}/{id}", zm.action) + router.HandleFunc("/", zm.dashboard) + srv := &http.Server{ + Addr: zm.cfg.ZoneMaster.GUI, + ReadTimeout: 10 * time.Second, + ReadHeaderTimeout: 5 * time.Second, + Handler: router, + BaseContext: func(l net.Listener) context.Context { + return ctx + }, + } + go func() { + if err := srv.ListenAndServe(); err != http.ErrServerClosed { + logger.Printf(logger.ERROR, "[zonemaster] Failed to start GUI: "+err.Error()) + } + }() +} + +//---------------------------------------------------------------------- + +// dashboard is the main entry point for the GUI +func (zm *ZoneMaster) dashboard(w http.ResponseWriter, r *http.Request) { + // collect information for the GUI + zg, err := zm.zdb.GetContent() + if err != nil { + _, _ = io.WriteString(w, "ERROR: "+err.Error()) + return + } + // show dashboard + renderPage(w, zg, "dashboard") +} + +//====================================================================== +// Handle GUI actions (add, edit and remove) +//====================================================================== + +// action dispatcher +func (zm *ZoneMaster) action(w http.ResponseWriter, r *http.Request) { + // prepare variables and form values + vars := mux.Vars(r) + mode := vars["mode"] + id, ok := util.CastFromString[int64](vars["id"]) + _ = r.ParseForm() + + var err error + if ok { + switch vars["cmd"] { + case "new": + err = zm.actionNew(w, r, mode, id) + case "upd": + err = zm.actionUpd(w, r, mode, id) + } + } else { + err = errors.New("action: missing object id") + } + if err != nil { + _, _ = io.WriteString(w, "ERROR: "+err.Error()) + return + } + // redirect back to dashboard + http.Redirect(w, r, "/", http.StatusMovedPermanently) +} + +//---------------------------------------------------------------------- +// NEW: create zone, label or resource record +//---------------------------------------------------------------------- + +func (zm *ZoneMaster) actionNew(w http.ResponseWriter, r *http.Request, mode string, id int64) (err error) { + switch mode { + // new zone + case "zone": + name := r.FormValue("name") + // create private key + seed := make([]byte, 32) + if _, err = rand.Read(seed); err != nil { + return + } + var zp *crypto.ZonePrivate + kt := enums.GNS_TYPE_PKEY + if r.FormValue("keytype") == "EDKEY" { + kt = enums.GNS_TYPE_EDKEY + } + zp, err = crypto.NewZonePrivate(kt, seed) + if err != nil { + return + } + // add zone to database + zone := store.NewZone(name, zp) + err = zm.zdb.SetZone(zone) + + // notify listeners + zm.OnChange("zones", zone.ID, ChangeNew) + + // new label + case "label": + name := r.FormValue("name") + // add label to database + label := store.NewLabel(name) + label.Zone = id + err = zm.zdb.SetLabel(label) + + // notify listeners + zm.OnChange("labels", label.ID, ChangeNew) + + // new resource record + case "rr": + err = zm.newRec(w, r, id) + } + return +} + +//---------------------------------------------------------------------- + +// create new resource record from dialog data +func (zm *ZoneMaster) newRec(w http.ResponseWriter, r *http.Request, label int64) error { + // get list of parameters from resource record dialog + params := make(map[string]string) + for key, val := range r.Form { + params[key] = strings.Join(val, ",") + } + // parse RR type (and set prefix for map keys) + t, ok := util.CastFromString[enums.GNSType](params["type"]) + if !ok { + return errors.New("new: missing resource record type") + } + pf := dlgPrefix[t] + + // construct RR data + exp, flags := guiParse(params, pf) + rrdata, err := Map2RRData(t, params) + if err == nil { + // assemble record and store in database + rr := store.NewRecord(exp, t, flags, rrdata) + rr.Label = label + err = zm.zdb.SetRecord(rr) + + // notify listeners + zm.OnChange("records", rr.ID, ChangeNew) + } + return err +} + +//---------------------------------------------------------------------- +// UPD: update zone, label or resource record +//---------------------------------------------------------------------- + +func (zm *ZoneMaster) actionUpd(w http.ResponseWriter, r *http.Request, mode string, id int64) (err error) { + // handle type + switch mode { + case "zone": + // update zone name in database + var zone *store.Zone + if zone, err = zm.zdb.GetZone(id); err != nil { + return + } + zone.Name = r.FormValue("name") + zone.Modified = util.AbsoluteTimeNow() + err = zm.zdb.SetZone(zone) + + // notify listeners + zm.OnChange("zones", zone.ID, ChangeUpdate) + + case "label": + // update label name + label := store.NewLabel(r.FormValue("name")) + label.ID = id + label.Modified = util.AbsoluteTimeNow() + err = zm.zdb.SetLabel(label) + + // notify listeners + zm.OnChange("labels", label.ID, ChangeUpdate) + + case "rr": + // update record + err = zm.updRec(w, r, id) + } + return +} + +//---------------------------------------------------------------------- + +// update resource record +func (zm *ZoneMaster) updRec(w http.ResponseWriter, r *http.Request, id int64) error { + // get list of parameters from resource record dialog + oldParams := make(map[string]string) + newParams := make(map[string]string) + for key, val := range r.Form { + v := strings.Join(val, ",") + if strings.HasPrefix(key, "old_") { + oldParams[key[4:]] = v + } else { + newParams[key] = v + } + } + // parse RR type (and set prefix for map keys) + t, ok := util.CastFromString[enums.GNSType](oldParams["type"]) + if !ok { + return errors.New("new: missing resource record type") + } + pf := guiPrefix(t) + + // check for changed resource record + changed := false + for key, val := range newParams { + old, ok := oldParams[key] + if ok && old != val { + changed = true + break + } + } + if changed { + // reconstruct record from GUI parameters + rrData, err := Map2RRData(t, newParams) + if err != nil { + return err + } + exp, flags := guiParse(newParams, pf) + rec := store.NewRecord(exp, t, flags, rrData) + rec.ID = id + rec.Label, _ = util.CastFromString[int64](newParams["lid"]) + + // update in database + if err := zm.zdb.SetRecord(rec); err != nil { + return err + } + + // notify listeners + zm.OnChange("records", rec.ID, ChangeUpdate) + } + return nil +} + +//---------------------------------------------------------------------- +// Create new zone. label or resource record +//---------------------------------------------------------------------- + +type NewEditData struct { + Ref int64 // database id of reference object + Action string // "new" or "upd" action + Button string // "Add new" or "Update" + Names []string // list of names in use (ZONE,LABEL) + RRspecs []*enums.GNSSpec // list of allowed record types and flags (REC) + Params map[string]string // list of current values +} + +func (zm *ZoneMaster) new(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + var err error + data := new(NewEditData) + data.Action = "new" + data.Button = "Add new" + data.Params = make(map[string]string) + switch vars["mode"] { + + // new zone dialog + case "zone": + if data.Names, err = zm.zdb.GetNames("zones"); err != nil { + break + } + renderPage(w, data, "new_zone") + return + + // new label dialog + case "label": + // get reference id + id, ok := util.CastFromString[int64](vars["id"]) + if !ok { + err = errors.New("new label: missing zone id") + break + } + // get all existing label names for zone + stmt := fmt.Sprintf("labels where zid=%d", id) + if data.Names, err = zm.zdb.GetNames(stmt); err == nil { + data.Ref = id + data.Params["zone"], _ = zm.zdb.GetName("zones", id) + data.Params["zid"] = util.CastToString(id) + renderPage(w, data, "new_label") + return + } + + // new resource record dialog + case "rr": + // get reference id + id, ok := util.CastFromString[int64](vars["id"]) + if !ok { + err = errors.New("new record: missing label id") + break + } + // get all rrtypes used under given label + var rrs []*enums.GNSSpec + var label string + if rrs, label, err = zm.zdb.GetRRTypes(id); err == nil { + // compile a list of acceptable types for new records + data.RRspecs = compatibleRR(rrs, label) + data.Ref = id + data.Params["label"] = label + data.Params["lid"] = util.CastToString(id) + renderPage(w, data, "new_record") + return + } + } + // handle error + if err != nil { + _, _ = io.WriteString(w, "ERROR: "+err.Error()) + return + } + // redirect back to dashboard + http.Redirect(w, r, "/", http.StatusMovedPermanently) +} + +//---------------------------------------------------------------------- +// Edit zone, label or resource record +//---------------------------------------------------------------------- + +func (zm *ZoneMaster) edit(w http.ResponseWriter, r *http.Request) { + // get database id of edited object + vars := mux.Vars(r) + var err error + id, ok := util.CastFromString[int64](vars["id"]) + if !ok { + err = errors.New("missing edit id") + } else { + // create edit data instance + data := new(NewEditData) + data.Ref = id + data.Action = "upd" + data.Button = "Update" + data.Params = make(map[string]string) + + switch vars["mode"] { + + // edit zone name (type can't be changed) + case "zone": + // get all existing zone names (including the edited one!) + if data.Names, err = zm.zdb.GetNames("zones"); err != nil { + break + } + // get edited zone + var zone *store.Zone + if zone, err = zm.zdb.GetZone(id); err != nil { + break + } + // set edit attributes + data.Params["name"] = zone.Name + data.Params["keytype"] = guiKeyType(zone.Key.Type) + data.Params["keydata"] = zone.Key.Public().ID() + data.Params["prvdata"] = zone.Key.ID() + data.Params["created"] = guiTime(zone.Created) + data.Params["modified"] = guiTime(zone.Modified) + + // show dialog + renderPage(w, data, "edit_zone") + return + + // edit label name + case "label": + // get existing label names (including the edited label!) + stmt := fmt.Sprintf("labels where zid=%d", id) + if data.Names, err = zm.zdb.GetNames(stmt); err != nil { + break + } + // get edited label + var label *store.Label + if label, err = zm.zdb.GetLabel(id); err != nil { + return + } + // set edit parameters + data.Params["zone"], _ = zm.zdb.GetName("zones", id) + data.Params["zid"] = util.CastToString(label.Zone) + data.Params["name"] = label.Name + data.Params["created"] = guiTime(label.Created) + data.Params["modified"] = guiTime(label.Modified) + + // show dialog + renderPage(w, data, "edit_label") + return + + // edit resource record + case "rr": + if err = zm.editRec(w, r, data); err == nil { + return + } + } + } + // handle error + if err != nil { + _, _ = io.WriteString(w, "ERROR: "+err.Error()) + return + } + // redirect back to dashboard + http.Redirect(w, r, "/", http.StatusMovedPermanently) +} + +//---------------------------------------------------------------------- + +func (zm *ZoneMaster) editRec(w http.ResponseWriter, r *http.Request, data *NewEditData) (err error) { + // get edited resource record + var rec *store.Record + if rec, err = zm.zdb.GetRecord(data.Ref); err != nil { + return + } + // build map of attribute values + pf := dlgPrefix[rec.RType] + + // save shared attributes + data.Params["prefix"] = pf + data.Params["type"] = util.CastToString(int(rec.RType)) + data.Params["created"] = guiTime(rec.Created) + data.Params["modified"] = guiTime(rec.Modified) + data.Params["label"], _ = zm.zdb.GetName("labels", rec.Label) + data.Params["lid"] = util.CastToString(rec.Label) + if rec.Expire.IsNever() { + data.Params[pf+"never"] = "on" + } else { + data.Params[pf+"expires"] = htmlTime(rec.Expire) + } + if rec.Flags&enums.GNS_FLAG_PRIVATE != 0 { + data.Params[pf+"private"] = "on" + } + if rec.Flags&enums.GNS_FLAG_SHADOW != 0 { + data.Params[pf+"shadow"] = "on" + } + if rec.Flags&enums.GNS_FLAG_SUPPL != 0 { + data.Params[pf+"suppl"] = "on" + } + // get record instance + var inst rr.RR + if inst, err = rr.ParseRR(rec.RType, rec.Data); err == nil { + // add RR attributes to list + inst.ToMap(data.Params, pf) + } + // show dialog + renderPage(w, data, "edit_rec") + return +} + +//---------------------------------------------------------------------- +// Remove zone. label or resource record +//---------------------------------------------------------------------- + +func (zm *ZoneMaster) remove(w http.ResponseWriter, r *http.Request) { + // get database id of edited object + vars := mux.Vars(r) + var err error + id, ok := util.CastFromString[int64](vars["id"]) + if !ok { + err = errors.New("missing remove id") + } else { + switch vars["mode"] { + + // remove zone + case "zone": + // get zone from database + var zone *store.Zone + if zone, err = zm.zdb.GetZone(id); err != nil { + return + } + // remove zone in database + zone.Name = "" + if err = zm.zdb.SetZone(zone); err != nil { + return + } + zm.OnChange("zones", id, ChangeDelete) + + // remove label + case "label": + label := store.NewLabel("") + label.ID = id + if err = zm.zdb.SetLabel(label); err != nil { + return + } + zm.OnChange("labels", id, ChangeDelete) + + // remove resource record + case "rr": + rec := new(store.Record) + rec.ID = id + rec.Label = 0 + if err = zm.zdb.SetRecord(rec); err != nil { + return + } + zm.OnChange("records", id, ChangeDelete) + } + } + // handle error + if err != nil { + _, _ = io.WriteString(w, "ERROR: "+err.Error()) + return + } + // redirect back to dashboard + http.Redirect(w, r, "/", http.StatusMovedPermanently) +} + +//====================================================================== +// Helper methods +//====================================================================== + +// render a webpage with given data and template reference +func renderPage(w io.Writer, data interface{}, page string) { + // create content section + t := tpl.Lookup(page) + if t == nil { + _, _ = io.WriteString(w, "No template '"+page+"' found") + return + } + content := new(bytes.Buffer) + if err := t.Execute(content, data); err != nil { + _, _ = io.WriteString(w, err.Error()) + return + } + // emit final page + t = tpl.Lookup("main") + if t == nil { + _, _ = io.WriteString(w, "No main template found") + return + } + if err := t.Execute(w, content.String()); err != nil { + _, _ = io.WriteString(w, err.Error()) + } +} + +//---------------------------------------------------------------------- +// Debug rendering +//---------------------------------------------------------------------- + +// DebugData for error page +type DebugData struct { + Params map[string]string + RR string + Err error +} diff --git a/src/gnunet/service/zonemaster/gui.htpl b/src/gnunet/service/zonemaster/gui.htpl new file mode 100644 index 0000000..ffa9720 --- /dev/null +++ b/src/gnunet/service/zonemaster/gui.htpl @@ -0,0 +1,133 @@ +{{define "main"}} + + + + + {{template "css"}} + + +

GNUnet Zone Master

+
+ {{.}} + + + +{{end}} + +{{define "dashboard"}} +
+
    + {{if .}} + {{range $zi, $zone := .}} +
  • + {{$z := $zone.Zone}} + {{$z.Name}} [{{keytype $z.Key.Type}}: {{$zone.PubID}}] + + + (Created: {{date $z.Created}}, Modified: {{date $z.Modified}}) +
      + {{if $zone.Labels}} + {{range $li, $label := $zone.Labels}} +
    • + {{$l := $label.Label}} + {{$l.Name}} + + + (Created: {{date $l.Created}}, Modified: {{date $l.Modified}}) +
        + {{if $label.Records}} +
      • + + + + + + + + + + + {{range $ri, $rec := $label.Records}} + + + + + + + + + + {{end}} +
        TypeValueFlagsExpiresCreatedModifiedActions
        {{rrtype $rec.RType}}{{rrdata $rec.RType $rec.Data}}{{rrflags $rec.Flags}}{{date $rec.Expire}}{{date $rec.Created}}{{date $rec.Modified}} + + +
        +
      • + {{else}} +
      • No resource records for label defined yet.

      • + {{end}} +
      • + +
      • +
      +
    • + {{end}} + {{else}} +
    • No labels for zone defined yet.

    • + {{end}} +
    • + +
    • +
    +
  • + {{end}} + {{else}} +
  • +

    No zones defined yet.

    +
  • + {{end}} +
  • + +
  • +
+
+ +{{end}} \ No newline at end of file diff --git a/src/gnunet/service/zonemaster/gui_css.htpl b/src/gnunet/service/zonemaster/gui_css.htpl new file mode 100644 index 0000000..b31e714 --- /dev/null +++ b/src/gnunet/service/zonemaster/gui_css.htpl @@ -0,0 +1,253 @@ +{{define "css"}} + +{{end}} diff --git a/src/gnunet/service/zonemaster/gui_debug.htpl b/src/gnunet/service/zonemaster/gui_debug.htpl new file mode 100644 index 0000000..3112600 --- /dev/null +++ b/src/gnunet/service/zonemaster/gui_debug.htpl @@ -0,0 +1,14 @@ +{{define "debug"}} +

Debug

+

Parameters:

+
    + {{range $k,$v := .Params}} +
  • {{$k}} = {{$v}}
  • + {{end}} +
+

RR data:

+

{{.RR}}

+ {{if .Err}} +

Error: {{.Err}}

+ {{end}} +{{end}} \ No newline at end of file diff --git a/src/gnunet/service/zonemaster/gui_edit.htpl b/src/gnunet/service/zonemaster/gui_edit.htpl new file mode 100644 index 0000000..a4673b0 --- /dev/null +++ b/src/gnunet/service/zonemaster/gui_edit.htpl @@ -0,0 +1,130 @@ +{{define "edit_zone"}} + {{$type := index .Params "keytype"}} + {{$name := index .Params "name"}} +
+

Edit a [{{$type}}] GNS zone:

+

(Created: {{index .Params "created"}}, Last edited: {{index .Params "modified"}})

+
+ + + + + + + + + +
Zone name:
+

The type of the zone key cannot be changed. It is currently set to + {{if eq $type "PKEY"}}PKEY (Ed25519+EcDSA){{else}}EDKEY (EdDSA){{end}}:

+ + + + + + + + + +
Public key:{{index .Params "keydata"}}
Private key:{{index .Params "prvdata"}}
+
+ +
+

+
+ +{{end}} + +{{define "edit_label"}} + {{$name := index .Params "name"}} + {{$zone := index .Params "zone"}} +
+

Edit a GNS label for zone "{{$zone}}":

+

(Created: {{index .Params "created"}}, Last edited: {{index .Params "modified"}})

+
+ + + + + + + +
Name:
+ +
+

+
+ +{{end}} + +{{define "edit_rec"}} + {{$label := index .Params "label"}} +
+

Edit a resource record for label "{{$label}}":

+

(Created: {{index .Params "created"}}, Last edited: {{index .Params "modified"}})

+ {{$t := rritype (index .Params "type")}} + {{if eq $t "PKEY"}}{{template "PKEY" .}}{{end}} + {{if eq $t "EDKEY"}}{{template "EDKEY" .}}{{end}} + {{if eq $t "NICK"}}{{template "NICK" .}}{{end}} + {{if eq $t "LEHO"}}{{template "LEHO" .}}{{end}} + {{if eq $t "REDIRECT"}}{{template "REDIRECT" .}}{{end}} + {{if eq $t "GNS2DNS"}}{{template "GNS2DNS" .}}{{end}} + {{if eq $t "BOX"}}{{template "BOX" .}}{{end}} + {{if eq $t "DNS_CNAME"}}{{template "DNS_CNAME" .}}{{end}} + {{if eq $t "DNS_A"}}{{template "DNS_A" .}}{{end}} + {{if eq $t "DNS_AAAA"}}{{template "DNS_AAAA" .}}{{end}} + {{if eq $t "DNS_MX"}}{{template "DNS_MX" .}}{{end}} + {{if eq $t "DNS_TXT"}}{{template "DNS_TXT" .}}{{end}} +
+ +{{end}} \ No newline at end of file diff --git a/src/gnunet/service/zonemaster/gui_new.htpl b/src/gnunet/service/zonemaster/gui_new.htpl new file mode 100644 index 0000000..f470de9 --- /dev/null +++ b/src/gnunet/service/zonemaster/gui_new.htpl @@ -0,0 +1,114 @@ +{{define "new_zone"}} +
+

Creating a new GNS zone:

+
+ + + + + + + + + +
Zone name:
Key type: +  PKEY (Ed25519+EcDSA)
+  EDKEY (EdDSA) +
+ +
+ +
+ +{{end}} + +{{define "new_label"}} +
+

Creating a new GNS label for zone "{{index .Params "zone"}}":

+
+ + + + + +
Name:
+ +
+ +
+ +{{end}} + +{{define "new_record"}} +{{$data := .}} +
+

Creating a new GNS resource record for label "{{index .Params "label"}}":

+
+ {{range $i, $type := .RRspecs}} + + + {{end}} +
+ {{range $i, $spec := .RRspecs}} +
+ {{$t := rrtype $spec.Type}} + {{$pf := setspecs $data.Params $spec}} + {{if eq $t "PKEY"}}{{template "PKEY" $data}}{{end}} + {{if eq $t "EDKEY"}}{{template "EDKEY" $data}}{{end}} + {{if eq $t "NICK"}}{{template "NICK" $data}}{{end}} + {{if eq $t "LEHO"}}{{template "LEHO" $data}}{{end}} + {{if eq $t "REDIRECT"}}{{template "REDIRECT" $data}}{{end}} + {{if eq $t "GNS2DNS"}}{{template "GNS2DNS" $data}}{{end}} + {{if eq $t "BOX"}}{{template "BOX" $data}}{{end}} + {{if eq $t "DNS_CNAME"}}{{template "DNS_CNAME" $data}}{{end}} + {{if eq $t "DNS_A"}}{{template "DNS_A" $data}}{{end}} + {{if eq $t "DNS_AAAA"}}{{template "DNS_AAAA" $data}}{{end}} + {{if eq $t "DNS_MX"}}{{template "DNS_MX" $data}}{{end}} + {{if eq $t "DNS_TXT"}}{{template "DNS_TXT" $data}}{{end}} +
+ {{end}} +
+
+ +
+{{end}} diff --git a/src/gnunet/service/zonemaster/gui_rr.htpl b/src/gnunet/service/zonemaster/gui_rr.htpl new file mode 100644 index 0000000..360a734 --- /dev/null +++ b/src/gnunet/service/zonemaster/gui_rr.htpl @@ -0,0 +1,420 @@ +{{define "RRCommon"}} + + {{range $k, $v := .}} + + {{end}} + {{$pf := index . "prefix"}} + + Expires: + + Never +
+ At given date and time: + +
+ + + + Flags: + + Private
+ Shadow
+ Supplemental
+ + + {{if eq .Action "new"}} + + {{end}} +{{end}} + +{{define "PKEY"}} +

PKEY delegation

+
+ + + + + + + + + {{template "RRCommon" .Params}} + +
+ + Enter the public zone key (type + PKEY + ) in + Base32GNS + encoding: +
Key: + +
+
+{{end}} +{{define "EDKEY"}} +

EDKEY delegation

+
+ + + + + + + + + {{template "RRCommon" .Params}} + +
+ + Enter the public zone key (type + EDKEY + ) in + Base32GNS + encoding: +
Key: + +
+
+{{end}} +{{define "REDIRECT"}} +

REDIRECT (GNS delegation)

+
+ + + + + + + + + {{template "RRCommon" .Params}} + +
+ + Enter the redirected GNS name (see + specification + ): +
Name: + +
+
+{{end}} +{{define "LEHO"}} +

LEHO (legacy hostname)

+
+ + + + + + + {{template "RRCommon" .Params}} + +
Legacy hostname: + +
+
+{{end}} +{{define "NICK"}} +

NICK

+
+ + + + + + + {{template "RRCommon" .Params}} + +
Nick name: + +
+
+{{end}} +{{define "GNS2DNS"}} +

GNS2DNS delegation

+
+ + + + + + + + + + + + + {{template "RRCommon" .Params}} + +
+ + Enter DNS name and server as + specified. +
DNS name: + +
DNS server: + +
+
+{{end}} +{{define "BOX"}} +

BOX

+
+ + + + + + + + + + + + + + + + + + + + + {{template "RRCommon" .Params}} + +
+ + Enter protocol, service (port) and type of the boxed resource type as + specified: +
Protocol: + + + {{range $id,$name := boxprotos}} + +
Service: + + + {{range $id,$name := boxsvcs}} + +
Type: + SRV (Service description) +
+
+ + +
+
+
+ TLSA (TLS Association) +
+
+ + {{$x := index .Params "box_tlsa_usage"}} + +
+
+ + {{$x = index .Params "box_tlsa_selector"}} + +
+
+ + {{$x = index .Params "box_tlsa_match"}} + +
+
+
+ +
+
+
+
+{{end}} +{{define "DNS_A"}} +

DNS A (IPv4 address)

+
+ + + + + + + {{template "RRCommon" .Params}} + +
Address: + +
+
+{{end}} +{{define "DNS_AAAA"}} +

DNS AAAA (IPv6 address)

+
+ + + + + + + {{template "RRCommon" .Params}} + +
Address: + +
+
+{{end}} +{{define "DNS_CNAME"}} +

DNS CNAME delegation

+
+ + + + + + + {{template "RRCommon" .Params}} + +
Name: + +
+
+{{end}} +{{define "DNS_TXT"}} +

DNS TXT

+
+ + + + + + + {{template "RRCommon" .Params}} + +
Text: + +
+
+{{end}} +{{define "DNS_MX"}} +

DNS MX (Mailbox)

+
+ + + + + + + + + + + {{template "RRCommon" .Params}} + +
Priority: + {{$v := index .Params "dnsmx_prio"}} + +
Mailserver: + +
+
+{{end}} \ No newline at end of file diff --git a/src/gnunet/service/zonemaster/module.go b/src/gnunet/service/zonemaster/module.go new file mode 100644 index 0000000..8f7b000 --- /dev/null +++ b/src/gnunet/service/zonemaster/module.go @@ -0,0 +1,84 @@ +// 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 zonemaster + +import ( + "context" + + "gnunet/core" + "gnunet/enums" + "gnunet/service" + "gnunet/service/dht/blocks" +) + +//====================================================================== +// "GNUnet Zonemaster" implementation +//====================================================================== + +// Module handles namestore and identity requests. +type Module struct { + service.ModuleImpl + + // Use function references for calls to methods in other modules: + StoreLocal func(ctx context.Context, query *blocks.GNSQuery, block *blocks.GNSBlock) error + StoreRemote func(ctx context.Context, query blocks.Query, block blocks.Block) error +} + +// NewModule instantiates a new GNS module. +func NewModule(ctx context.Context, c *core.Core) (m *Module) { + m = &Module{ + ModuleImpl: *service.NewModuleImpl(), + } + if c != nil { + // register as listener for core events + listener := m.ModuleImpl.Run(ctx, m.event, m.Filter(), 0, nil) + c.Register("zonemaster", listener) + } + return +} + +//---------------------------------------------------------------------- + +// Filter returns the event filter for the service +func (m *Module) Filter() *core.EventFilter { + f := core.NewEventFilter() + f.AddMsgType(enums.MSG_NAMESTORE_ZONE_ITERATION_START) + return f +} + +// Event handler +func (m *Module) event(ctx context.Context, ev *core.Event) { + +} + +//---------------------------------------------------------------------- + +// Export functions +func (m *Module) Export(fcn map[string]any) { + // add exported functions from module +} + +// Import functions +func (m *Module) Import(fcn map[string]any) { + // resolve imports from other modules + m.StoreLocal, _ = fcn["namecache:put"].(func(ctx context.Context, query *blocks.GNSQuery, block *blocks.GNSBlock) error) + m.StoreRemote, _ = fcn["dht:put"].(func(ctx context.Context, query blocks.Query, block blocks.Block) error) +} + +//---------------------------------------------------------------------- diff --git a/src/gnunet/service/zonemaster/records.go b/src/gnunet/service/zonemaster/records.go new file mode 100644 index 0000000..3015893 --- /dev/null +++ b/src/gnunet/service/zonemaster/records.go @@ -0,0 +1,348 @@ +// 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 zonemaster + +import ( + "encoding/hex" + "errors" + "fmt" + "gnunet/enums" + "gnunet/service/gns/rr" + "gnunet/util" + "net" + "time" + + "github.com/bfix/gospel/data" +) + +var ( + // list of managed RR types + rrtypes = []enums.GNSType{ + enums.GNS_TYPE_PKEY, // PKEY zone delegation + enums.GNS_TYPE_EDKEY, // EDKEY zone delegation + enums.GNS_TYPE_REDIRECT, // GNS delegation by name + enums.GNS_TYPE_GNS2DNS, // DNS delegation by name + enums.GNS_TYPE_NICK, // Nick name + enums.GNS_TYPE_LEHO, // Legacy hostname + enums.GNS_TYPE_BOX, // Boxed resource record + enums.GNS_TYPE_DNS_A, // IPv4 address + enums.GNS_TYPE_DNS_AAAA, // IPv6 address + enums.GNS_TYPE_DNS_CNAME, // CNAME in DNS + enums.GNS_TYPE_DNS_TXT, // DNS TXT + enums.GNS_TYPE_DNS_MX, // Mailbox + } +) + +//====================================================================== +// Convert binary resource records to ParameterSet and vice-versa. +// The map keys must match the HTML names of dialog fields. +//====================================================================== + +//---------------------------------------------------------------------- +// GUI rendering hepers +//---------------------------------------------------------------------- + +var ( + // List of key prefixes based on RR type + dlgPrefix = map[enums.GNSType]string{ + enums.GNS_TYPE_PKEY: "pkey_", + enums.GNS_TYPE_EDKEY: "edkey_", + enums.GNS_TYPE_REDIRECT: "redirect_", + enums.GNS_TYPE_LEHO: "leho_", + enums.GNS_TYPE_NICK: "nick_", + enums.GNS_TYPE_GNS2DNS: "gns2dns_", + enums.GNS_TYPE_BOX: "box_", + enums.GNS_TYPE_DNS_A: "dnsa_", + enums.GNS_TYPE_DNS_AAAA: "dnsaaaa_", + enums.GNS_TYPE_DNS_CNAME: "dnscname_", + enums.GNS_TYPE_DNS_TXT: "dnstxt_", + enums.GNS_TYPE_DNS_MX: "dnsmx_", + } +) + +// convert GNUnet time to string for HTML +func htmlTime(ts util.AbsoluteTime) string { + if ts.IsNever() { + return "" + } + return time.UnixMicro(int64(ts.Val)).Format(timeHTML) +} + +func guiTime(ts util.AbsoluteTime) string { + if ts.IsNever() { + return "Never" + } + return time.UnixMicro(int64(ts.Val)).Format(timeGUI) +} + +// convert zone key type to string +func guiKeyType(t enums.GNSType) string { + switch t { + case enums.GNS_TYPE_PKEY: + return "PKEY" + case enums.GNS_TYPE_EDKEY: + return "EDKEY" + } + return "???" +} + +func guiRRdata(t enums.GNSType, buf []byte) string { + // get record instance + inst, err := rr.ParseRR(t, buf) + if err != nil { + return "" + } + // type-dependent rendering + switch rec := inst.(type) { + case *rr.PKEY: + return fmt.Sprintf("%s", rec.ZoneKey.ID()) + case *rr.EDKEY: + return fmt.Sprintf("%s", rec.ZoneKey.ID()) + case *rr.REDIRECT: + return fmt.Sprintf("%s", rec.Name) + case *rr.NICK: + return fmt.Sprintf("%s", rec.Name) + case *rr.LEHO: + return fmt.Sprintf("%s", rec.Name) + case *rr.CNAME: + return fmt.Sprintf("%s", rec.Name) + case *rr.TXT: + return fmt.Sprintf("%s", rec.Text) + case *rr.DNSA: + return fmt.Sprintf("%s", rec.Addr.String()) + case *rr.DNSAAAA: + return fmt.Sprintf("%s", rec.Addr.String()) + case *rr.MX: + s := fmt.Sprintf("[%d] ", rec.Prio) + return s + fmt.Sprintf("%s", rec.Server) + case *rr.BOX: + s := fmt.Sprintf("%s/", rr.GetServiceName(rec.Svc, rec.Proto)) + s += fmt.Sprintf("%s ", rr.GetProtocolName(rec.Proto)) + switch rec.Type { + case enums.GNS_TYPE_DNS_TLSA: + tlsa := new(rr.TLSA) + _ = data.Unmarshal(tlsa, rec.RR) + s += "TLSA[
" + s += fmt.Sprintf("∙ Usage: %s
", rr.TLSAUsage[tlsa.Usage]) + s += fmt.Sprintf("∙ Selector: %s
", rr.TLSASelector[tlsa.Selector]) + s += fmt.Sprintf("∙ Match: %s
", rr.TLSAMatch[tlsa.Match]) + s += "∙ CertData:
" + cert := hex.EncodeToString(tlsa.Cert) + for len(cert) > 32 { + s += "  " + cert[:32] + "
" + cert = cert[32:] + } + s += "  " + cert + "
]" + return s + case enums.GNS_TYPE_DNS_SRV: + srv, _ := util.ReadCString(rec.RR, 0) + s += fmt.Sprintf("SRV[ %s ]", srv) + return s + } + case *rr.GNS2DNS: + s := fmt.Sprintf("%s (Resolver: ", rec.Name) + return s + fmt.Sprintf("%s)", rec.Server) + } + return "(unknown)" +} + +// get prefix for GUI fields for given RR type +func guiPrefix(t enums.GNSType) string { + pf, ok := dlgPrefix[t] + if !ok { + return "" + } + return pf +} + +// parse expiration time and flags from GUI parameters +func guiParse(params map[string]string, pf string) (exp util.AbsoluteTime, flags enums.GNSFlag) { + // parse expiration time + exp = util.AbsoluteTimeNever() + if _, ok := params[pf+"never"]; !ok { + ts, _ := time.Parse(timeHTML, params[pf+"expires"]) + exp.Val = uint64(ts.UnixMicro()) + } + // parse flags + flags = 0 + if _, ok := params[pf+"private"]; ok { + flags |= enums.GNS_FLAG_PRIVATE + } + if _, ok := params[pf+"shadow"]; ok { + flags |= enums.GNS_FLAG_SHADOW + } + if _, ok := params[pf+"suppl"]; ok { + flags |= enums.GNS_FLAG_SUPPL + } + return +} + +//---------------------------------------------------------------------- +// Convert RR to string-keyed map and vice-versa. +//---------------------------------------------------------------------- + +// RRData2Map converts resource record data in to a map +func RRData2Map(t enums.GNSType, buf []byte) (set map[string]string) { + pf := dlgPrefix[t] + set = make(map[string]string) + switch t { + // Ed25519 public key + case enums.GNS_TYPE_PKEY, + enums.GNS_TYPE_EDKEY: + set[pf+"data"] = util.EncodeBinaryToString(buf) + + // Name string data + case enums.GNS_TYPE_REDIRECT, + enums.GNS_TYPE_NICK, + enums.GNS_TYPE_LEHO, + enums.GNS_TYPE_DNS_CNAME: + set[pf+"name"], _ = util.ReadCString(buf, 0) + + // DNS TXT + case enums.GNS_TYPE_DNS_TXT: + set[pf+"text"], _ = util.ReadCString(buf, 0) + + // IPv4/IPv6 address + case enums.GNS_TYPE_DNS_A, + enums.GNS_TYPE_DNS_AAAA: + addr := net.IP(buf) + set[pf+"addr"] = addr.String() + + // DNS MX + case enums.GNS_TYPE_DNS_MX: + mx := new(rr.MX) + _ = data.Unmarshal(mx, buf) + set[pf+"prio"] = util.CastToString(mx.Prio) + set[pf+"host"] = mx.Server + + // BOX + case enums.GNS_TYPE_BOX: + // get BOX from data + box := rr.NewBOX(buf) + set[pf+"proto"] = util.CastToString(box.Proto) + set[pf+"svc"] = util.CastToString(box.Svc) + set[pf+"type"] = util.CastToString(box.Type) + + // handle TLSA and SRV cases + switch box.Type { + case enums.GNS_TYPE_DNS_TLSA: + tlsa := new(rr.TLSA) + _ = data.Unmarshal(tlsa, box.RR) + set[pf+"tlsa_usage"] = util.CastToString(tlsa.Usage) + set[pf+"tlsa_selector"] = util.CastToString(tlsa.Selector) + set[pf+"tlsa_match"] = util.CastToString(tlsa.Match) + set[pf+"tlsa_cert"] = hex.EncodeToString(tlsa.Cert) + + case enums.GNS_TYPE_DNS_SRV: + set[pf+"srv_host"], _ = util.ReadCString(box.RR, 0) + } + + // GNS2DNS + case enums.GNS_TYPE_GNS2DNS: + list := util.StringList(buf) + set[pf+"name"] = list[0] + set[pf+"server"] = list[1] + } + return +} + +// Map2RRData converts a map to resource record data +func Map2RRData(t enums.GNSType, set map[string]string) (buf []byte, err error) { + pf := dlgPrefix[t] + switch t { + // Ed25519 public key + case enums.GNS_TYPE_PKEY, + enums.GNS_TYPE_EDKEY: + return util.DecodeStringToBinary(set[pf+"data"], 36) + + // Name string data + case enums.GNS_TYPE_REDIRECT, + enums.GNS_TYPE_NICK, + enums.GNS_TYPE_LEHO, + enums.GNS_TYPE_DNS_CNAME: + return util.WriteCString(set[pf+"name"]), nil + + // DNS TXT + case enums.GNS_TYPE_DNS_TXT: + return util.WriteCString(set[pf+"text"]), nil + + // IPv4/IPv6 address + case enums.GNS_TYPE_DNS_A, + enums.GNS_TYPE_DNS_AAAA: + buf := net.ParseIP(set[pf+"addr"]) + if buf == nil { + return nil, errors.New("ParseIP failed") + } + return buf, nil + + // DNS MX + case enums.GNS_TYPE_DNS_MX: + mx := new(rr.MX) + mx.Prio, _ = util.CastFromString[uint16](set[pf+"prio"]) + mx.Server = set[pf+"host"] + return data.Marshal(mx) + + // BOX + case enums.GNS_TYPE_BOX: + // assemble box + box := new(rr.BOX) + box.Proto, _ = util.CastFromString[uint16](set[pf+"proto"]) + box.Svc, _ = util.CastFromString[uint16](set[pf+"svc"]) + box.Type, _ = util.CastFromString[enums.GNSType](set[pf+"type"]) + + // handle TLSA and SRV cases + switch box.Type { + case enums.GNS_TYPE_DNS_TLSA: + tlsa := new(rr.TLSA) + tlsa.Usage, _ = util.CastFromString[uint8](set[pf+"tlsa_usage"]) + tlsa.Selector, _ = util.CastFromString[uint8](set[pf+"tlsa_selector"]) + tlsa.Match, _ = util.CastFromString[uint8](set[pf+"tlsa_match"]) + tlsa.Cert, _ = hex.DecodeString(set[pf+"tlsa_cert"]) + box.RR, _ = data.Marshal(tlsa) + + case enums.GNS_TYPE_DNS_SRV: + box.RR = util.WriteCString(set[pf+"srv_host"]) + } + return data.Marshal(box) + + // GNS2DNS + case enums.GNS_TYPE_GNS2DNS: + buf := util.WriteCString(set[pf+"name"]) + return append(buf, util.WriteCString(set[pf+"server"])...), nil + } + return nil, errors.New("unknown RR type") +} + +//====================================================================== +// Get list of allowed new RRs given a set of existing RRs. +//====================================================================== + +// Create a list of compatible record types from list of +// existing record types. +func compatibleRR(in []*enums.GNSSpec, label string) (out []*enums.GNSSpec) { + for _, t := range rrtypes { + if ok, forced := rr.CanCoexist(t, in, label); ok { + out = append(out, &enums.GNSSpec{ + Type: t, + Flags: forced, + }) + } + } + return +} diff --git a/src/gnunet/service/zonemaster/rpc.go b/src/gnunet/service/zonemaster/rpc.go new file mode 100644 index 0000000..2060e56 --- /dev/null +++ b/src/gnunet/service/zonemaster/rpc.go @@ -0,0 +1,24 @@ +// 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 zonemaster + +import "gnunet/service" + +func (s *Service) InitRPC(rpc *service.JRPCServer) { +} diff --git a/src/gnunet/service/zonemaster/service.go b/src/gnunet/service/zonemaster/service.go new file mode 100644 index 0000000..c73857f --- /dev/null +++ b/src/gnunet/service/zonemaster/service.go @@ -0,0 +1,150 @@ +// 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 zonemaster + +import ( + "context" + "fmt" + "io" + + "gnunet/config" + "gnunet/core" + "gnunet/crypto" + "gnunet/message" + "gnunet/service" + "gnunet/service/dht/blocks" + "gnunet/transport" + "gnunet/util" + + "github.com/bfix/gospel/logger" +) + +type ZoneIterator struct { + zk *crypto.ZonePrivate +} + +//---------------------------------------------------------------------- +// "GNUnet Zonemaster" service implementation: +// The zonemaster service handles Namestore messages +//---------------------------------------------------------------------- + +// Service implements a GNS service +type Service struct { + Module + + ZoneIters *util.Map[uint32, *ZoneIterator] +} + +// NewService creates a new GNS service instance +func NewService(ctx context.Context, c *core.Core) service.Service { + // instantiate service + mod := NewModule(ctx, c) + srv := &Service{ + Module: *mod, + ZoneIters: util.NewMap[uint32, *ZoneIterator](), + } + // set external function references (external services) + srv.StoreLocal = srv.StoreNamecache + srv.StoreRemote = srv.StoreDHT + + return srv +} + +// 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) + + for { + // receive next message from client + reqID++ + logger.Printf(logger.DBG, "[zonemaster:%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, "[zonemaster:%d:%d] Client channel closed.\n", id, reqID) + } else if err == service.ErrConnectionInterrupted { + logger.Printf(logger.INFO, "[zonemaster:%d:%d] Service operation interrupted.\n", id, reqID) + } else { + logger.Printf(logger.ERROR, "[zonemaster:%d:%d] Message-receive failed: %s\n", id, reqID, err.Error()) + } + break + } + logger.Printf(logger.INFO, "[zonemaster:%d:%d] Received request: %v\n", id, reqID, msg) + + // handle message + valueCtx := context.WithValue(ctx, core.CtxKey("label"), fmt.Sprintf(":%d:%d", id, reqID)) + s.HandleMessage(valueCtx, nil, msg, mc) + } + // close client connection + mc.Close() + + // cancel all tasks running for this session/connection + logger.Printf(logger.INFO, "[zonemaster:%d] Start closing session...\n", id) + cancel() +} + +// Handle a single incoming message +func (s *Service) HandleMessage(ctx context.Context, sender *util.PeerID, msg message.Message, back transport.Responder) bool { + // assemble log label + label := "" + if v := ctx.Value("label"); v != nil { + label, _ = v.(string) + } + // perform lookup + switch m := msg.(type) { + + // start new zone iteration + case *message.NamestoreZoneIterStartMsg: + zi := new(ZoneIterator) + zi.zk = m.ZoneKey + s.ZoneIters.Put(m.ID, zi, 0) + + default: + //---------------------------------------------------------- + // UNKNOWN message type received + //---------------------------------------------------------- + logger.Printf(logger.ERROR, "[zonemaster%s] Unhandled message of type (%s)\n", label, msg.Type()) + return false + } + return true +} + +// storeDHT stores a GNS block in the DHT. +func (s *Service) StoreDHT(ctx context.Context, query blocks.Query, block blocks.Block) (err error) { + // assemble DHT request + req := message.NewDHTP2PPutMsg(block) + req.Flags = query.Flags() + req.Key = query.Key().Clone() + + // store block + _, err = service.RequestResponse(ctx, "zonemaster", "dht", config.Cfg.DHT.Service.Socket, req, false) + return +} + +// storeNamecache stores a GNS block in the local namecache. +func (s *Service) StoreNamecache(ctx context.Context, query *blocks.GNSQuery, block *blocks.GNSBlock) (err error) { + // assemble Namecache request + req := message.NewNamecacheCacheMsg(block) + + // get response from Namecache service + _, err = service.RequestResponse(ctx, "zonemaster", "namecache", config.Cfg.Namecache.Service.Socket, req, false) + return +} diff --git a/src/gnunet/service/zonemaster/zonemaster.go b/src/gnunet/service/zonemaster/zonemaster.go new file mode 100644 index 0000000..7c2a13c --- /dev/null +++ b/src/gnunet/service/zonemaster/zonemaster.go @@ -0,0 +1,179 @@ +// 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 zonemaster + +import ( + "context" + "gnunet/config" + "gnunet/enums" + "gnunet/service/dht/blocks" + "gnunet/service/store" + "gnunet/util" + "time" + + "github.com/bfix/gospel/logger" +) + +//====================================================================== +// "GNS ZoneMaster" implementation: +// Manage and publish local zone records +//====================================================================== + +// ZoneMaster instance +type ZoneMaster struct { + cfg *config.Config // Zonemaster configuration + zdb *store.ZoneDB // ZoneDB connection + srv *Service // NameStore service +} + +// NewZoneMaster initializes a new zone master instance. +func NewZoneMaster(cfg *config.Config, srv *Service) *ZoneMaster { + zm := new(ZoneMaster) + zm.cfg = cfg + return zm +} + +// Run zone master: connect to zone database and start the RPC/HTTP +// services as background processes. Periodically publish GNS blocks +// into the DHT. +func (zm *ZoneMaster) Run(ctx context.Context) { + // connect to database + logger.Println(logger.INFO, "[zonemaster] Connecting to zone database...") + dbFile, ok := util.GetParam[string](zm.cfg.ZoneMaster.Storage, "file") + if !ok { + logger.Printf(logger.ERROR, "[zonemaster] missing database file specification") + return + } + var err error + if zm.zdb, err = store.OpenZoneDB(dbFile); err != nil { + logger.Printf(logger.ERROR, "[zonemaster] open database: %v", err) + return + } + defer zm.zdb.Close() + + // start HTTP GUI + zm.startGUI(ctx) + /* + // publish on start-up + if err = zm.Publish(ctx); err != nil { + logger.Printf(logger.ERROR, "[zonemaster] initial publish failed: %s", err.Error()) + return + } + */ + // periodically publish GNS blocks to the DHT + tick := time.NewTicker(time.Duration(zm.cfg.ZoneMaster.Period) * time.Second) +loop: + for { + select { + case <-tick.C: + if err := zm.Publish(ctx); err != nil { + logger.Printf(logger.ERROR, "[zonemaster] periodic publish failed: %s", err.Error()) + } + + // check for termination + case <-ctx.Done(): + break loop + } + } +} + +// OnChange is called if a zone or record has changed or was inserted +func (zm *ZoneMaster) OnChange(table string, id int64, mode int) { +} + +// Publish all zone labels to the DHT +func (zm *ZoneMaster) Publish(ctx context.Context) error { + // collect all zones + zones, err := zm.zdb.GetZones("") + if err != nil { + return err + } + for _, z := range zones { + // collect labels for zone + var labels []*store.Label + if labels, err = zm.zdb.GetLabels("zid=%d", z.ID); err != nil { + return err + } + for _, l := range labels { + // publish label + if err = zm.PublishZoneLabel(ctx, z, l); err != nil { + return err + } + } + } + return nil +} + +// PublishZoneLabel with public records +func (zm *ZoneMaster) PublishZoneLabel(ctx context.Context, zone *store.Zone, label *store.Label) error { + zk := zone.Key.Public() + logger.Printf(logger.INFO, "[zonemaster] Publishing label '%s' of zone %s", label.Name, zk.ID()) + + // collect public records for zone label + recs, err := zm.zdb.GetRecords("lid=%d and flags&%d = 0", label.ID, enums.GNS_FLAG_PRIVATE) + if err != nil { + return err + } + // assemble record set and find earliest expiration + expire := util.AbsoluteTimeNever() + rrSet := blocks.NewRecordSet() + for _, r := range recs { + if r.Expire.Compare(expire) < 0 { + expire = r.Expire + } + rrSet.AddRecord(&r.ResourceRecord) + } + rrSet.SetPadding() + if rrSet.Count == 0 { + logger.Println(logger.INFO, "[zonemaster] No resource records -- skipped") + return nil + } + + // assemble GNS query + query := blocks.NewGNSQuery(zk, label.Name) + + // assemble, encrypt and sign GNS block + blk, _ := blocks.NewGNSBlock().(*blocks.GNSBlock) + + blk.Body.Expire = expire + blk.Body.Data, err = zk.Encrypt(rrSet.Bytes(), label.Name, expire) + if err != nil { + return err + } + dzk, _, err := zone.Key.Derive(label.Name, "gns") + if err != nil { + return err + } + if err = blk.Sign(dzk); err != nil { + return err + } + + // DEBUG: + // logger.Printf(logger.DBG, "[zonemaster] Query key = %s", hex.EncodeToString(query.Key().Data)) + // logger.Printf(logger.DBG, "[zonemaster] Block data = %s", hex.EncodeToString(blk.Bytes())) + + // publish GNS block to DHT and Namecache + if err = zm.srv.StoreDHT(ctx, query, blk); err != nil { + return err + } + if err = zm.srv.StoreNamecache(ctx, query, blk); err != nil { + return err + } + return nil +} diff --git a/src/gnunet/util/array.go b/src/gnunet/util/array.go index 97d884a..b4a1776 100644 --- a/src/gnunet/util/array.go +++ b/src/gnunet/util/array.go @@ -19,6 +19,7 @@ package util import ( + "bytes" "fmt" ) @@ -114,14 +115,13 @@ func CopyAlignedBlock(out, in []byte) { // not terminated, it is skipped. func StringList(b []byte) []string { res := make([]string, 0) - str := "" - for _, ch := range b { - if ch == 0 { + pos := 0 + var str string + for pos != -1 { + str, pos = ReadCString(b, pos) + if len(str) > 0 { res = append(res, str) - str = "" - continue } - str += string(ch) } return res } @@ -137,3 +137,11 @@ func ReadCString(buf []byte, pos int) (string, int) { } return "", -1 } + +// WriteCString returns the binary C-representation of a string +func WriteCString(s string) []byte { + buf := new(bytes.Buffer) + _, _ = buf.WriteString(s) + _ = buf.WriteByte(0) + return buf.Bytes() +} diff --git a/src/gnunet/util/misc.go b/src/gnunet/util/misc.go index c5fd308..aeda678 100644 --- a/src/gnunet/util/misc.go +++ b/src/gnunet/util/misc.go @@ -21,6 +21,7 @@ package util import ( "encoding/hex" "encoding/json" + "fmt" "strings" "github.com/bfix/gospel/data" @@ -73,6 +74,21 @@ func GetParam[V any](params ParameterSet, key string) (i V, ok bool) { return } +// CastFromString a string to given type (only works for intrinsic type) +func CastFromString[V any](s string) (i V, ok bool) { + num, err := fmt.Sscanf(s, "%v", &i) + ok = true + if err != nil || num != 1 { + ok = false + } + return +} + +// CastToString returns a string representation (for intrinsic types) +func CastToString(v any) string { + return fmt.Sprintf("%v", v) +} + //---------------------------------------------------------------------- // additional helpers //---------------------------------------------------------------------- diff --git a/src/gnunet/util/time.go b/src/gnunet/util/time.go index cf3739a..c49755c 100644 --- a/src/gnunet/util/time.go +++ b/src/gnunet/util/time.go @@ -60,6 +60,11 @@ func AbsoluteTimeNever() AbsoluteTime { return AbsoluteTime{math.MaxUint64} } +// IsNever returns true if time is "never" +func (t AbsoluteTime) IsNever() bool { + return t.Val == math.MaxUint64 +} + // Epoch returns the seconds since Unix epoch. func (t AbsoluteTime) Epoch() uint64 { return t.Val / 1000000 -- cgit v1.2.3