about summary refs log tree commit diff
diff options
context:
space:
mode:
authorLaurent Bercot <ska-skaware@skarnet.org>2024-07-02 18:54:34 +0000
committerLaurent Bercot <ska@appnovation.com>2024-07-02 18:54:34 +0000
commit8b435b76d68dd8f11808f0cff4d8998d2be48f4c (patch)
tree501e5a0047f48fc082b3ed505022a59919c9e716
parent9879f5bf1c7bac2f0ae2e230694c9a86b1b567b0 (diff)
downloadshibari-8b435b76d68dd8f11808f0cff4d8998d2be48f4c.tar.gz
shibari-8b435b76d68dd8f11808f0cff4d8998d2be48f4c.tar.xz
shibari-8b435b76d68dd8f11808f0cff4d8998d2be48f4c.zip
Prepare for 0.0.2.0; add shibari-cache-config
Signed-off-by: Laurent Bercot <ska@appnovation.com>
-rw-r--r--.gitignore1
-rwxr-xr-xconfigure27
-rw-r--r--package/deps.mak10
-rw-r--r--package/info2
-rw-r--r--package/modes1
-rw-r--r--package/targets.mak3
-rw-r--r--src/cache/shibari-cache.c282
-rw-r--r--src/config/PARSING-config.txt26
-rw-r--r--src/config/conftree.c51
-rw-r--r--src/config/defaults.c70
-rw-r--r--src/config/deps-exe/shibari-cache-config8
-rw-r--r--src/config/lexparse.c230
-rw-r--r--src/config/node.c34
-rw-r--r--src/config/repo.c46
-rw-r--r--src/config/shibari-cache-config-internal.h91
-rw-r--r--src/config/shibari-cache-config.c101
-rw-r--r--src/config/util.c15
17 files changed, 993 insertions, 5 deletions
diff --git a/.gitignore b/.gitignore
index 0552150..ee5b903 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,3 +10,4 @@
 /libshibari-server.so.xyzzy
 /shibari-server-tcp
 /shibari-server-udp
+/shibari-cache-config
diff --git a/configure b/configure
index efee0c9..bbc68e7 100755
--- a/configure
+++ b/configure
@@ -23,6 +23,7 @@ Fine tuning of the installation directories:
   --libexecdir=DIR              package-scoped executables [EPREFIX/libexec]
   --libdir=DIR                  static library files [PREFIX/lib/$package]
   --includedir=DIR              C header files [PREFIX/include]
+  --sysconfdir=DIR              global configuration files [PREFIX/etc]
 
  If no --prefix option is given, by default libdir (but not dynlibdir) will be
  /usr/lib/$package, and includedir will be /usr/include.
@@ -133,13 +134,14 @@ LDFLAGS_POST="$LDFLAGS"
 LDFLAGS=
 LDFLAGS_NOSHARED=
 LDFLAGS_SHARED=-shared
-prefix=/usr
+prefix=
 exec_prefix='$prefix'
 dynlibdir='$prefix/lib'
 libexecdir='$exec_prefix/libexec'
 bindir='$exec_prefix/bin'
 libdir='$prefix/lib/$package'
 includedir='$prefix/include'
+sysconfdir='$prefix/etc'
 sysdeps='$prefix/lib/skalibs/sysdeps'
 manualsysdeps=false
 shared=false
