gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[gnunet-go] branch master updated: Initial revision of Zonemaster implem


From: gnunet
Subject: [gnunet-go] branch master updated: Initial revision of Zonemaster implementation.
Date: Sat, 22 Oct 2022 16:59:03 +0200

This is an automated email from the git hooks/post-receive script.

bernd-fix pushed a commit to branch master
in repository gnunet-go.

The following commit(s) were added to refs/heads/master by this push:
     new 6ed1dfa  Initial revision of Zonemaster implementation.
6ed1dfa is described below

commit 6ed1dfac9e0ac7a24114198ae8a364388cb55528
Author: Bernd Fix <brf@hoi-polloi.org>
AuthorDate: Sat Oct 22 16:57:16 2022 +0200

    Initial revision of Zonemaster implementation.
---
 README.md                                          |  11 +-
 src/gnunet/build.sh                                |   7 +-
 src/gnunet/cmd/gnunet-service-gns-go/main.go       |  13 +-
 .../main.go                                        |  83 ++-
 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_namecache.go                |  97 +--
 src/gnunet/message/msg_namestore.go                | 199 ++++++
 src/gnunet/service/client.go                       |  13 +-
 src/gnunet/service/dht/blocks/gns.go               | 108 +++-
 .../dht/blocks/gns_test.go}                        |  99 ++-
 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, 5109 insertions(+), 583 deletions(-)

diff --git a/README.md b/README.md
index 79825da..bbaced7 100644
--- a/README.md
+++ b/README.md
@@ -213,13 +213,16 @@ The above configuration will expect a network of 10 nodes 
and has a single
 bootstrap node (the first DHTU node in the testbed). `gnunet-go` will listen
 on port 2086.
 
+Check the configuration for path definitions (especially 
`/var/libg/gnunet/...`)
+and make sure the folders exist and have R/W permissions for the user running
+the test.
+
 #### Running the `gnunet-go`node
 
 Run the following commands to start the `gnunet-go` node:
 
 ```bash
-rm -rf /tmp/gnunet-system-runtime
-mkdir -p /tmp/gnunet-system-runtime
+rm -f /tmp/gnunet-system-runtime/*-go.sock
 ${GOPATH}/bin/gnunet-service-dht-go -c dhtu-config.json 2>&1 | tee run.log
 ```
 