@@ -169,6 +171,7 @@ for arg ; do
     --bindir=*) bindir=${arg#*=} ;;
     --libdir=*) libdir=${arg#*=} ;;
     --includedir=*) includedir=${arg#*=} ;;
+    --sysconfdir=*) sysconfdir=${arg#*=} ;;
     --with-sysdeps=*) sysdeps=${arg#*=} manualsysdeps=true ;;
     --with-include=*) var=${arg#*=} ; stripdir var ; addincpath="$addincpath -I$var" ;;
     --with-lib=*) var=${arg#*=} ; stripdir var ; addlibspath="$addlibspath -L$var" ; vpaths="$vpaths $var" ;;
@@ -197,9 +200,25 @@ for arg ; do
   esac
 done
 
+# Add /usr in the default default case
+if test -z "$prefix" ; then
+  if test "$libdir" = '$prefix/lib/$package' ; then
+    libdir=/usr/lib/$package
+  fi
+  if test "$dynlibdir" = '$prefix/lib' ; then
+    dynlibdir=/usr/lib
+  fi
+  if test "$includedir" = '$prefix/include' ; then
+    includedir=/usr/include
+  fi
+  if test "$sysdeps" = '$prefix/lib/skalibs/sysdeps' ; then
+    sysdeps=/usr/lib/skalibs/sysdeps
+  fi
+fi
+
 # Expand installation directories
 stripdir prefix
-for i in exec_prefix dynlibdir libexecdir bindir libdir includedir sysdeps sproot ; do
+for i in exec_prefix dynlibdir libexecdir bindir libdir includedir sysconfdir sysdeps sproot ; do
   eval tmp=\${$i}
   eval $i=$tmp
   stripdir $i
@@ -230,7 +249,6 @@ if $slashpackage ; then
   else
     sysdeps=${DESTDIR}${sproot}/package/prog/skalibs/sysdeps
   fi
-  prefix=
   extbinprefix=${exthome}/command
   dynlibdir=${home}/library.so
   bindir=${home}/command
@@ -378,6 +396,7 @@ libexecdir := $libexecdir
 bindir := $bindir
 libdir := $libdir
 includedir := $includedir
+sysconfdir := $sysconfdir
 sysdeps := $sysdeps
 slashpackage := $slashpackage
 sproot := $sproot
@@ -453,6 +472,8 @@ else
   echo "#define ${package_macro_name}_EXTBINPREFIX \"\""
 fi
 echo "#define ${package_macro_name}_LIBEXECPREFIX \"$libexecdir/\""
+echo "#define ${package_macro_name}_SYSCONFPREFIX \"$sysconfdir/\""
+
 echo
 echo "#endif"
 exec 1>&3 3>&-
diff --git a/package/deps.mak b/package/deps.mak
index ffc2c1a..78ab070 100644
--- a/package/deps.mak
+++ b/package/deps.mak
@@ -16,6 +16,7 @@ src/cache/dcache_init.o src/cache/dcache_init.lo: src/cache/dcache_init.c src/in
 src/cache/dcache_load.o src/cache/dcache_load.lo: src/cache/dcache_load.c src/include/shibari/dcache.h
 src/cache/dcache_save.o src/cache/dcache_save.lo: src/cache/dcache_save.c src/include/shibari/dcache.h
 src/cache/dcache_search.o src/cache/dcache_search.lo: src/cache/dcache_search.c src/cache/dcache-internal.h src/include/shibari/dcache.h
+src/cache/shibari-cache.o src/cache/shibari-cache.lo: src/cache/shibari-cache.c src/include/shibari/cache.h src/include/shibari/common.h
 src/common/shibari_log_answer.o src/common/shibari_log_answer.lo: src/common/shibari_log_answer.c src/include/shibari/log.h src/include/shibari/util.h
 src/common/shibari_log_exit.o src/common/shibari_log_exit.lo: src/common/shibari_log_exit.c src/include/shibari/log.h
 src/common/shibari_log_query.o src/common/shibari_log_query.lo: src/common/shibari_log_query.c src/include/shibari/log.h src/include/shibari/util.h
@@ -26,6 +27,13 @@ src/common/shibari_util_get_prefixlen.o src/common/shibari_util_get_prefixlen.lo
 src/common/shibari_util_qtype_num.o src/common/shibari_util_qtype_num.lo: src/common/shibari_util_qtype_num.c src/include/shibari/util.h
 src/common/shibari_util_qtype_str.o src/common/shibari_util_qtype_str.lo: src/common/shibari_util_qtype_str.c src/include/shibari/util.h
 src/common/shibari_util_rcode_str.o src/common/shibari_util_rcode_str.lo: src/common/shibari_util_rcode_str.c src/include/shibari/util.h
+src/config/conftree.o src/config/conftree.lo: src/config/conftree.c src/config/shibari-cache-config-internal.h
+src/config/defaults.o src/config/defaults.lo: src/config/defaults.c src/config/shibari-cache-config-internal.h
+src/config/lexparse.o src/config/lexparse.lo: src/config/lexparse.c src/config/shibari-cache-config-internal.h src/include/shibari/config.h
+src/config/node.o src/config/node.lo: src/config/node.c src/config/shibari-cache-config-internal.h
+src/config/repo.o src/config/repo.lo: src/config/repo.c src/config/shibari-cache-config-internal.h
+src/config/shibari-cache-config.o src/config/shibari-cache-config.lo: src/config/shibari-cache-config.c src/config/shibari-cache-config-internal.h src/include/shibari/config.h
+src/config/util.o src/config/util.lo: src/config/util.c src/config/shibari-cache-config-internal.h
 src/server/shibari-server-tcp.o src/server/shibari-server-tcp.lo: src/server/shibari-server-tcp.c src/include/shibari/common.h src/include/shibari/server.h
 src/server/shibari-server-udp.o src/server/shibari-server-udp.lo: src/server/shibari-server-udp.c src/include/shibari/common.h src/include/shibari/server.h
 src/server/shibari_packet_add_glue.o src/server/shibari_packet_add_glue.lo: src/server/shibari_packet_add_glue.c src/include/shibari/constants.h src/include/shibari/packet.h src/include/shibari/tdb.h src/include/shibari/util.h
@@ -55,6 +63,8 @@ libshibari-common.a.xyzzy: src/common/shibari_log_answer.lo src/common/shibari_l
 endif
 libshibari-common.so.xyzzy: EXTRA_LIBS := -ls6dns -lskarnet
 libshibari-common.so.xyzzy: src/common/shibari_log_answer.lo src/common/shibari_log_exit.lo src/common/shibari_log_query.lo src/common/shibari_log_queryplus.lo src/common/shibari_log_start.lo src/common/shibari_util_qtype_num.lo src/common/shibari_util_qtype_str.lo src/common/shibari_util_rcode_str.lo src/common/shibari_util_canon_domain.lo src/common/shibari_util_get_prefixlen.lo
+shibari-cache-config: EXTRA_LIBS := -ls6dns -lskarnet
+shibari-cache-config: src/config/shibari-cache-config.o src/config/util.o src/config/node.o src/config/repo.o src/config/conftree.o src/config/defaults.o src/config/lexparse.o
 ifeq ($(strip $(STATIC_LIBS_ARE_PIC)),)
 libshibari-server.a.xyzzy: src/server/shibari_packet_init.o src/server/shibari_packet_begin.o src/server/shibari_packet_end.o src/server/shibari_packet_add_rr.o src/server/shibari_tdb_entry_parse.o src/server/shibari_tdb_extract_domain.o src/server/shibari_tdb_find_authority.o src/server/shibari_tdb_read_entry.o src/server/shibari_packet_add_glue.o src/server/shibari_packet_assert_authority.o src/server/shibari_packet_tdb_answer_query.o src/server/shibari_packet_tdb_axfr.o
 else
diff --git a/package/info b/package/info
index fc40b23..391cef7 100644
--- a/package/info
+++ b/package/info
@@ -1,4 +1,4 @@
 package=shibari
-version=0.0.1.1
+version=0.0.2.0
 category=web
 package_macro_name=SHIBARI
diff --git a/package/modes b/package/modes
index 94bdf3e..238d332 100644
--- a/package/modes
+++ b/package/modes
@@ -1,2 +1,3 @@
 shibari-server-tcp	0755
 shibari-server-udp	0755
+shibari-cache-config	0755
diff --git a/package/targets.mak b/package/targets.mak
index 0f2d479..a5a43cc 100644
--- a/package/targets.mak
+++ b/package/targets.mak
@@ -1,6 +1,7 @@
 BIN_TARGETS := \
 shibari-server-tcp \
-shibari-server-udp
+shibari-server-udp \
+shibari-cache-config
 
 LIBEXEC_TARGETS :=
 
diff --git a/src/cache/shibari-cache.c b/src/cache/shibari-cache.c
new file mode 100644
index 0000000..1d88556
--- /dev/null
+++ b/src/cache/shibari-cache.c
@@ -0,0 +1,282 @@
+/* ISC license. */
+
+#include <stdint.h>
+#include <string.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <signal.h>
+
+#include <skalibs/ip46.h>
+#include <skalibs/posixplz.h>
+#include <skalibs/uint16.h>
+#include <skalibs/uint32.h>
+#include <skalibs/types.h>
+#include <skalibs/error.h>
+#include <skalibs/strerr.h>
+#include <skalibs/sgetopt.h>
+#include <skalibs/allreadwrite.h>
+#include <skalibs/tai.h>
+#include <skalibs/socket.h>
+#include <skalibs/ip46.h>
+#include <skalibs/cdb.h>
+#include <skalibs/sig.h>
+#include <skalibs/iopause.h>
+#include <skalibs/selfpipe.h>
+
+#include <s6/accessrules.h>
+
+#include <shibari/common.h>
+#include <shibari/cache.h>
+
+#define USAGE "shibari-cache [ -U ] [ -v verbosity ] [ -d notif ] [ -D cachedumpfile ] [ -w wtimeout ] [ -i rulesdir | -x rulesfile ] ip[_port]..."
+#define dieusage() strerr_dieusage(100, USAGE)
+
+
+static char const *dumpfile = 0 ;
+
+
+typedef struct shibari_ip4_s shibari_ip4 ;
+struct shibari_ip4_s
+{
+  int fd ;
+  char ip[4] ;
+} ;
+
+typedef struct shibari_ip6_s shibari_ip6 ;
+struct shibari_ip6_s
+{
+  int fd ;
+  char ip[16] ;
+} ;
+
+static inline void argv_pass1 (char const *const *argv, unsigned int *n4, unsigned int *n6)
+{
+  char ip[16] ;
+  for (; *argv ; argv++)
+    if (ip6_scan(argv, ip))
+#ifdef SKALIBS_IPV6_ENABLED
+      n6++ ;
+#else
+      strerr_dief1x(100, "IPv6 listening addresses unsupported on this system") ;
+#endif
+    else n4++ ;
+}
+
+static inline void argv_pass2 (char const *const *argv, shibari_ip4 *ip4, shibari_ip6 *ip6, uint16_t *ports)
+{
+  unsigned int i4 = 0, i6 = 0 ;
+  char ip[16] ;
+  size_t len ;
+  for (; *argv ; argv++)
+  {
+    len = ip6_scan(argv, ip) ;
+    if (len)
+    {
+      if (argv[0][len] == '_')
+      {
+        uint16_t port ;
+        if (!uint160_scan(*argv + len + 1))
+          strerr_dief
+      }
+    }
+  }
+}
+
+static inline void reload_cdbs (void)
+{
+  cdb newtdb = CDB_ZERO ;
+  if (!cdb_init(&newtdb, tdbfile))
+  {
+    if (verbosity) strerr_warnwu2sys("reopen DNS data file ", tdbfile) ;
+  }
+  else
+  {
+    cdb_free(&tdb) ;
+    tdb = newtdb ;
+  }
+  if (rulestype == 2)
+  {
+    cdb newrules = CDB_ZERO ;
+    if (!cdb_init(&newrules, rulesfile))
+    {
+      if (verbosity) strerr_warnwu2sys("reopen access rules file ", rulesfile) ;
+    }
+    else
+    {
+      cdb_free(&rules) ;
+      rules = newrules ;
+    }
+  }
+}
+
+static int check_rules (ip46 const *remoteip, s6_accessrules_params_t *params, char const **loc)
+{
+  s6_accessrules_result_t r ;
+  params->env.len = 0 ;
+  params->exec.len = 0 ;
+  r = rulestype == 2 ?
+    s6_accessrules_ip46_cdb(remoteip, &rules, params) :
+    s6_accessrules_ip46_fs(remoteip, rulesfile, params) ;
+  if (r != S6_ACCESSRULES_ALLOW) return 0 ;
+
+  if (params->env.len)
+  {
+    char const *p ;
+    if (params->env.s[params->env.len - 1])
+    {
+      if (verbosity)
+      {
+        char fmt[IP46_FMT] ;
+        fmt[ip46_fmt(fmt, remoteip)] = 0 ;
+        strerr_warnw6x("invalid environment parameters in rules ", rulestype == 2 ? "cdb " : "directory ", rulesfile, " for ip ", fmt, " - denying connection") ;
+      }
+      return 0 ;
+    }
+    p = memmem(params->env.s, params->env.len - 1, VAR "=", sizeof(VAR)) ;
+    if (p && (p == params->env.s || !p[-1])) *loc = p + sizeof(VAR) ;
+  }
+  return 1 ;
+}
+
+static inline void handle_signals (void)
+{
+  for (;;) switch (selfpipe_read())
+  {
+    case -1 : strerr_diefu1sys(111, "read selfpipe") ;
+    case 0 : return ;
+    case SIGTERM : cont = 0 ; break ;
+    case SIGHUP : reload_cdbs() ; break ;
+    case SIGALRM : dump_cache() ; break ;
+    default : break ;
+  }
+}
+
+int main (int argc, char const *const *argv)
+{
+  unsigned int notif = 0 ;
+  unsigned int n4 = 0, n6 = 0 ;
+  uid_t uid = 0 ;
+  gid_t gid = 0 ;
+  PROG = "shibari-cache" ;
+  {
+    int flagdrop = 0 ;
+    subgetopt l = SUBGETOPT_ZERO ;
+    for (;;)
+    {
+      int opt = subgetopt_r(argc, argv, "Uv:d:D:w:i:x:", &l) ;
+      if (opt == -1) break ;
+      switch (opt)
+      {
+        case 'U' : flagdrop = 1 ; break ;
+        case 'v' : if (!uint320_scan(l.arg, &verbosity)) dieusage() ; break ;
+        case 'd' : if (!uint0_scan(l.arg, &notif)) dieusage() ; break ;
+        case 'D' : dumpfile = l.arg ; break ;
+        case 'w' : if (!uint0_scan(l.arg, &wtimeout)) dieusage() ; break ;
+        case 'i' : rulesfile = l.arg ; rulestype = 1 ; break ;
+        case 'x' : rulesfile = l.arg ; rulestype = 2 ; break ;
+        default : strerr_dieusage(10, USAGE) ;
+      }
+    }
+    argc -= l.ind ; argv += l.ind ;
+    if (!argc) default_iplist ;
+
+    if (!ip46_scan(argv[0], &localip)) dieusage() ;
+    if (flagdrop)
+    {
+      char const *x = getenv("UID") ;
+      if (!x) strerr_dienotset(100, "UID") ;
+      if (!uid0_scan(x, &uid)) strerr_dieinvalid(100, "UID") ;
+      x = getenv("GID") ;
+      if (!x) strerr_dienotset(100, "GID") ;
+      if (!uid0_scan(x, &gid)) strerr_dieinvalid(100, "GID") ;
+    }
+    if (wtimeout) tain_from_millisecs(&wtto, wtimeout) ;
+  }
+
+  if (notif)
+  {
+    if (notif < 3) strerr_dief1x(100, "notification fd cannot be 0, 1 or 2") ;
+    if (fcntl(notif, F_GETFD) == -1) strerr_diefu1sys(111, "check notification fd") ;
+  }
+
+  close(0) ;
+  close(1) ;
+  x[0].fd = selfpipe_init() ;
+  if (x[0].fd == -1) strerr_diefu1sys(111, "create selfpipe") ;
+  if (!sig_altignore(SIGPIPE)) strerr_diefu1sys(111, "ignore SIGPIPE") ;
+  {
+    sigset_t set ;
+    sigemptyset(&set) ;
+    sigaddset(&set, SIGHUP) ;
+    sigaddset(&set, SIGTERM) ;
+    if (!selfpipe_trapset(&set)) strerr_diefu1sys(111, "trap signals") ;
+  }
+
+  if (!cdb_init(&tdb, tdbfile)) strerr_diefu2sys(111, "open cdb file ", tdbfile) ;
+  if (rulestype == 2 && !cdb_init(&rules, rulesfile)) strerr_diefu2sys(111, "open rules file ", rulesfile) ;
+
+  x[1].fd = socket_udp46_nb(ip46_is6(&localip)) ;
+  if (x[1].fd == -1) strerr_diefu1sys(111, "create socket") ;
+  if (socket_bind46_reuse(x[1].fd, &localip, localport) == -1) strerr_diefu1sys(111, "bind socket") ;
+
+  if (gid && setgid(gid) == -1) strerr_diefu1sys(111, "setgid") ;
+  if (uid && setuid(uid) == -1) strerr_diefu1sys(111, "setuid") ;
+  if (!tain_now_set_stopwatch_g()) strerr_diefu1sys(111, "initialize clock") ;
+
+  shibari_log_start(verbosity, &localip, localport) ;
+  if (notif)
+  {
+    write(notif, "\n", 1) ;
+    close(notif) ;
+  }
+
+  while (cont)
+  {
+    tain wstamp = TAIN_INFINITE ;
+    char const *loc = 0 ;
+    s6dns_message_header_t hdr ;
+    s6dns_message_counts_t counts ;
+    s6dns_domain_t name ;
+    unsigned int rcode ;
+    ssize_t r ;
+    uint16_t qtype ;
+    uint16_t remoteport ;
+    ip46 remoteip ;
+
+    if (iopause_g(x, 2, &wstamp) == -1) strerr_diefu1sys(111, "iopause") ;
+    if (x[0].revents & IOPAUSE_EXCEPT) strerr_dief1x(111, "trouble with selfpipe") ;
+    if (x[0].revents & IOPAUSE_READ) { handle_signals() ; continue ; }
+
+    r = sanitize_read(socket_recv46(x[1].fd, buf, 512, &remoteip, &remoteport, ip46_is6(&localip))) ;
+    if (!r) continue ;
+    if (r == -1) strerr_diefu1sys(111, "recv from socket") ;
+    if (rulestype && !check_rules(&remoteip, &params, &loc)) continue ;
+    if (!s6dns_message_parse_init(&hdr, &counts, buf, r, &rcode)) continue ;
+    if (hdr.opcode) { rcode = 4 ; goto answer ; }
+    if (!s6dns_message_parse_question(&counts, &name, &qtype, buf, r, &rcode) || !s6dns_domain_encode(&name))
+    {
+      rcode = errno == ENOTSUP ? 4 : 1 ;
+      goto answer ;
+    }
+    shibari_log_queryplus(verbosity, &name, qtype, &remoteip, remoteport) ;
+    tain_wallclock_read(&wstamp) ;
+    rcode = shibari_packet_tdb_answer_query(&pkt, &tdb, &hdr, &name, qtype, loc, &wstamp) ;
+
+ answer:
+    if (rcode && rcode != 3)
+    {
+      shibari_packet_begin(&pkt, hdr.id, &name, qtype) ;
+      pkt.hdr.rcode = rcode ;
+      shibari_packet_end(&pkt) ;
+    }
+    shibari_log_answer(verbosity, &pkt.hdr, pkt.pos) ;
+    tain_add_g(&wstamp, &wtto) ;
+    if (socket_sendnb46_g(x[1].fd, buf, pkt.pos, &remoteip, remoteport, &wstamp) < pkt.pos && verbosity)
+      strerr_warnwu1sys("send answer") ;
+  }
+
+  shibari_log_exit(verbosity, 0) ;
+  return 0 ;
+}
diff --git a/src/config/PARSING-config.txt b/src/config/PARSING-config.txt
new file mode 100644
index 0000000..072a1fa
--- /dev/null
+++ b/src/config/PARSING-config.txt
@@ -0,0 +1,26 @@
+
+class	|	0	1	2	3	4
+st\ev	|	\0	space	#	\n	other
+
+START	|				P	np
+00	|	END	SPACE	COMMENT	START	WORD
+
+COMMENT	|				P
+01	|	END	COMMENT	COMMENT	START	COMMENT
+
+SPACE	|	P			P	np
+02	|	END	SPACE	COMMENT	START	WORD
+
+WORD	|	0P	0	p	0P	p
+03	|	END	SPACE	WORD	START	WORD
+
+END: 04
+X: 05
+
+states: 3 bits
+actions: 4 bits
+
+0x10	n	new word
+0x20	p	push cur
+0x40	0	push \0
+0x80	P	process line
diff --git a/src/config/conftree.c b/src/config/conftree.c
new file mode 100644
index 0000000..573646a
--- /dev/null
+++ b/src/config/conftree.c
@@ -0,0 +1,51 @@
+/* ISC license. */
+
+#include <skalibs/genalloc.h>
+#include <skalibs/avltree.h>
+#include <skalibs/cdbmake.h>
+
+#include "shibari-cache-config-internal.h"
+
+static repo conftree = \
+{ \
+  .ga = GENALLOC_ZERO, \
+  .tree = AVLTREE_INIT(8, 3, 8, &node_dtok, &node_cmp, &conftree.ga), \
+  .storage = &g.storage \
+} ;
+
+void confnode_start (node *node, char const *key, size_t filepos, uint32_t line)
+{
+  return node_start(&g.storage, node, key, filepos, line) ;
+}
+
+void confnode_add (node *node, char const *s, size_t len)
+{
+  return node_add(&g.storage, node, s, len) ;
+}
+
+node const *conftree_search (char const *key)
+{
+  return repo_search(&conftree, key) ;
+}
+
+void conftree_add (node const *node)
+{
+  return repo_add(&conftree, node) ;
+}
+
+void conftree_update (node const *node)
+{
+  return repo_update(&conftree, node) ;
+}
+
+static int confnode_write (uint32_t d, unsigned int h, void *data)
+{
+  node *nod = genalloc_s(node, &conftree.ga) + d ;
+  (void)h ;
+  return cdbmake_add((cdbmaker *)data, conftree.storage->s + nod->key, nod->keylen, conftree.storage->s + nod->data, nod->datalen) ;
+}
+
+int conftree_write (cdbmaker *cm)
+{
+  return avltree_iter(&conftree.tree, &confnode_write, cm) ;
+}
diff --git a/src/config/defaults.c b/src/config/defaults.c
new file mode 100644
index 0000000..14fce76
--- /dev/null
+++ b/src/config/defaults.c
@@ -0,0 +1,70 @@
+/* ISC license. */
+
+#include <stddef.h>
+
+#include "shibari-cache-config-internal.h"
+
+struct defaults_s
+{
+  char const *key ;
+  char const *value ;
+  size_t vlen ;
+} ;
+
+#define REC(k, v, n) { .key = (k), .value = (v), .vlen = (n) }
+#define RECS(k, v) REC(k, v, sizeof(v))
+#define RECU32(k, u) { .key = (k), .value = (char const [4]){ (u) >> 24 & 0xffu, (u) >> 16 & 0xffu, (u) >> 8 & 0xffu, (u) & 0xffu }, .vlen = 4 }
+
+static struct defaults_s const defaults[] =
+{
+  RECU32("G:logv", 1),
+  REC("G:listen4", "\0\0\0\0\0\35", 6),
+  REC("G:listen6", "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\35", 18),
+
+  REC("R4:",
+   "\0\306\51\0\4"
+   "\0\252\367\252\2"
+   "\0\300\41\4\14"
+   "\0\307\7\133\15"
+   "\0\300\313\346\12"
+   "\0\300\5\5\361"
+   "\0\300\160\44\4"
+   "\0\306\141\276\65"
+   "\0\300\44\224\21"
+   "\0\300\72\200\36"
+   "\0\301\0\16\201"
+   "\0\307\7\123\52"
+   "\0\312\14\33\41"
+   , 65),
+
+  REC("R6:",
+   "\0\40\1\5\3\272\76\0\0\0\0\0\0\0\2\0\60"
+   "\0\50\1\1\270\0\20\0\0\0\0\0\0\0\0\0\13"
+   "\0\40\1\5\0\0\2\0\0\0\0\0\0\0\0\0\14"
+   "\0\40\1\5\0\0\55\0\0\0\0\0\0\0\0\0\15"
+   "\0\40\1\5\0\0\250\0\0\0\0\0\0\0\0\0\16"
+   "\0\40\1\5\0\0\57\0\0\0\0\0\0\0\0\0\17"
+   "\0\40\1\5\0\0\22\0\0\0\0\0\0\0\0\15\15"
+   "\0\40\1\5\0\0\1\0\0\0\0\0\0\0\0\0\123"
+   "\0\40\1\7\376\0\0\0\0\0\0\0\0\0\0\0\123"
+   "\0\40\1\5\3\14\47\0\0\0\0\0\0\0\2\0\60"
+   "\0\40\1\7\375\0\0\0\0\0\0\0\0\0\0\0\1"
+   "\0\40\1\5\0\0\237\0\0\0\0\0\0\0\0\0\102"
+   "\0\40\1\15\303\0\0\0\0\0\0\0\0\0\0\0\65"
+   , 221),
+  REC(0, 0, 0)
+} ;
+
+void conf_defaults (void)
+{
+  for (struct defaults_s const *p = defaults ; p->key ; p++)
+  {
+    if (!conftree_search(p->key))
+    {
+      node node ;
+      confnode_start(&node, p->key, 0, 0) ;
+      confnode_add(&node, p->value, p->vlen) ;
+      conftree_add(&node) ;
+    }
+  }
+}
diff --git a/src/config/deps-exe/shibari-cache-config b/src/config/deps-exe/shibari-cache-config
new file mode 100644
index 0000000..1685f25
--- /dev/null
+++ b/src/config/deps-exe/shibari-cache-config
@@ -0,0 +1,8 @@
+util.o
+node.o
+repo.o
+conftree.o
+defaults.o
+lexparse.o
+-ls6dns
+-lskarnet
diff --git a/src/config/lexparse.c b/src/config/lexparse.c
new file mode 100644
index 0000000..9987fbd
--- /dev/null
+++ b/src/config/lexparse.c
@@ -0,0 +1,230 @@
+/* ISC license. */
+
+#include <stdint.h>
+#include <string.h>
+#include <stdlib.h>
+#include <errno.h>
+
+#include <skalibs/uint32.h>
+#include <skalibs/bitarray.h>
+#include <skalibs/buffer.h>
+#include <skalibs/strerr.h>
+#include <skalibs/stralloc.h>
+#include <skalibs/genalloc.h>
+#include <skalibs/skamisc.h>
+
+#include <s6-dns/s6dns-domain.h>
+
+#include <shibari/config.h>
+#include "shibari-cache-config-internal.h"
+
+#define dietoobig() strerr_diefu1sys(100, "read configuration")
+
+typedef struct mdt_s mdt, *mdt_ref ;
+struct mdt_s
+{
+  size_t filepos ;
+  uint32_t line ;
+  char linefmt[UINT32_FMT] ;
+} ;
+#define MDT_ZERO { .filepos = 0, .line = 0, .linefmt = "0" }
+
+struct namevalue_s
+{
+  char const *name ;
+  uint32_t value ;
+} ;
+
+enum directivevalue_e
+{
+  T_VERBOSITY,
+  T_LISTEN,
+  T_SERVER,
+  T_FORWARD,
+} ;
+
+static void conftree_checkunique (char const *key, mdt const *md)
+{
+  node const *node = conftree_search(key) ;
+  if (node)
+  {
+    char fmt[UINT32_FMT] ;
+    fmt[uint32_fmt(fmt, node->line)] = 0 ;
+    strerr_diefn(1, 12, "duplicate ", "key ", key, " in file ", g.storage.s + md->filepos, " line ", md->linefmt, ", previously defined", " in file ", g.storage.s + node->filepos, " line ", fmt) ;
+  }
+}
+
+static void add_unique (char const *key, char const *value, size_t valuelen, mdt const *md)
+{
+  node node ;
+  conftree_checkunique(key, md) ;
+  confnode_start(&node, key, md->filepos, md->line) ;
+  confnode_add(&node, value, valuelen) ;
+  conftree_add(&node) ;
+}
+
+static int ip40_scan (char const *s, char *ip)
+{
+  size_t len = ip4_scan(s, ip) ;
+  return len ? !s[len] : 0 ;
+}
+
+static int ip60_scan (char const *s, char *ip)
+{
+  size_t len = ip6_scan(s, ip) ;
+  return len ? !s[len] : 0 ;
+}
+
+static inline void parse_verbosity (char const *s, size_t const *word, size_t n, mdt const *md)
+{
+  uint32_t v ;
+  char pack[4] ;
+  if (n != 1)
+    strerr_dief8x(1, "too ", n ? "many" : "few", " arguments to directive ", "verbosity", " in file ", g.storage.s + md->filepos, " line ", md->linefmt) ;
+  if (!uint320_scan(s + word[0], &v))
+    strerr_dief7x(1, " argument to directive ", "verbosity", " must be an integer ", " in file ", g.storage.s + md->filepos, " line ", md->linefmt) ;
+  uint32_pack_big(pack, v) ;
+  add_unique("G:logv", pack, 4, md) ;
+}
+
+static inline void parse_listen (char const *s, size_t const *word, size_t n, mdt const *md)
+{
+  if (!n)
+    strerr_dief6x(1, "too few arguments to directive ", "listen", " in file ", g.storage.s + md->filepos, " line ", md->linefmt) ;
+  {
+    size_t n4 = 0, n6 = 0 ;
+    char ip6[n << 4] ;
+    char ip4[n << 2] ;
+    for (size_t i = 0 ; i < n ; i++)
+    {
+      if (ip60_scan(s + word[i], ip6 + (n6 << 4))) n6++ ;
+      else if (ip40_scan(s + word[i], ip4 + (n4 << 2))) n4++ ;
+      else strerr_dief6x(1, "arguments to directive ", "listen", " must be IPs in file ", g.storage.s + md->filepos, " line ", md->linefmt) ;
+    }
+    add_unique("G:listen4", ip4, n4 << 2, md) ;
+    add_unique("G:listen6", ip6, n6 << 4, md) ;
+  }
+}
+
+static inline void parse_server (char const *s, size_t const *word, size_t n, mdt const *md, int forward)
+{
+  char const *x = forward ? "forward" : "server" ;
+  s6dns_domain_t domain ;
+  if (n-- < 2)
+    strerr_dief8x(1, "too ", "few", " arguments to directive ", x, " in file ", g.storage.s + md->filepos, " line ", md->linefmt) ;
+  if (!s6dns_domain_fromstring(&domain, s + word[0], strlen(s + word[0]))
+   || !s6dns_domain_noqualify(&domain))
+    strerr_dief7x(1, "first argument to directive ", x, " must be a zone ", " in file ", g.storage.s + md->filepos, " line ", md->linefmt) ;
+  word++ ;
+  {
+    size_t n4 = 0, n6 = 0 ;
+    char ip6[n * 17] ;
+    char ip4[n * 5] ;
+    char key[3 + domain.len] ;
+    for (size_t i = 0 ; i < n ; i++)
+    {
+      if (ip60_scan(s + word[i], ip6 + (n6 * 17) + 1)) ip6[n6++ * 17] = !!forward ;
+      else if (ip40_scan(s + word[i], ip4 + (n4 * 5) + 1)) ip4[n4++ * 5] = !!forward ;
+      else strerr_dief6x(1, "subsequent arguments to directive ", x, " must be IPs in file ", g.storage.s + md->filepos, " line ", md->linefmt) ;
+    }
+    memcpy(key, "R4:", 3) ;
+    memcpy(key + 3, domain.s + 1, domain.len - 1) ;
+    key[2 + domain.len] = 0 ;
+    add_unique(key, ip4, n4 * 5, md) ;
+    key[1] = '6' ;
+    add_unique(key, ip6, n6 * 17, md) ; 
+  }
+}
+
+static inline void process_line (char const *s, size_t const *word, size_t n, mdt *md)
+{
+  static struct namevalue_s const directives[] =
+  {
+    { .name = "forward", .value = T_FORWARD },
+    { .name = "listen", .value = T_LISTEN },
+    { .name = "server", .value = T_SERVER },
+    { .name = "verbosity", .value = T_VERBOSITY },
+  } ;
+  struct namevalue_s const *directive ;
+  char const *word0 ;
+  if (!n--) return ;
+  word0 = s + *word++ ;
+  directive = BSEARCH(struct namevalue_s, word0, directives) ;
+  if (!directive)
+    strerr_dief6x(1, "unrecognized word ", word0, " in file ", g.storage.s + md->filepos, " line ", md->linefmt) ;
+  switch (directive->value)
+  {
+    case T_VERBOSITY :
+      parse_verbosity(s, word, n, md) ;
+      break ;
+    case T_LISTEN :
+      parse_listen(s, word, n, md) ;
+      break ;
+    case T_SERVER :
+      parse_server(s, word, n, md, 0) ;
+      break ;
+    case T_FORWARD :
+      parse_server(s, word, n, md, 1) ;
+      break ;
+  }
+}
+
+static inline uint8_t cclass (char c)
+{
+  switch (c)
+  {
+    case 0 : return 0 ;
+    case ' ' :
+    case '\t' :
+    case '\f' :
+    case '\r' : return 1 ;
+    case '#' : return 2 ;
+    case '\n' : return 3 ;
+    default : return 4 ;
+  }
+}
+
+static inline char next (buffer *b, mdt const *md)
+{
+  char c ;
+  ssize_t r = buffer_get(b, &c, 1) ;
+  if (r == -1) strerr_diefu1sys(111, "read from preprocessor") ;
+  if (!r) return 0 ;
+  if (!c) strerr_dief5x(1, "null character", " in file ", g.storage.s + md->filepos, " line ", md->linefmt) ;
+  return c ;
+}
+
+void conf_lexparse (buffer *b, char const *ifile)
+{
+  static uint8_t const table[4][5] =  /* see PARSING-config.txt */
+  {
+    { 0x04, 0x02, 0x01, 0x80, 0x33 },
+    { 0x04, 0x01, 0x01, 0x80, 0x01 },
+    { 0x84, 0x02, 0x01, 0x80, 0x33 },
+    { 0xc4, 0x42, 0x23, 0xc0, 0x23 }
+  } ;
+  stralloc sa = STRALLOC_ZERO ;
+  genalloc words = GENALLOC_ZERO ; /* size_t */
+  mdt md = MDT_ZERO ;
+  uint8_t state = 0 ;
+  if (!stralloc_catb(&g.storage, ifile, strlen(ifile) + 1)) dienomem() ;
+  while (state < 0x04)
+  {
+    char c = next(b, &md) ;
+    uint8_t what = table[state][cclass(c)] ;
+    state = what & 0x07 ;
+    if (what & 0x10) if (!genalloc_catb(size_t, &words, &sa.len, 1)) dienomem() ;
+    if (what & 0x20) if (!stralloc_catb(&sa, &c, 1)) dienomem() ; 
+    if (what & 0x40) if (!stralloc_0(&sa)) dienomem() ;
+    if (what & 0x80)
+    {
+      process_line(sa.s, genalloc_s(size_t, &words), genalloc_len(size_t, &words), &md) ;
+      genalloc_setlen(size_t, &words, 0) ;
+      sa.len = 0 ;
+      md.line++ ;
+      md.linefmt[uint32_fmt(md.linefmt, md.line)] = 0 ;
+    }
+  }
+  genalloc_free(size_t, &words) ;
+  stralloc_free(&sa) ;
+}
diff --git a/src/config/node.c b/src/config/node.c
new file mode 100644
index 0000000..7e6cd4b
--- /dev/null
+++ b/src/config/node.c
@@ -0,0 +1,34 @@
+/* ISC license. */
+
+#include <stdint.h>
+#include <string.h>
+
+#include <skalibs/stralloc.h>
+#include <skalibs/strerr.h>
+
+#include "shibari-cache-config-internal.h"
+
+#define diestorage() strerr_diefu2x(100, "add node to configuration tree", ": too much data")
+#define diefilepos() strerr_diefu2x(100, "add node to configuration tree", ": file too large")
+
+void node_start (stralloc *storage, node *node, char const *key, size_t filepos, uint32_t line)
+{
+  size_t l = strlen(key) ;
+  size_t k = storage->len ;
+  if (!stralloc_catb(storage, key, l + 1)) dienomem() ;
+  if (storage->len >= UINT32_MAX) diestorage() ;
+  if (filepos > UINT32_MAX) diefilepos() ;
+  node->key = k ;
+  node->keylen = l ;
+  node->data = storage->len ;
+  node->datalen = 0 ;
+  node->filepos = filepos ;
+  node->line = line ;
+}
+
+void node_add (stralloc *storage, node *node, char const *s, size_t len)
+{
+  if (!stralloc_catb(storage, s, len)) dienomem() ;
+  if (storage->len >= UINT32_MAX) diestorage() ;
+  node->datalen += len ;
+}
diff --git a/src/config/repo.c b/src/config/repo.c
new file mode 100644
index 0000000..8ed8c51
--- /dev/null
+++ b/src/config/repo.c
@@ -0,0 +1,46 @@
+/* ISC license. */
+
+#include <stdint.h>
+#include <string.h>
+
+#include <skalibs/genalloc.h>
+#include <skalibs/avltree.h>
+
+#include "shibari-cache-config-internal.h"
+
+void *node_dtok (uint32_t d, void *data)
+{
+  repo *r = data ;
+  return r->storage->s + genalloc_s(node, &r->ga)[d].key ;
+}
+
+int node_cmp (void const *a, void const *b, void *data)
+{
+  (void)data ;
+  return strcmp((char const *)a, (char const *)b) ;
+}
+
+node const *repo_search (repo const *r, char const *key)
+{
+  uint32_t i ;
+  return avltree_search(&r->tree, key, &i) ? genalloc_s(node const, &r->ga) + i : 0 ;
+}
+
+void repo_add (repo *r, node const *nod)
+{
+  uint32_t i = genalloc_len(node, &r->ga) ;
+  if (!genalloc_append(node, &r->ga, nod)) dienomem() ;
+  if (!avltree_insert(&r->tree, i)) dienomem() ;
+}
+
+void repo_update (repo *r, node const *nod)
+{
+  uint32_t i ;
+  if (avltree_search(&r->tree, r->storage->s + nod->key, &i))
+  {
+    if (!avltree_delete(&r->tree, r->storage->s + nod->key)) dienomem() ;
+    genalloc_s(node, &r->ga)[i] = *nod ;
+    if (!avltree_insert(&r->tree, i)) dienomem() ;
+  }
+  else repo_add(r, nod) ;
+}
diff --git a/src/config/shibari-cache-config-internal.h b/src/config/shibari-cache-config-internal.h
new file mode 100644
index 0000000..c7b0197
--- /dev/null
+++ b/src/config/shibari-cache-config-internal.h
@@ -0,0 +1,91 @@
+/* ISC license. */
+
+#ifndef SHIBARI_CACHE_CONFIG_INTERNAL_H
+#define SHIBARI_CACHE_CONFIG_INTERNAL_H
+
+#include <stdint.h>
+#include <string.h>
+#include <stdlib.h>
+
+#include <skalibs/buffer.h>
+#include <skalibs/strerr.h>
+#include <skalibs/stralloc.h>
+#include <skalibs/genalloc.h>
+#include <skalibs/cdbmake.h>
+#include <skalibs/avltree.h>
+
+#define dienomem() strerr_diefu1sys(111, "stralloc_catb")
+
+typedef struct node_s node, *node_ref ;
+struct node_s
+{
+  uint32_t key ;
+  uint32_t keylen ;
+  uint32_t data ;
+  uint32_t datalen ;
+  uint32_t filepos ;
+  uint32_t line ;
+} ;
+#define NODE_ZERO { .key = 0, .keylen = 0, .data = 0, .datalen = 0 }
+
+typedef struct repo_s repo, *repo_ref ;
+struct repo_s
+{
+  genalloc ga ;
+  avltree tree ;
+  stralloc *storage ;
+} ;
+#define REPO_ZERO { .ga = GENALLOC_ZERO, .tree = AVLTREE_ZERO, .storage = 0 }
+
+struct global_s
+{
+  stralloc storage ;
+} ;
+#define GLOBAL_ZERO { .storage = STRALLOC_ZERO }
+
+extern struct global_s g ;
+
+
+ /* util */
+
+extern int keycmp (void const *, void const *) ;  /* for any struct starting with a string key */
+#define BSEARCH(type, key, array) bsearch(key, (array), sizeof(array)/sizeof(type), sizeof(type), &keycmp)
+
+
+ /* node */
+
+extern void node_start (stralloc *, node *, char const *, size_t, uint32_t) ;
+extern void node_add (stralloc *, node *, char const *, size_t) ;
+
+
+ /* repo */
+
+extern void *node_dtok (uint32_t, void *) ;
+extern int node_cmp (void const *, void const *, void *) ;
+extern node const *repo_search (repo const *, char const *) ;
+extern void repo_add (repo *, node const *) ;
+extern void repo_update (repo *, node const *) ;
+
+
+ /* conftree */
+
+extern void confnode_start (node *, char const *, size_t, uint32_t) ;
+extern void confnode_add (node *, char const *, size_t) ;
+
+extern node const *conftree_search (char const *) ;
+extern void conftree_add (node const *) ;
+extern void conftree_update (node const *) ;
+
+extern int conftree_write (cdbmaker *) ;
+
+
+ /* lexparse */
+
+extern void conf_lexparse (buffer *, char const *) ;
+
+
+ /* defaults */
+
+extern void conf_defaults (void) ;
+
+#endif
diff --git a/src/config/shibari-cache-config.c b/src/config/shibari-cache-config.c
new file mode 100644
index 0000000..0aef2ce
--- /dev/null
+++ b/src/config/shibari-cache-config.c
@@ -0,0 +1,101 @@
+/* ISC license. */
+
+#include <string.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <stdio.h>  /* rename() */
+#include <errno.h>
+#include <signal.h>
+
+#include <skalibs/posixplz.h>
+#include <skalibs/types.h>
+#include <skalibs/sgetopt.h>
+#include <skalibs/buffer.h>
+#include <skalibs/strerr.h>
+#include <skalibs/djbunix.h>
+
+#include <shibari/config.h>
+#include "shibari-cache-config-internal.h"
+
+#define USAGE "shibari-cache-config [ -i textfile ] [ -o cdbfile ] [ -m mode ]"
+#define dieusage() strerr_dieusage(100, USAGE)
+
+struct global_s g = GLOBAL_ZERO ;
+
+static inline void conf_output (char const *ofile, unsigned int omode)
+{
+  int fdw ;
+  cdbmaker cm = CDBMAKER_ZERO ;
+  size_t olen = strlen(ofile) ;
+  char otmp[olen + 8] ;
+  memcpy(otmp, ofile, olen) ;
+  memcpy(otmp + olen, ":XXXXXX", 8) ;
+  fdw = mkstemp(otmp) ;
+  if (fdw == -1) strerr_diefu3sys(111, "open ", otmp, " for writing") ;
+  if (!cdbmake_start(&cm, fdw))
+  {
+    unlink_void(otmp) ;
+    strerr_diefu2sys(111, "cdmake_start ", otmp) ;
+  }
+  if (!conftree_write(&cm))
+  {
+    unlink_void(otmp) ;
+    strerr_diefu2sys(111, "write config tree into ", otmp) ;
+  }
+  if (!cdbmake_finish(&cm))
+  {
+    unlink_void(otmp) ;
+    strerr_diefu2sys(111, "cdbmake_finish ", otmp) ;
+  }
+  if (fsync(fdw) == -1)
+  {
+    unlink_void(otmp) ;
+    strerr_diefu2sys(111, "fsync ", otmp) ;
+  }
+  if (fchmod(fdw, omode & 0777) == -1)
+  {
+    unlink_void(otmp) ;
+    strerr_diefu2sys(111, "fchmod ", otmp) ;
+  }
+  if (rename(otmp, ofile) == -1)
+  {
+    unlink_void(otmp) ;
+    strerr_diefu4sys(111, "rename ", otmp, " to ", ofile) ;
+  }
+}
+
+int main (int argc, char const *const *argv, char const *const *envp)
+{
+  char const *ifile = SHIBARI_SYSCONFPREFIX "shibari-cache.conf" ;
+  char const *ofile = SHIBARI_SYSCONFPREFIX "shibari-cache.conf.cdb" ;
+  unsigned int omode = 0644 ;
+
+  PROG = "shibari-cache-config" ;
+  {
+    subgetopt l = SUBGETOPT_ZERO ;
+    for (;;)
+    {
+      int opt = subgetopt_r(argc, argv, "i:o:m:", &l) ;
+      if (opt == -1) break ;
+      switch (opt)
+      {
+        case 'i' : ifile = l.arg ; break ;
+        case 'o' : ofile = l.arg ; break ;
+        case 'm' : if (!uint0_oscan(l.arg, &omode)) dieusage() ; break ;
+        default : strerr_dieusage(100, USAGE) ;
+      }
+    }
+    argc -= l.ind ; argv += l.ind ;
+  }
+
+  {
+    int fdr = openc_readb(ifile) ;
+    char buf[4096] ;
+    buffer b = BUFFER_INIT(&buffer_read, fdr, buf, 4096) ;
+    if (fdr == -1) strerr_diefu2sys(111, "open ", ifile) ;
+    conf_lexparse(&b, ifile) ;
+  }
+  conf_defaults() ;
+  conf_output(ofile, omode) ;
+  return 0 ;
+}
diff --git a/src/config/util.c b/src/config/util.c
new file mode 100644
index 0000000..f3da287
--- /dev/null
+++ b/src/config/util.c
@@ -0,0 +1,15 @@
+/* ISC license. */
+
+#include <string.h>
+
+#include "shibari-cache-config-internal.h"
+
+struct starts_with_a_string_key_s
+{
+  char const *s ;
+} ;
+
+int keycmp (void const *a, void const *b)
+{
+  return strcmp((char const *)a, ((struct starts_with_a_string_key_s const *)b)->s) ;
+}