@@ -389,9 +392,9 @@ func main() {
 * edit `go.mod` and add at end of file:
 
 ```bash
-require gnunet v0.1.27
+require gnunet v0.1.34
 
-replace gnunet v0.1.27 => /home/user/gnunet-go/src/gnunet
+replace gnunet v0.1.34 => /home/user/gnunet-go/src/gnunet
 ```
 
 * run `go mod tidy`
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/gnunet-service-gns-go/main.go 
b/src/gnunet/cmd/zonemaster-go/main.go
similarity index 57%
copy from src/gnunet/cmd/gnunet-service-gns-go/main.go
copy to src/gnunet/cmd/zonemaster-go/main.go
index ebf2151..1e4b985 100644
--- a/src/gnunet/cmd/gnunet-service-gns-go/main.go
+++ b/src/gnunet/cmd/zonemaster-go/main.go
@@ -28,80 +28,76 @@ import (
        "time"
 
        "gnunet/config"
-       "gnunet/core"
        "gnunet/service"
-       "gnunet/service/gns"
+       "gnunet/service/zonemaster"
 
        "github.com/bfix/gospel/logger"
 )
 
 func main() {
        defer func() {
-               logger.Println(logger.INFO, "[gns] Bye.")
+               logger.Println(logger.INFO, "[zonemaster] Bye.")
                // flush last messages
                logger.Flush()
        }()
-       logger.Println(logger.INFO, "[gns] Starting service...")
+       // intro
+       logger.SetLogLevel(logger.DBG)
+       logger.Println(logger.INFO, "[zonemaster] Starting service...")
 
        var (
                cfgFile  string
-               socket   string
-               param    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(&socket, "s", "", "GNS service socket")
-       flag.StringVar(&param, "p", "", "socket parameters (<key>=<value>,...)")
-       flag.IntVar(&logLevel, "L", logger.INFO, "GNS log level (default: 
INFO)")
+       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, "[gns] Invalid configuration file: 
%s\n", err.Error())
+               logger.Printf(logger.ERROR, "[zonemaster] Invalid configuration 
file: %s\n", err.Error())
                return
        }
 
-       // apply configuration (from file and command-line)
-       logger.SetLogLevel(logLevel)
-       if len(socket) == 0 {
-               socket = config.Cfg.GNS.Service.Socket
+       // apply configuration
+       if config.Cfg.Logging.Level > 0 {
+               logLevel = config.Cfg.Logging.Level
        }
-       params := make(map[string]string)
-       if len(param) == 0 {
-               for _, p := range strings.Split(param, ",") {
-                       kv := strings.SplitN(p, "=", 2)
-                       params[kv[0]] = kv[1]
-               }
-       } else {
-               params = config.Cfg.GNS.Service.Params
+       logger.SetLogLevel(logLevel)
+       if len(gui) > 0 {
+               config.Cfg.ZoneMaster.GUI = gui
        }
 
-       // instantiate core service
+       // start a new namestore service under zonemaster umbrella
        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())
+       srv, ok := zonemaster.NewService(ctx, nil).(*zonemaster.Service)
+       if !ok {
+               logger.Println(logger.ERROR, "[zonemaster] Failed to create 
service")
                return
        }
-       defer c.Shutdown()
-
-       // start a new GNS service
-       gns := gns.NewService(ctx, c)
-       srv := service.NewSocketHandler("gns", gns)
-       if err = srv.Start(ctx, socket, params); err != nil {
-               logger.Printf(logger.ERROR, "[gns] Error: '%s'", err.Error())
-               return
+       // start 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, "[gns] RPC must have a 
TCP/IP endpoint")
+                       logger.Println(logger.ERROR, "[zonemaster] RPC must 
have a TCP/IP endpoint")
                        return
                }
                config.Cfg.RPC.Endpoint = parts[1]
@@ -110,12 +106,11 @@ func main() {
        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, "[gns] RPC failed to start: 
%s", err.Error())
+                       logger.Printf(logger.ERROR, "[zonemaster] RPC failed to 
start: %s", err.Error())
                        return
                }
-               gns.InitRPC(rpc)
+               srv.InitRPC(rpc)
        }
-
        // handle OS signals
        sigCh := make(chan os.Signal, 5)
        signal.Notify(sigCh)
@@ -130,24 +125,20 @@ loop:
                case sig := <-sigCh:
                        switch sig {
                        case syscall.SIGKILL, syscall.SIGINT, syscall.SIGTERM:
-                               logger.Printf(logger.INFO, "[gns] Terminating 
service (on signal '%s')\n", sig)
+                               logger.Printf(logger.INFO, "[zonemaster] 
Terminating service (on signal '%s')\n", sig)
                                break loop
                        case syscall.SIGHUP:
-                               logger.Println(logger.INFO, "[gns] SIGHUP")
+                               logger.Println(logger.INFO, "[zonemaster] 
SIGHUP")
                        case syscall.SIGURG:
                                // TODO: 
https://github.com/golang/go/issues/37942
                        default:
-                               logger.Println(logger.INFO, "[gns] Unhandled 
signal: "+sig.String())
+                               logger.Println(logger.INFO, "[zonemaster] 
Unhandled signal: "+sig.String())
                        }
                // handle heart beat
                case now := <-tick.C:
-                       logger.Println(logger.INFO, "[gns] Heart beat at 
"+now.String())
+                       logger.Println(logger.INFO, "[zonemaster] Heart beat at 
"+now.String())
                }
        }
-
        // terminating service
        cancel()
-       if err = srv.Stop(); err != nil {
-               logger.Printf(logger.ERROR, "[gns] Failed to stop service: %s", 
err.Error())
-       }
 }
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 <http://www.gnu.org/licenses/>.
+//
+// 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 <http://www.gnu.org/licenses/>.
+//
+// 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_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 <http://www.gnu.org/licenses/>.
+//
+// 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/message/msg_gns_test.go 
b/src/gnunet/service/dht/blocks/gns_test.go
similarity index 78%
rename from src/gnunet/message/msg_gns_test.go
rename to src/gnunet/service/dht/blocks/gns_test.go
index 034e1a8..260d83b 100644
--- a/src/gnunet/message/msg_gns_test.go
+++ b/src/gnunet/service/dht/blocks/gns_test.go
@@ -16,7 +16,7 @@
 //
 // SPDX-License-Identifier: AGPL3.0-or-later
 
-package message
+package blocks
 
 import (
        "bytes"
@@ -29,6 +29,97 @@ import (
        "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) {
@@ -69,7 +160,7 @@ func TestRecordsetPKEY(t *testing.T) {
                                                Val: uint64(26147096139323793),
                                        },
                                        Size:  36,
-                                       RType: crypto.ZONE_PKEY,
+                                       RType: enums.GNS_TYPE_PKEY,
                                        Flags: 2,
                                        Data: []byte{
                                                0x00, 0x01, 0x00, 0x00,
@@ -134,7 +225,7 @@ func TestRecordsetPKEY(t *testing.T) {
        )
 
        // check zone key pair
-       prv, err := crypto.NewZonePrivate(crypto.ZONE_PKEY, D)
+       prv, err := crypto.NewZonePrivate(enums.GNS_TYPE_PKEY, D)
        if err != nil {
                t.Fatal(err)
        }
@@ -229,7 +320,7 @@ func TestRecordsetEDKEY(t *testing.T) {
                                                Val: uint64(49556645701000000),
                                        },
                                        Size:  36,
-                                       RType: uint32(enums.GNS_TYPE_NICK),
+                                       RType: enums.GNS_TYPE_NICK,
                                        Flags: 2,
                                        Data: []byte{
                                                0x4d, 0x79, 0x20, 0x4e, 0x69, 
0x63, 0x6b, 0x00,
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 <http://www.gnu.org/licenses/>.
-//
-// 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 <http://www.gnu.org/licenses/>.
+//
+// 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 <http://www.gnu.org/licenses/>.
+//
+// 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 <http://www.gnu.org/licenses/>.
+//
+// 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 <http://www.gnu.org/licenses/>.
+//
+// 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 <http://www.gnu.org/licenses/>.
+//
+// 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 <http://www.gnu.org/licenses/>.
+--
+-- 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 <http://www.gnu.org/licenses/>.
+//
+// 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 <http://www.gnu.org/licenses/>.
+//
+// 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, ",<br>")
+               },
+               "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"}}
+<!doctype html>
+<html lang="en">
+    <head>
+        <meta name="viewport" content="width=device-width, initial-scale=1, 
shrink-to-fit=no">
+        {{template "css"}}
+    </head>
+    <body>
+        <h1>GNUnet Zone Master</h1>
+        <hr/>
+        {{.}}
+        <script>
+            function notify(msg) {
+                if ('Notification' in window) {
+                    if (Notification.permission !== 'denied') {
+                        Notification.requestPermission(function (permission) {
+                            if (permission === 'granted') {
+                                var note = new Notification('GNUnet Zone 
Master', {
+                                    body: msg,
+                                    actions: []
+                                });
+                            }
+                        });
+                    }
+                }
+            }
+        </script>
+    </body>
+</html>
+{{end}}
+
+{{define "dashboard"}}
+<div>
+    <ul id="dashboard">
+    {{if .}}
+        {{range $zi, $zone := .}}
+        <li>
+            {{$z := $zone.Zone}}
+            <span class="caret"><b>{{$z.Name}}</b></span> [{{keytype 
$z.Key.Type}}: {{$zone.PubID}}]
+            <a href="/edit/zone/{{$z.ID}}" title="Edit zone"><button 
class="icon blue">&#9998;</button></a>
+            <a href="/del/zone/{{$z.ID}}" title="Remove zone"><button 
class="icon red">&#10006;</button></a>
+            (Created: {{date $z.Created}}, Modified: {{date $z.Modified}})
+            <ul class="nested">
+            {{if $zone.Labels}}
+                {{range $li, $label := $zone.Labels}}
+                <li>
+                    {{$l := $label.Label}}
+                    <span class="caret"><b>{{$l.Name}}</b></span>
+                    <a href="/edit/label/{{$l.ID}}" title="Edit label"><button 
class="icon blue">&#9998;</button></a>
+                    <a href="/del/label/{{$l.ID}}" title="Remove 
label"><button class="icon red">&#10006;</button></a>
+                    (Created: {{date $l.Created}}, Modified: {{date 
$l.Modified}})
+                    <ul class="nested">
+                    {{if $label.Records}}
+                        <li>
+                            <table class="rowed">
+                                <tr class="header">
+                                    <th>Type</th>
+                                    <th>Value</th>
+                                    <th>Flags</th>
+                                    <th>Expires</th>
+                                    <th>Created</th>
+                                    <th>Modified</th>
+                                    <th>Actions</th>
+                                </tr>
+                                {{range $ri, $rec := $label.Records}}
+                                <tr class="row">
+                                    <td>{{rrtype $rec.RType}}</td>
+                                    <td>{{rrdata $rec.RType $rec.Data}}</td>
+                                    <td>{{rrflags $rec.Flags}}</td>
+                                    <td>{{date $rec.Expire}}</td>
+                                    <td>{{date $rec.Created}}</td>
+                                    <td>{{date $rec.Modified}}</td>
+                                    <td>
+                                        <a href="/edit/rr/{{$rec.ID}}" 
title="Edit record"><button class="icon blue">&#9998;</button></a>
+                                        <a href="/del/rr/{{$rec.ID}}" 
title="Remove record"><button class="icon red">&#10006;</button></a>
+                                    </td>
+                                </tr>
+                                {{end}}
+                            </table>
+                        </li>
+                    {{else}}
+                        <li><h3>No resource records for label defined 
yet.</h3></li>
+                    {{end}}
+                        <li>
+                            <a href="/new/rr/{{$l.ID}}" title="Add new 
record..."><button class="icon blue">&#10010;</button></a>
+                        </li>
+                    </ul>
+                </li>
+                {{end}}
+            {{else}}
+                <li><h3>No labels for zone defined yet.</h3></li>
+            {{end}}
+                <li>
+                    <a href="/new/label/{{$z.ID}}" title="Add new 
label..."><button class="icon blue">&#10010;</button></a>
+                </li>
+            </ul>
+        </li>
+        {{end}}
+    {{else}}
+        <li>
+            <h3>No zones defined yet.</h3>
+        </li>
+    {{end}}
+        <li>
+            <a href="/new/zone/0" title="Add new zone..."><button class="icon 
blue">&#10010;</button></a>
+        </li>
+    </ul>
+</div>
+<script>
+    var toggler = document.getElementsByClassName("caret");
+    for (var i = 0; i < toggler.length; i++) {
+        toggler[i].addEventListener("click", function() {
+            
this.parentElement.querySelector(".nested").classList.toggle("active");
+            this.classList.toggle("caret-down");
+        });
+    }
+
+    for (var i = 0; i < toggler.length; i++) {
+        if (localStorage.getItem("t"+i) == "true") {
+            
toggler[i].parentElement.querySelector(".nested").classList.toggle("active");
+            toggler[i].classList.toggle("caret-down");
+        }
+    }
+    document.documentElement.scrollTop = document.body.scrollTop = 
localStorage.getItem("top");
+
+    window.addEventListener('beforeunload', function (e) {
+        for (var i = 0; i < toggler.length; i++) {
+            localStorage.setItem("t"+i, 
toggler[i].classList.contains("caret-down"));
+        }
+        localStorage.setItem("top", window.pageYOffset || 
document.documentElement.scrollTop);
+    });
+</script>
+{{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"}}
+<style>
+    * {
+        box-sizing: border-box;
+    }
+    body {
+        margin: 2em 9em 2em 9em;
+    }
+    input[type=text] {
+        font-size: 1.2em;
+        padding: 5px;
+        border: 2px solid #ddd;
+        border-radius: 7px;
+    }
+    input[type=text]:focus { 
+        outline: none;
+        border-color: #ace;
+        box-shadow: 0 0 10px #ace;
+    }
+    div.row::after {
+        content: "";
+        clear: both;
+        display: table;
+    }
+    div.cell {
+        display: inline;
+        float: left;
+    }
+    div.box {
+        border: 2px solid black;
+        margin: 0.5em;
+        padding: 0.5em;
+    }
+    div.block {
+        margin: 0.5em;
+        padding: 0.5em;
+    }
+    div.heading {
+        color: white;
+        background-color: orange;
+        font-size: 200%;
+        font-weight: bold;
+        padding: 0.3em;
+        margin: 1em 0 1em 0;
+    }
+    button.icon {
+        border: none;
+        color: black;
+        background-color: transparent;
+        padding: 0 0;
+        text-align: center;
+        text-decoration: none;
+        display: inline-block;
+        font-size: 100%;
+        margin: 4px 2px;
+        cursor: pointer;
+    }
+    .label {
+        text-align: right;
+        vertical-align: top;
+        font-weight: bold;
+    }            
+    .title {
+        font-size: 120%;
+        font-weight: bold;
+        margin-bottom: 0.5em;
+    }
+    .large {
+        font-size: 200%;
+        font-weight: bold;
+    }
+    .small {
+        font-size: 75%;
+    }
+    .blue {
+        color: blue !important;
+    }
+    .red {
+        color: red !important;
+    }
+    .disabled {
+        pointer-events:none;
+    }
+    .headline {
+        color: white;
+        padding: 0.3em;
+    }
+    .status-0 {
+        background-color: green;
+    }
+    .status-1 {
+        background-color: orange;
+    }
+    .status-2 {
+        background-color: red;
+    }
+    .spacer-right {
+        margin-right: 2em;
+    }
+    .changed {
+        background-color: #fee;
+    }
+    table.rowed {
+        border-collapse: separate;
+    }
+    table.rowed > tbody > tr {
+        border: solid;
+        border-width: 1px 0;
+        border-color: #ccc;
+    }
+    tr.row:nth-child(even) {
+        background: #fff;
+    }
+    tr.row:nth-child(odd) {
+        background: #eee;
+    }
+    tr.header {
+        background: #eef;
+        color: black;
+        font-weight: bold;
+    }
+    td {
+        padding: 0.5em;
+    }
+    th {
+        padding: 0.5em;
+        text-align: center;
+    }
+    label[for=toggle] {
+        cursor: pointer;
+        border: 1px solid black;
+        border-radius: 0.2em;
+        background-color: #eeeeee;
+        padding: 0.1em;
+    }
+    #toggle {
+        display: none;
+    }
+    #toggle:not(:checked) ~ #toggled {
+        display: none;
+    }
+    ul, #dashboard {
+        list-style-type: none;
+    }
+    #dashboard {
+        margin: 0;
+        padding: 0;
+    }
+    li {
+        margin: 0.5em;
+    }
+    .caret {
+        cursor: pointer;
+        user-select: none;
+    }
+    .caret::before {
+        content: "\25B6";
+        color: black;
+        display: inline-block;
+        margin-right: 6px;
+    }
+    .caret-down::before {
+        transform: rotate(90deg);
+    }
+    .nested {
+        display: none;
+    }
+    .active {
+        display: block;
+    }
+    .tabset > input[type="radio"] {
+        position: absolute;
+        left: -200vw;
+    }
+    .tabset .tab-panel {
+        display: none;
+    }
+    .tabset > input:first-child:checked ~ .tab-panels > .tab-panel:first-child,
+    .tabset > input:nth-child(3):checked ~ .tab-panels > 
.tab-panel:nth-child(2),
+    .tabset > input:nth-child(5):checked ~ .tab-panels > 
.tab-panel:nth-child(3),
+    .tabset > input:nth-child(7):checked ~ .tab-panels > 
.tab-panel:nth-child(4),
+    .tabset > input:nth-child(9):checked ~ .tab-panels > 
.tab-panel:nth-child(5),
+    .tabset > input:nth-child(11):checked ~ .tab-panels > 
.tab-panel:nth-child(6),
+    .tabset > input:nth-child(13):checked ~ .tab-panels > 
.tab-panel:nth-child(7),
+    .tabset > input:nth-child(15):checked ~ .tab-panels > 
.tab-panel:nth-child(8),
+    .tabset > input:nth-child(17):checked ~ .tab-panels > 
.tab-panel:nth-child(9),
+    .tabset > input:nth-child(19):checked ~ .tab-panels > 
.tab-panel:nth-child(10),
+    .tabset > input:nth-child(21):checked ~ .tab-panels > 
.tab-panel:nth-child(11),
+    .tabset > input:nth-child(23):checked ~ .tab-panels > 
.tab-panel:nth-child(12),
+    .tabset > input:nth-child(25):checked ~ .tab-panels > 
.tab-panel:nth-child(13),
+    .tabset > input:nth-child(27):checked ~ .tab-panels > 
.tab-panel:nth-child(14),
+    .tabset > input:nth-child(29):checked ~ .tab-panels > 
.tab-panel:nth-child(15),
+    .tabset > input:nth-child(31):checked ~ .tab-panels > 
.tab-panel:nth-child(16),
+    .tabset > input:nth-child(33):checked ~ .tab-panels > 
.tab-panel:nth-child(17),
+    .tabset > input:nth-child(35):checked ~ .tab-panels > 
.tab-panel:nth-child(18),
+    .tabset > input:nth-child(37):checked ~ .tab-panels > 
.tab-panel:nth-child(19),
+    .tabset > input:nth-child(39):checked ~ .tab-panels > 
.tab-panel:nth-child(20) {
+        display: block;
+    }
+    .tabset > label {
+        position: relative;
+        display: inline-block;
+        padding: 15px 15px 25px;
+        border: 1px solid transparent;
+        border-bottom: 0;
+        cursor: pointer;
+        font-weight: 600;
+    }
+    .tabset > label::after {
+        content: "";
+        position: absolute;
+        left: 15px;
+        bottom: 10px;
+        width: 22px;
+        height: 4px;
+        background: #8d8d8d;
+    }
+    .tabset > label:hover {
+        color: #f90;
+    }
+    .tabset > input:focus + label {
+        color: #06c;
+    }
+    .tabset > label:hover::after {
+        background: #f90;
+    }
+    .tabset > input:focus + label::after,
+    .tabset > input:checked + label::after {
+        background: #06c;
+    }
+    .tabset > input:checked + label {
+        border-color: #ccc;
+        border-bottom: 1px solid #fff;
+        margin-bottom: -1px;
+    }
+    .tab-panel {
+        padding: 30px 0;
+        border-top: 1px solid #ccc;
+    }
+    div.switch {
+        display: none;
+    }
+    input.switch:checked ~ div.switch {
+        display: block;
+    }
+    div.alternate {
+        display: block;
+    }
+    input.alternate:checked ~ div.alternate {
+        display: none;
+    }
+</style>
+{{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"}}
+    <h1>Debug</h1>
+    <h3>Parameters:</h3>
+    <ul>
+    {{range $k,$v := .Params}}
+        <li><b>{{$k}}</b> = {{$v}}</li>
+    {{end}}
+    </ul>
+    <h3>RR data:</h3>
+    <p>{{.RR}}</p>
+    {{if .Err}}
+    <p>Error: <b>{{.Err}}</b></p>
+    {{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"}}
+    <div>
+        <h3>Edit a [{{$type}}] GNS zone:</h3>
+        <p><small>(Created: {{index .Params "created"}}, Last edited: {{index 
.Params "modified"}})</small></p>
+        <form action="/action/upd/zone/{{.Ref}}" method="post" 
onsubmit="return(zone_validate());">
+            <input type="hidden" name="old_name" value="{{$name}}">
+            <table>
+                <tr>
+                    <td align="right"><b>Zone name:</b></td>
+                    <td><input type="text" id="name" name="name" 
value="{{$name}}"></td>
+                </tr>
+                <tr>
+                    <td colspan="2">
+                        <p>The type of the zone key cannot be changed. It is 
currently set to
+                        <b>{{if eq $type "PKEY"}}PKEY 
(Ed25519+EcDSA){{else}}EDKEY (EdDSA){{end}}</b>:</p>
+                        <table>
+                            <tr>
+                                <td align="right"><b>Public key:</b></td>
+                                <td>{{index .Params "keydata"}}</td>
+                            </tr>
+                            <tr>
+                                <td align="right"><b>Private key:</b></td>
+                                <td>{{index .Params "prvdata"}}</td>
+                            </tr>
+                        </table>
+                    </td>
+                </tr>
+            </table>
+            <button id="submit">Change zone name</button>
+        </form>
+        <p><a href="/"><button>Leave</button></a></p>
+    </div>
+    <script>
+        const old_zone = "{{$name}}";
+        const zone_names = [
+            {{range $i, $n := .Names}}
+                "{{$n}}",
+            {{end}}
+        ];
+        function zone_validate() {
+            const name = document.getElementById("name").value;
+            if (!name) {
+                alert("Empty zone name not allowed");
+                return false;
+            }
+            if (name == old_zone) {
+                alert("Zone name not changed");
+                return false;
+            }
+            for (var i = 0; i < names.length; i++) {
+                if (zone_names[i] == name) {
+                    alert("Zone name already in-use");
+                    return false;
+                }
+            }
+            return(true);
+        }
+    </script>
+{{end}}
+
+{{define "edit_label"}}
+    {{$name := index .Params "name"}}
+    {{$zone := index .Params "zone"}}
+    <div>
+        <h3>Edit a GNS label for zone "{{$zone}}":</h3>
+        <p><small>(Created: {{index .Params "created"}}, Last edited: {{index 
.Params "modified"}})</small></p>
+        <form action="/action/upd/label/{{.Ref}}" method="post" 
onsubmit="return(label_validate());">
+            <input type="hidden" name="old_name" value="{{$name}}">
+            <input type="hidden" name="zid" value="{{index .Params "zid"}}">
+            <table>
+                <tr>
+                    <td align="right">Name:</td>
+                    <td><input type="text" id="name" name="name" 
value="{{$name}}"></td>
+                </tr>
+            </table>
+            <button id="submit">Change label name</button>
+        </form>
+        <p><a href="/"><button>Leave</button></a></p>
+    </div>
+    <script>
+        const old_label = "{{$name}}";
+        const label_names = [
+            {{range $i, $n := .Names}}
+            '{{$n}}',
+            {{end}}
+        ];
+        function label_validate() {
+            const name = document.getElementById("name").value;
+            if (!name) {
+                alert("Empty labels not allowed");
+                return false;
+            }
+            if (name == old_label) {
+                alert("Label name not changed");
+                return false;
+            }
+            for (var i = 0; i < names.length; i++) {
+                if (label_names[i] == name) {
+                    alert("Label name already in-use");
+                    return false;
+                }
+            }
+            return(true);
+        }
+    </script>
+{{end}}
+
+{{define "edit_rec"}}
+    {{$label := index .Params "label"}}
+    <div>
+        <h3>Edit a resource record for label "{{$label}}":</h3>
+        <p><small>(Created: {{index .Params "created"}}, Last edited: {{index 
.Params "modified"}})</small></p>
+        {{$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}}
+    </div>
+    <a href="/"><button>Leave</button></a>
+{{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"}}
+<div>
+    <h3>Creating a new GNS zone:</h3>
+    <form action="/action/new/zone/0" method="post" 
onsubmit="return(zone_validate());">
+        <table>
+            <tr>
+                <td align="right"><b>Zone name:</b></td>
+                <td><input type="text" id="name" name="name"></td>
+            </tr>
+            <tr>
+                <td align="right" valign="top"><b>Key type:</b></td>
+                <td>
+                    <input type="radio" id="pkey" name="keytype" value="PKEY" 
checked="checked">&nbsp;PKEY (Ed25519+EcDSA)<br>
+                    <input type="radio" id="edkey" name="keytype" 
value="EDKEY">&nbsp;EDKEY (EdDSA)
+                </td>
+            </tr>
+        </table>
+        <button id="submit">Add zone</button>
+    </form>
+    <a href="/"><button id="leave">Leave</button></a>
+</div>
+<script>
+    const zone_names = [
+        {{range $i, $n := .Names}}
+        '{{$n}}',
+        {{end}}
+    ];
+    function zone_validate() {
+        const name = document.getElementById("name").value;
+        if (!name) {
+            alert("Empty zone name not allowed");
+            return false;
+        }
+        for (var i = 0; i < names.length; i++) {
+            if (zone_names[i] == name) {
+                alert("Zone name already used");
+                return false;
+            }
+        }
+        return(true);
+    }
+</script>
+{{end}}
+
+{{define "new_label"}}
+<div>
+    <h3>Creating a new GNS label for zone "{{index .Params "zone"}}":</h3>
+    <form action="/action/new/label/{{.Ref}}" 
onsubmit="return(label_validate());">
+        <table>
+            <tr>
+                <td align="right">Name:</td>
+                <td><input type="text" id="name" name="name"></td>
+            </tr>
+        </table>
+        <button id="submit">Add label</button>
+    </form>
+    <a href="/"><button>Leave</button></a>
+</div>
+<script>
+    const label_names = [
+        {{range $i, $n := .Names}}
+        '{{$n}}',
+        {{end}}
+    ];
+    function label_validate() {
+        const name = document.getElementById("name").value;
+        if (!name) {
+            alert("Empty labels not allowed");
+            return false;
+        }
+        for (var i = 0; i < names.length; i++) {
+            if (label_names[i] == name) {
+                alert("Label already used");
+                return false;
+            }
+        }
+        return(true);
+    }
+</script>
+{{end}}
+
+{{define "new_record"}}
+{{$data := .}}
+<div>
+    <h3>Creating a new GNS resource record for label "{{index .Params 
"label"}}":</h3>
+    <div class="tabset">
+        {{range $i, $type := .RRspecs}}
+        <input type="radio" name="tabset" id="tab{{$i}}" 
aria-controls="tab{{$i}}" {{if eq $i 0}}checked{{end}}>
+        <label for="tab{{$i}}">{{rrtype $type.Type}}</label>
+        {{end}}
+        <div class="tab-panels">
+            {{range $i, $spec := .RRspecs}}
+            <section id="tab{{$i}}" class="tab-panel">
+                {{$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}}
+            </section>
+            {{end}}
+        </div>
+    </div>
+    <a href="/"><button>Leave</button></a>
+</div>
+{{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"}}
+    <input type="hidden" name="lid" value="{{index . "lid"}}">
+    {{range $k, $v := .}}
+        <input type="hidden" name="old_{{$k}}" value="{{$v}}">
+    {{end}}
+    {{$pf := index . "prefix"}}
+    <tr>
+        <td align="right" valign="top"><b>Expires:</b></td>
+        <td>
+            Never <input type="checkbox" class="alternate" name="{{$pf}}never"
+                {{if eq "on" (index . (print $pf 
"never"))}}checked="checked"{{end}}
+            >
+            <div class="alternate">
+                At given date and time:
+                <input type="datetime-local" id="{{$pf}}expires" 
name="{{$pf}}expires" required
+                    value="{{index . (print $pf "expires")}}"
+                >
+            </div>
+        </td>
+    </tr>
+    <tr>
+        <td align="right" valign="top"><b>Flags:</b></td>
+        <td>
+            <input type="checkbox" name="{{$pf}}private"
+                {{if eq "on" (index . (print $pf 
"private"))}}checked="checked" class="disabled"{{end}}
+                > Private<br>
+            <input type="checkbox" name="{{$pf}}shadow"
+                {{if eq "on" (index . (print $pf "shadow"))}}checked="checked" 
class="disabled"{{end}}
+                > Shadow<br>
+            <input type="checkbox" name="{{$pf}}suppl"
+                {{if eq "on" (index . (print $pf "suppl"))}}checked="checked" 
class="disabled"{{end}}
+                > Supplemental<br>
+        </td>
+    </tr>
+    {{if eq .Action "new"}}
+    <script>
+        var dt = document.getElementById("{{$pf}}expires");
+        if (!dt.value) {
+            var exp = new Date(new Date().getTime() + 31536000000);
+            dt.value = exp.toISOString().slice(0, 16);
+        }
+    </script>
+    {{end}}
+{{end}}
+
+{{define "PKEY"}}
+    <h3>PKEY delegation</h3>
+    <form action="/action/{{.Action}}/rr/{{.Ref}}" {{if eq .Action 
"upd"}}method="post"{{end}}>
+        <input type="hidden" name="type" value="65536">
+        <table>
+            <tr><td/>
+                <td>
+                    Enter the public zone key (type
+                    <a href="https://lsd.gnunet.org/lsd0001/#name-pkey"; 
target="_blank">PKEY</a>
+                    ) in
+                    <a href="https://lsd.gnunet.org/lsd0001/#name-base32gns"; 
target="_blank">Base32GNS</a>
+                    encoding:
+                </td>
+            </tr>
+            <tr>
+                <td align="right"><b>Key:</b></td>
+                <td>
+                    <input type="text" name="pkey_data"
+                        maxlength="58" minlength="58" size="64"
+                        pattern="[0-9A-HJKMNP-TV-Z]{58}"
+                        autofocus required
+                        value="{{index .Params "pkey_data"}}"
+                    >
+                </td>
+            </tr>
+            {{template "RRCommon" .Params}}
+            <tr><td/><td><button id="submit">{{.Button}} 
record</button></td></tr>
+        </table>
+    </form>
+{{end}}
+{{define "EDKEY"}}
+    <h3>EDKEY delegation</h3>
+    <form action="/action/{{.Action}}/rr/{{.Ref}}" {{if eq .Action 
"upd"}}method="post"{{end}}>
+        <input type="hidden" name="type" value="65556">
+        <table>
+            <tr><td/>
+                <td>
+                    Enter the public zone key (type
+                    <a href="https://lsd.gnunet.org/lsd0001/#name-edkey"; 
target="_blank">EDKEY</a>
+                    ) in
+                    <a href="https://lsd.gnunet.org/lsd0001/#name-base32gns"; 
target="_blank">Base32GNS</a>
+                    encoding:
+                </td>
+            </tr>
+            <tr>
+                <td align="right"><b>Key:</b></td>
+                <td>
+                    <input type="text" name="edkey_data"
+                        maxlength="58" minlength="58" size="64"
+                        pattern="[0-9A-HJKMNP-TV-Z]{58}"
+                        autofocus required
+                        value="{{index .Params "edkey_data"}}"
+                    >
+                </td>
+            </tr>
+            {{template "RRCommon" .Params}}
+            <tr><td/><td><button id="submit">{{.Button}} 
record</button></td></tr>
+        </table>
+    </form>
+{{end}}
+{{define "REDIRECT"}}
+    <h3>REDIRECT (GNS delegation)</h3>
+    <form action="/action/{{.Action}}/rr/{{.Ref}}" {{if eq .Action 
"upd"}}method="post"{{end}}>
+        <input type="hidden" name="type" value="65551">
+        <table>
+            <tr><td/>
+                <td>
+                    Enter the redirected GNS name (see
+                    <a href="https://lsd.gnunet.org/lsd0001/#name-redirect"; 
target="_blank">specification</a>
+                    ):
+                </td>
+            </tr>
+            <tr>
+                <td align="right"><b>Name:</b></td>
+                <td>
+                    <input type="text" name="redirect_name"
+                        maxlength="63" size="63"
+                        autofocus required
+                        value="{{index .Params "redirect_name"}}"
+                    >
+                </td>
+            </tr>
+            {{template "RRCommon" .Params}}
+            <tr><td/><td><button id="submit">{{.Button}} 
record</button></td></tr>
+        </table>
+    </form>
+{{end}}
+{{define "LEHO"}}
+    <h3>LEHO (legacy hostname)</h3>
+    <form action="/action/{{.Action}}/rr/{{.Ref}}" {{if eq .Action 
"upd"}}method="post"{{end}}>
+        <input type="hidden" name="type" value="65538">
+        <table>
+            <tr>
+                <td align="right"><b>Legacy hostname:</b></td>
+                <td>
+                    <input type="text" name="leho_name"
+                        maxlength="63" size="63"
+                        autofocus required
+                        value="{{index .Params "leho_name"}}"
+                    >
+                </td>
+            </tr>
+            {{template "RRCommon" .Params}}
+            <tr><td/><td><button id="submit">{{.Button}} 
record</button></td></tr>
+        </table>
+    </form>
+{{end}}
+{{define "NICK"}}
+    <h3>NICK</h3>
+    <form action="/action/{{.Action}}/rr/{{.Ref}}" {{if eq .Action 
"upd"}}method="post"{{end}}>
+        <input type="hidden" name="type" value="65537">
+        <table>
+            <tr>
+                <td align="right"><b>Nick name:</b></td>
+                <td>
+                    <input type="text" name="nick_name"
+                        maxlength="63" size="63"
+                        autofocus required
+                        value="{{index .Params "nick_name"}}"
+                    >
+                </td>
+            </tr>
+            {{template "RRCommon" .Params}}
+            <tr><td/><td><button id="submit">{{.Button}} 
record</button></td></tr>
+        </table>
+    </form>
+{{end}}
+{{define "GNS2DNS"}}
+    <h3>GNS2DNS delegation</h3>
+    <form action="/action/{{.Action}}/rr/{{.Ref}}" {{if eq .Action 
"upd"}}method="post"{{end}}>
+        <input type="hidden" name="type" value="65540">
+        <table>
+            <tr><td/>
+                <td>
+                    Enter DNS name and server as
+                    <a href="https://lsd.gnunet.org/lsd0001/#name-gns2dns"; 
target="_blank">specified</a>.
+                </td>
+            </tr>
+            <tr>
+                <td align="right"><b>DNS name:</b></td>
+                <td>
+                    <input type="text" name="gns2dns_name"
+                        maxlength="63" size="63"
+                        autofocus required
+                        value="{{index .Params "gns2dns_name"}}"
+                    >
+                </td>
+            </tr>
+            <tr>
+                <td align="right"><b>DNS server:</b></td>
+                <td>
+                    <input type="text" name="gns2dns_server"
+                        maxlength="63" size="63" required
+                        value="{{index .Params "gns2dns_server"}}"
+                    >
+                </td>
+            </tr>
+            {{template "RRCommon" .Params}}
+            <tr><td/><td><button id="submit">{{.Button}} 
record</button></td></tr>
+        </table>
+    </form>
+{{end}}
+{{define "BOX"}}
+    <h3>BOX</h3>
+    <form action="/action/{{.Action}}/rr/{{.Ref}}" {{if eq .Action 
"upd"}}method="post"{{end}}>
+        <input type="hidden" name="type" value="65541">
+        <table>
+            <tr><td/>
+                <td>
+                    Enter protocol, service (port) and type of the boxed 
resource type as
+                    <a href="https://lsd.gnunet.org/lsd0001/#name-box"; 
target="_blank">specified</a>:
+                </td>
+            </tr>
+            <tr>
+                <td align="right"><b>Protocol:</b></td>
+                <td>
+                    <input type="text" name="box_proto" size="16" 
list="protocols" required
+                        value="{{index .Params "box_proto"}}"
+                    >
+                    <datalist id="protocols">
+                    {{range $id,$name := boxprotos}}
+                        <option value="{{$id}} ({{$name}})">
+                    {{end}}
+                    </datalist>
+                </td>
+            </tr>
+            <tr>
+                <td align="right"><b>Service:</b></td>
+                <td>
+                    <input type="text" name="box_svc" size="16" 
list="services" required
+                        value="{{index .Params "box_svc"}}"
+                    >
+                    <datalist id="services">
+                    {{range $id,$name := boxsvcs}}
+                        <option value='{{$id}} {{$name}}'>
+                    {{end}}
+                    </datalist>
+                </td>
+            </tr>
+            <tr>
+                <td align="right" valign="top"><b>Type:</b></td>
+                <td>
+                    <input type="radio" class="switch" name="box_type" 
value="33"
+                        {{if eq (index .Params "box_type") "33"}}checked{{end}}
+                    > SRV (Service description)
+                    <div class="switch">
+                        <div class="block">
+                            <label for="box_srv_host">Host:</label>
+                            <input type="text" name="box_srv_host" 
maxlength="63" size="63"
+                                value="{{index .Params "box_srv_host"}}"
+                            >
+                        </div>
+                    </div>
+                </td>
+            </tr>
+            </tr>
+            <tr>
+                <td/><td>
+                    <input type="radio" class="switch" name="box_type" 
value="52"
+                        {{if eq (index .Params "box_type") "52"}}checked{{end}}
+                    > TLSA (TLS Association)
+                    <div class="switch">
+                        <div class="block">
+                            <label for="box_tlsa_usage">Usage:</label>
+                            {{$x := index .Params "box_tlsa_usage"}}
+                            <select size="1" name="box_tlsa_usage">
+                                <option value="0" {{if eq $x 
"0"}}selected{{end}}>CA certificate</option>
+                                <option value="1" {{if eq $x 
"1"}}selected{{end}}>Service certificate constraint</option>
+                                <option value="2" {{if eq $x 
"2"}}selected{{end}}>Trust anchor assertion</option>
+                                <option value="3" {{if eq $x 
"3"}}selected{{end}}>Domain-issued certificate</option>
+                                <option value="255" {{if eq $x 
"255"}}selected{{end}}>Private use</option>
+                            </select>
+                        </div>
+                        <div class="block">
+                            <label for="box_tlsa_selector">Selector:</label>
+                            {{$x = index .Params "box_tlsa_selector"}}
+                            <select size="1" name="box_tlsa_selector">
+                                <option value="0" {{if eq $x 
"0"}}selected{{end}}>Full certificate</option>
+                                <option value="1" {{if eq $x 
"1"}}selected{{end}}>SubjectPublicKeyInfo</option>
+                                <option value="255" {{if eq $x 
"255"}}selected{{end}}>Private use</option>
+                            </select>
+                        </div>
+                        <div class="block">
+                            <label for="box_tlsa_match">Match:</label>
+                            {{$x = index .Params "box_tlsa_match"}}
+                            <select size="1" name="box_tlsa_match">
+                                <option value="0" {{if eq $x 
"0"}}selected{{end}}>No hash</option>
+                                <option value="1" {{if eq $x 
"1"}}selected{{end}}>SHA-256</option>
+                                <option value="2" {{if eq $x 
"2"}}selected{{end}}>SHA-512</option>
+                                <option value="255" {{if eq $x 
"255"}}selected{{end}}>Private use</option>
+                            </select>
+                        </div>
+                        <div class="block">
+                            <label for="box_tlsa_cert">Certificate information 
(hex):</label><br>
+                            <textarea name="box_tlsa_cert" rows="10" 
cols="50">{{index .Params "box_tlsa_cert"}}</textarea>
+                        </div>
+                    </div>
+                </td>
+            </tr>
+            {{template "RRCommon" .Params}}
+            <tr><td/><td><button id="submit">{{.Button}} 
record</button></td></tr>
+        </table>
+    </form>
+{{end}}
+{{define "DNS_A"}}
+    <h3>DNS A (IPv4 address)</h3>
+    <form action="/action/{{.Action}}/rr/{{.Ref}}" {{if eq .Action 
"upd"}}method="post"{{end}}>
+        <input type="hidden" name="type" value="1">
+        <table>
+            <tr>
+                <td align="right"><b>Address:</b></td>
+                <td>
+                    <input type="text" name="dnsa_addr"
+                        maxlength="15" size="15"
+                        
pattern="[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}"
+                        autofocus required
+                        value="{{index .Params "dnsa_addr"}}"
+                    >
+                </td>
+            </tr>
+            {{template "RRCommon" .Params}}
+            <tr><td/><td><button id="submit">{{.Button}} 
record</button></td></tr>
+        </table>
+    </form>
+{{end}}
+{{define "DNS_AAAA"}}
+    <h3>DNS AAAA (IPv6 address)</h3>
+    <form action="/action/{{.Action}}/rr/{{.Ref}}" {{if eq .Action 
"upd"}}method="post"{{end}}>
+        <input type="hidden" name="type" value="28">
+        <table>
+            <tr>
+                <td align="right"><b>Address:</b></td>
+                <td>
+                    <input type="text" name="dnsaaaa_addr"
+                        maxlength="15" size="15"
+                        
pattern="(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[
 [...]
+                        autofocus required
+                        value="{{index .Params "dnsaaaa_addr"}}"
+                    >
+                </td>
+            </tr>
+            {{template "RRCommon" .Params}}
+            <tr><td/><td><button id="submit">{{.Button}} 
record</button></td></tr>
+        </table>
+    </form>
+{{end}}
+{{define "DNS_CNAME"}}
+    <h3>DNS CNAME delegation</h3>
+    <form action="/action/{{.Action}}/rr/{{.Ref}}" {{if eq .Action 
"upd"}}method="post"{{end}}>
+        <input type="hidden" name="type" value="5">
+        <table>
+            <tr>
+                <td align="right"><b>Name:</b></td>
+                <td>
+                    <input type="text" name="dnscname_name"
+                        maxlength="63" size="63"
+                        autofocus required
+                        value="{{index .Params "dnscname_name"}}"
+                    >
+                </td>
+            </tr>
+            {{template "RRCommon" .Params}}
+            <tr><td/><td><button id="submit">{{.Button}} 
record</button></td></tr>
+        </table>
+    </form>
+{{end}}
+{{define "DNS_TXT"}}
+    <h3>DNS TXT</h3>
+    <form action="/action/{{.Action}}/rr/{{.Ref}}" {{if eq .Action 
"upd"}}method="post"{{end}}>
+        <input type="hidden" name="type" value="16">
+        <table>
+            <tr>
+                <td align="right"><b>Text:</b></td>
+                <td>
+                    <input type="text" name="dnstxt_text"
+                        maxlength="63" size="63"
+                        autofocus required
+                        value="{{index .Params "dnstxt_text"}}"
+                    >
+                </td>
+            </tr>
+            {{template "RRCommon" .Params}}
+            <tr><td/><td><button id="submit">{{.Button}} 
record</button></td></tr>
+        </table>
+    </form>
+{{end}}
+{{define "DNS_MX"}}
+    <h3>DNS MX (Mailbox)</h3>
+    <form action="/action/{{.Action}}/rr/{{.Ref}}" {{if eq .Action 
"upd"}}method="post"{{end}}>
+        <input type="hidden" name="type" value="15">
+        <table>
+            <tr>
+                <td align="right" valign="top"><b>Priority:</b></td>
+                <td>
+                    {{$v := index .Params "dnsmx_prio"}}
+                    <input type="number" name="dnsmx_prio" min="1" max="100"
+                        value="{{if $v}}{{$v}}{{else}}10{{end}}"
+                    >
+                </td>
+            </tr>
+            <tr>
+                <td align="right" valign="top"><b>Mailserver:</b></td>
+                <td>
+                    <input type="text" name="dnsmx_host"
+                        maxlength="63" size="63"
+                        autofocus required
+                        value="{{index .Params "dnsmx_host"}}"
+                    >
+                </td>
+            </tr>
+            {{template "RRCommon" .Params}}
+            <tr><td/><td><button id="submit">{{.Button}} 
record</button></td></tr>
+        </table>
+    </form>
+{{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 <http://www.gnu.org/licenses/>.
+//
+// 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 <http://www.gnu.org/licenses/>.
+//
+// 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 "<unknown>"
+       }
+       // type-dependent rendering
+       switch rec := inst.(type) {
+       case *rr.PKEY:
+               return fmt.Sprintf("<span title='public zone key'>%s</span>", 
rec.ZoneKey.ID())
+       case *rr.EDKEY:
+               return fmt.Sprintf("<span title='public zone key'>%s</span>", 
rec.ZoneKey.ID())
+       case *rr.REDIRECT:
+               return fmt.Sprintf("<span title='redirect target'>%s</span>", 
rec.Name)
+       case *rr.NICK:
+               return fmt.Sprintf("<span title='nick name'>%s</span>", 
rec.Name)
+       case *rr.LEHO:
+               return fmt.Sprintf("<span title='legacy hostname'>%s</span>", 
rec.Name)
+       case *rr.CNAME:
+               return fmt.Sprintf("<span title='canonical name'>%s</span>", 
rec.Name)
+       case *rr.TXT:
+               return fmt.Sprintf("<span title='text'>%s</span>", rec.Text)
+       case *rr.DNSA:
+               return fmt.Sprintf("<span title='IPv4 address'>%s</span>", 
rec.Addr.String())
+       case *rr.DNSAAAA:
+               return fmt.Sprintf("<span title='IPv6 address'>%s</span>", 
rec.Addr.String())
+       case *rr.MX:
+               s := fmt.Sprintf("<span title='priority'>[%d]</span>&nbsp;", 
rec.Prio)
+               return s + fmt.Sprintf("<span title='server'>%s</span>", 
rec.Server)
+       case *rr.BOX:
+               s := fmt.Sprintf("<span title='service'>%s</span>/", 
rr.GetServiceName(rec.Svc, rec.Proto))
+               s += fmt.Sprintf("<span title='protocol'>%s</span> ", 
rr.GetProtocolName(rec.Proto))
+               switch rec.Type {
+               case enums.GNS_TYPE_DNS_TLSA:
+                       tlsa := new(rr.TLSA)
+                       _ = data.Unmarshal(tlsa, rec.RR)
+                       s += "TLSA[<br>"
+                       s += fmt.Sprintf("&#8729;&nbsp;Usage: %s<br>", 
rr.TLSAUsage[tlsa.Usage])
+                       s += fmt.Sprintf("&#8729;&nbsp;Selector: %s<br>", 
rr.TLSASelector[tlsa.Selector])
+                       s += fmt.Sprintf("&#8729;&nbsp;Match: %s<br>", 
rr.TLSAMatch[tlsa.Match])
+                       s += "&#8729;&nbsp;CertData:<br>"
+                       cert := hex.EncodeToString(tlsa.Cert)
+                       for len(cert) > 32 {
+                               s += "&nbsp;&nbsp;" + cert[:32] + "<br>"
+                               cert = cert[32:]
+                       }
+                       s += "&nbsp;&nbsp;" + cert + "<br>]"
+                       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("<span title='name'>%s</span> (Resolver: ", 
rec.Name)
+               return s + fmt.Sprintf("<span title='server'>%s</span>)", 
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 <http://www.gnu.org/licenses/>.
+//
+// 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 <http://www.gnu.org/licenses/>.
+//
+// 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 <http://www.gnu.org/licenses/>.
+//
+// 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

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]