about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorLaurent Bercot <ska-skaware@skarnet.org>2020-12-28 22:09:00 +0000
committerLaurent Bercot <ska-skaware@skarnet.org>2020-12-28 22:09:00 +0000
commitfab29b78d87d0114ecbdfbcac3b0e7c0edb463ba (patch)
tree786d09f619fa7302851e02fa7d51f53151db518c /src
downloaddnsfunnel-fab29b78d87d0114ecbdfbcac3b0e7c0edb463ba.tar.gz
dnsfunnel-fab29b78d87d0114ecbdfbcac3b0e7c0edb463ba.tar.xz
dnsfunnel-fab29b78d87d0114ecbdfbcac3b0e7c0edb463ba.zip
Initial commit
Diffstat (limited to 'src')
-rw-r--r--src/dnsfunnel/deps-exe/dnsfunnel-daemon1
-rw-r--r--src/dnsfunnel/deps-exe/dnsfunnel-translate1
-rw-r--r--src/dnsfunnel/deps-exe/dnsfunneld4
-rw-r--r--src/dnsfunnel/dnsfunnel-daemon.c150
-rw-r--r--src/dnsfunnel/dnsfunnel-translate.c93
-rw-r--r--src/dnsfunnel/dnsfunneld.c305
-rw-r--r--src/dnsfunnel/dnsfunneld.h44
-rw-r--r--src/dnsfunnel/dnsfunneld_answer.c132
-rw-r--r--src/dnsfunnel/dnsfunneld_process.c136
9 files changed, 866 insertions, 0 deletions
diff --git a/src/dnsfunnel/deps-exe/dnsfunnel-daemon b/src/dnsfunnel/deps-exe/dnsfunnel-daemon
new file mode 100644
index 0000000..e7187fe
--- /dev/null
+++ b/src/dnsfunnel/deps-exe/dnsfunnel-daemon
@@ -0,0 +1 @@
+-lskarnet
diff --git a/src/dnsfunnel/deps-exe/dnsfunnel-translate b/src/dnsfunnel/deps-exe/dnsfunnel-translate
new file mode 100644
index 0000000..e7187fe
--- /dev/null
+++ b/src/dnsfunnel/deps-exe/dnsfunnel-translate
@@ -0,0 +1 @@
+-lskarnet
diff --git a/src/dnsfunnel/deps-exe/dnsfunneld b/src/dnsfunnel/deps-exe/dnsfunneld
new file mode 100644
index 0000000..90302a1
--- /dev/null
+++ b/src/dnsfunnel/deps-exe/dnsfunneld
@@ -0,0 +1,4 @@
+dnsfunneld_answer.o
+dnsfunneld_process.o
+-ls6dns
+-lskarnet
diff --git a/src/dnsfunnel/dnsfunnel-daemon.c b/src/dnsfunnel/dnsfunnel-daemon.c
new file mode 100644
index 0000000..1df6a38
--- /dev/null
+++ b/src/dnsfunnel/dnsfunnel-daemon.c
@@ -0,0 +1,150 @@
+/* ISC license. */
+
+#include <skalibs/sysdeps.h>
+
+#ifndef SKALIBS_HASCHROOT
+# error "this program can only be built on systems that provide a chroot() function"
+#endif
+
+#include <skalibs/nonposix.h>  /* chroot */
+#include <stdint.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <stdlib.h>
+
+#include <skalibs/uint16.h>
+#include <skalibs/types.h>
+#include <skalibs/fmtscan.h>
+#include <skalibs/sgetopt.h>
+#include <skalibs/strerr2.h>
+#include <skalibs/djbunix.h>
+#include <skalibs/socket.h>
+#include <skalibs/exec.h>
+
+#include <dnsfunnel/config.h>
+
+#define USAGE "dnsfunnel-daemon [ -v verbosity ] [ -d notif ] [ -U | -u uid -g gid ] [ -i ip:port ] [ -R root ] [ -b bufsize ] [ -f cachelist ] [ -T | -t ] [ -N | -n ] "
+#define dieusage() strerr_dieusage(100, USAGE)
+
+int main (int argc, char const *const *argv)
+{
+  int notif = 0 ;
+  unsigned int verbosity = 1 ;
+  unsigned int bufsize = 131072 ;
+  int flagU = 0 ;
+  uid_t uid = -1 ;
+  gid_t gid = -1 ;
+  char const *ipport = "127.0.0.1:53" ;
+  char const *newroot = 0 ;
+  char const *cachelist = DNSFUNNEL_DEFAULT_CACHELIST ;
+  uint32_t ops = 0 ;
+  PROG = "dnsfunnel-daemon" ;
+  {
+    subgetopt_t l = SUBGETOPT_ZERO ;
+    for (;;)
+    {
+      int opt = subgetopt_r(argc, argv, "v:d:Uu:g:i:R:b:f:TtNn", &l) ;
+      if (opt == -1) break ;
+      switch (opt)
+      {
+        case 'v' : if (!uint0_scan(l.arg, &verbosity)) dieusage() ; break ;
+        case 'd' : if (!uint0_scan(l.arg, (unsigned int *)&notif)) dieusage() ; break ;
+        case 'U' : flagU = 1 ; break ;
+        case 'u' : if (!uid0_scan(l.arg, &uid)) dieusage() ; break ;
+        case 'g' : if (!gid0_scan(l.arg, &gid)) dieusage() ; break ;
+        case 'i' : ipport = l.arg ; break ;
+        case 'R' : newroot = l.arg ; break ;
+        case 'b' : if (!uint0_scan(l.arg, &bufsize)) dieusage() ; break ;
+        case 'f' : cachelist = l.arg ; break ;
+        case 'T' : ops &= ~1 ; break ;
+        case 't' : ops |= 1 ; break ;
+        case 'N' : ops &= ~2 ; break ;
+        case 'n' : ops |= 2 ; break ;
+        default : dieusage() ;
+      }
+    }
+    argc -= l.ind ; argv += l.ind ;
+  }
+
+  {
+    int fd ;
+    char ip[4] ;
+    uint16_t port ;
+    size_t pos = ip4_scan(ipport, ip) ;
+    if (!pos) dieusage() ;
+    if (ipport[pos] != ':') dieusage() ;
+    if (!uint160_scan(ipport + pos + 1, &port)) dieusage() ;
+    fd = socket_udp4() ;
+    if (fd < 0) strerr_diefu1sys(111, "create UDP socket") ;
+    if (socket_bind4_reuse(fd, ip, port) < 0)
+    {
+      char fmti[IP4_FMT] ;
+      char fmtp[UINT16_FMT] ;
+      fmti[ip4_fmt(fmti, ip)] = 0 ;
+      fmtp[uint16_fmt(fmtp, port)] = 0 ;
+      strerr_diefu4sys(111, "bind on ip ", fmti, " port ", fmtp) ;
+    }
+    if (bufsize) socket_tryreservein(fd, bufsize) ;
+    if (fd_move(0, fd) < 0)
+      strerr_diefu1sys(111, "move file descriptors") ;
+  }
+
+  if (newroot)
+  {
+    if (chdir(newroot) < 0 || chroot(".") < 0)
+      strerr_diefu2sys(111, "chroot to ", newroot) ;
+  }
+
+  if (flagU)
+  {
+    char const *x = getenv("UID") ;
+    if (x && !uid0_scan(x, &uid))
+      strerr_dieinvalid(100, "UID") ;
+    x = getenv("GID") ;
+    if (x && !gid0_scan(x, &gid))
+      strerr_dieinvalid(100, "GID") ;
+  }
+  if (gid != (gid_t)-1 && setgid(gid) < 0)
+  {
+    char fmt[GID_FMT] ;
+    fmt[gid_fmt(fmt, gid)] = 0 ;
+    strerr_diefu2sys(111, "setgid to ", fmt) ;
+  }
+  if (uid != (uid_t)-1 && setuid(uid) < 0)
+  {
+    char fmt[UID_FMT] ;
+    fmt[uid_fmt(fmt, uid)] = 0 ;
+    strerr_diefu2sys(111, "setuid to ", fmt) ;
+  }
+
+  {
+    char const *newargv[10] = { "dnsfunneld" } ;
+    char const *newenvp[1] = { 0 } ;
+    unsigned int m = 1 ;
+    char fmtv[UINT_FMT] ;
+    char fmtn[UINT_FMT] ;
+    char fmto[UINT_FMT] ;
+    if (verbosity != 1)
+    {
+      fmtv[uint_fmt(fmtv, verbosity)] = 0 ;
+      newargv[m++] = "-v" ;
+      newargv[m++] = fmtv ;
+    }
+    if (notif)
+    {
+      fmtn[uint_fmt(fmtn, notif)] = 0 ;
+      newargv[m++] = "-d" ;
+      newargv[m++] = fmtn ;
+    }
+    if (ops)
+    {
+      fmto[uint_fmt(fmto, ops)] = 0 ;
+      newargv[m++] = "-o" ;
+      newargv[m++] = fmto ;
+    }   
+    newargv[m++] = "--" ;
+    newargv[m++] = cachelist ;
+    newargv[m++] = 0 ;
+    xexec_ae(DNSFUNNEL_BINPREFIX "dnsfunneld", newargv, newenvp) ;
+  }
+}
diff --git a/src/dnsfunnel/dnsfunnel-translate.c b/src/dnsfunnel/dnsfunnel-translate.c
new file mode 100644
index 0000000..70610b8
--- /dev/null
+++ b/src/dnsfunnel/dnsfunnel-translate.c
@@ -0,0 +1,93 @@
+/* ISC license. */
+
+#include <string.h>
+
+#include <skalibs/bytestr.h>
+#include <skalibs/sgetopt.h>
+#include <skalibs/buffer.h>
+#include <skalibs/fmtscan.h>
+#include <skalibs/strerr2.h>
+#include <skalibs/djbunix.h>
+#include <skalibs/ip46.h>
+
+#include <s6-dns/s6dns-constants.h>
+
+#include <dnsfunnel/config.h>
+
+#define USAGE "dnsfunnel-translate [ -i resolvconf ] [ -o cachelist ] [ -x ignoredip ]"
+#define dieusage() strerr_dieusage(100, USAGE)
+
+
+static size_t parse_nameservers (ip46_t *list, char const *file, char const *ignore)
+{
+  static char const zero[SKALIBS_IP_SIZE] = { 0 } ;
+  char buf[4096] ;
+  size_t n = 0, i = 0 ;
+  ssize_t len = openreadnclose(file, buf, 4095) ;
+  if (len < 0) strerr_diefu2sys(111, "open ", file) ;
+  buf[len++] = '\n' ;
+  while ((i < len) && (n < S6DNS_MAX_SERVERS))
+  {
+    size_t j = byte_chr(buf + i, len - i, '\n') ;
+    if ((i + j < len) && (j > 13U) && !memcmp("nameserver", buf + i, 10))
+    {
+      size_t k = 0 ;
+      while ((buf[i+10+k] == ' ') || (buf[i+10+k] == '\t')) k++ ;
+      if (k && ip46_scan(buf+i+10+k, list + n)
+       && memcmp(list[n].ip, zero, SKALIBS_IP_SIZE)
+       && (ip46_is6(list + n) || memcmp(list[n].ip, ignore, 4))
+      ) n++ ;
+    }
+    i += j + 1 ;
+  }
+  return n ;
+}
+
+
+int main (int argc, char const *const *argv)
+{
+  ip46_t list[S6DNS_MAX_SERVERS] = { IP46_ZERO } ;
+  char const *resolvconf = "/etc/resolv.conf" ;
+  char const *cachelist = DNSFUNNEL_DEFAULT_CACHELIST ;
+  char ignore[4] = "\177\0\0\1" ;
+  size_t n ;
+  PROG = "dnsfunnel-translate" ;
+  {
+    subgetopt_t l = SUBGETOPT_ZERO ;
+    for (;;)
+    {
+      int opt = subgetopt_r(argc, argv, "i:o:x:", &l) ;
+      if (opt == -1) break ;
+      switch (opt)
+      {
+        case 'i' : resolvconf = l.arg ; break ;
+        case 'o' : cachelist = l.arg ; break ;
+        case 'x' : if (!ip4_scan(l.arg, ignore)) dieusage() ; break ;
+        default : dieusage() ;
+      }
+    }
+    argc -= l.ind ; argv += l.ind ;
+  }
+
+  n = parse_nameservers(list, resolvconf, ignore) ;
+  if (!n) strerr_dief2x(1, "no suitable cache address in ", resolvconf) ;
+
+  {
+    char buf[4096] ;
+    buffer b ;
+    int fd = openc_trunc(cachelist) ;
+    if (fd < 0) strerr_diefu2sys(111, "open ", cachelist) ;
+    buffer_init(&b, &buffer_write, fd, buf, 4096) ;
+    for (size_t i = 0 ; i < n ; i++)
+    {
+      char fmt[IP46_FMT] ;
+      size_t len = ip46_fmt(fmt, list + i) ;
+      fmt[len++] = '\n' ;
+      if (buffer_put(&b, fmt, len) < len)
+        strerr_diefu2sys(111, "write to ", cachelist) ;
+    }
+    if (!buffer_flush(&b))
+      strerr_diefu2sys(111, "write to ", cachelist) ;
+  }
+  return 0 ;
+}
diff --git a/src/dnsfunnel/dnsfunneld.c b/src/dnsfunnel/dnsfunneld.c
new file mode 100644
index 0000000..bd4dc89
--- /dev/null
+++ b/src/dnsfunnel/dnsfunneld.c
@@ -0,0 +1,305 @@
+/* ISC license. */
+
+#include <stdint.h>
+#include <string.h>
+#include <signal.h>
+#include <fcntl.h>
+#include <errno.h>
+
+#include <skalibs/uint32.h>
+#include <skalibs/types.h>
+#include <skalibs/allreadwrite.h>
+#include <skalibs/error.h>
+#include <skalibs/bitarray.h>
+#include <skalibs/strerr2.h>
+#include <skalibs/sgetopt.h>
+#include <skalibs/stralloc.h>
+#include <skalibs/sig.h>
+#include <skalibs/socket.h>
+#include <skalibs/tai.h>
+#include <skalibs/djbunix.h>
+#include <skalibs/iopause.h>
+#include <skalibs/selfpipe.h>
+#include <skalibs/gensetdyn.h>
+
+#include <s6-dns/s6dns.h>
+
+#include "dnsfunneld.h"
+
+#define USAGE "dnsfunneld [ -v verbosity ] [ -d notif ] [ -o operations ] cachelist"
+#define dieusage() strerr_dieusage(100, USAGE)
+
+#define DNSFUNNELD_INPUT_MAX 64
+
+unsigned int verbosity = 1 ;
+static tain_t globaltto = TAIN_INFINITE_RELATIVE ;
+static int cont = 1 ;
+static char const *cachelistfile = 0 ;
+static s6dns_ip46list_t cachelist ;
+static uint32_t ops = 0 ;
+
+static inline void X (void)
+{
+  strerr_dief1x(101, "internal inconsistency. Please submit a bug-report.") ;
+}
+
+static inline void s6dns_ip46list_copy (s6dns_ip46list_t *dst, ip46full_t const *src, size_t n)
+{
+  if (n >= S6DNS_MAX_SERVERS) n = S6DNS_MAX_SERVERS - 1 ;
+  for (size_t i = 0 ; i < n ; i++)
+  {
+    memcpy(dst->ip + i * SKALIBS_IP_SIZE, src[i].ip, SKALIBS_IP_SIZE) ;
+#ifdef SKALIBS_IPV6_ENABLED
+    bitarray_poke(dst->is6, i, ip46_is6(src + i)) ;
+#endif
+  }
+  memset(dst->ip + n * SKALIBS_IP_SIZE, 0, SKALIBS_IP_SIZE) ;
+}
+
+static int load_cachelist (int initial)
+{
+  char buf[4096] ;
+  ip46full_t list[S6DNS_MAX_SERVERS] ;
+  size_t n ;
+  ssize_t r = openreadnclose_nb(cachelistfile, buf, 4095) ;
+  if (r < 0) return -1 ;
+  buf[r++] = 0 ;
+  ip46_scanlist(list, S6DNS_MAX_SERVERS, buf, &n) ;
+  if (!n) return -2 ;
+  s6dns_ip46list_copy(&cachelist, list, n) ;
+  return 0 ;
+}
+
+static inline void handle_signals (void)
+{
+  for (;;)
+  {
+    switch (selfpipe_read())
+    {
+      case -1 : strerr_diefu1sys(111, "read from selfpipe") ;
+      case 0 : return ;
+      case SIGTERM : cont = 0 ; break ;
+      case SIGHUP :
+      {
+        switch (load_cachelist(0))
+        {
+          case 0 : query_process_reload() ; break ;
+          case -1 : strerr_warnwu2sys("read ", cachelistfile) ; break ;
+          case -2 : strerr_warnw2x("invalid cache list in ", cachelistfile) ; break ;
+          default : X() ;
+        }
+        break ;
+      }
+      default : X() ;
+    }
+  }
+}
+
+static dfquery_t const dfquery_zero = DFQUERY_ZERO ;
+static gensetdyn queries = GENSETDYN_INIT(dfquery_t, 16, 3, 8) ;
+static uint32_t sentinel ;
+#define inflight (gensetdyn_n(&queries) - 1)
+#define QUERY(i) GENSETDYN_P(dfquery_t, &queries, i)
+
+void query_new (s6dns_domain_t const *d, uint16_t qtype, uint16_t id, uint32_t ip, uint16_t port, uint32_t procid)
+{
+  dfquery_t q =
+  {
+    .next = QUERY(sentinel)->next,
+    .xindex = 0,
+    .procid = procid,
+    .ip = ip,
+    .port = port,
+    .id = id,
+    .dt = S6DNS_ENGINE_ZERO
+  } ;
+  tain_t deadline ;
+  uint32_t i ;
+  if (!gensetdyn_new(&queries, &i))
+    strerr_diefu1sys(111, "create new query") ;
+  tain_add_g(&deadline, &globaltto) ;
+  if (!s6dns_engine_init_g(&q.dt, &cachelist, S6DNS_O_RECURSIVE, d->s, d->len, qtype, &deadline))
+    strerr_diefu1sys(111, "start new query") ;
+  *QUERY(i) = q ;
+  QUERY(sentinel)->next = i ;
+}
+
+static inline void sanitize_and_new (char const *buf, unsigned int len, char const *ippack, uint16_t port)
+{
+  s6dns_domain_t d ;
+  uint32_t ip ;
+  unsigned int pos ;
+  s6dns_message_header_t hdr ;
+  s6dns_message_counts_t counts ;
+  uint16_t qtype ;
+  if (!s6dns_message_parse_init(&hdr, &counts, buf, len, &pos)
+   || hdr.qr
+   || hdr.opcode
+   || !hdr.rd
+   || hdr.counts.qd != 1 || hdr.counts.an || hdr.counts.ns || hdr.counts.nr
+   || !s6dns_message_parse_question(&counts, &d, &qtype, buf, len, &pos))
+    return ;
+  uint32_unpack_big(ippack, &ip) ;
+  if (ops) query_process_question(ops, &d, qtype, hdr.id, ip, port) ;
+  else query_new(&d, qtype, hdr.id, ip, port, 0) ;
+}
+
+int main (int argc, char const *const *argv)
+{
+  int spfd = -1 ;
+  int notif = -1 ;
+  PROG = "dnsfunneld" ;
+  {
+    subgetopt_t l = SUBGETOPT_ZERO ;
+    for (;;)
+    {
+      int opt = subgetopt_r(argc, argv, "v:d:o:", &l) ;
+      if (opt == -1) break ;
+      switch (opt)
+      {
+        case 'v' : if (!uint0_scan(l.arg, &verbosity)) dieusage() ; break ;
+        case 'd' : if (!uint0_scan(l.arg, (unsigned int *)&notif)) dieusage() ; break ;
+        case 'o' : if (!uint320_scan(l.arg, &ops)) dieusage() ; break ;
+        default : dieusage() ;
+      }
+    }
+    argc -= l.ind ; argv += l.ind ;
+    if (!argc) dieusage() ;
+  }
+  if (notif >= 0)
+  {
+    if (notif < 3) strerr_dief1x(100, "notification fd must be 3 or more") ;
+    if (fcntl(notif, F_GETFD) < 0) strerr_dief1sys(100, "invalid notification fd") ;
+  }
+
+  if (ndelay_on(0) < 0) strerr_diefu1sys(111, "turn stdin non-blocking") ;
+  if (sig_ignore(SIGPIPE) < 0) strerr_diefu1sys(111, "ignore SIGPIPE") ;
+  cachelistfile = argv[0] ;
+  switch (load_cachelist(1))
+  {
+    case 0 : break ;
+    case -1 : strerr_diefu2sys(111, "read ", cachelistfile) ;
+    case -2 : strerr_dief2x(100, "invalid cache list in ", cachelistfile) ;
+    default : X() ;
+  }
+  if (!s6dns_init()) strerr_diefu1sys(111, "s6dns_init") ;
+  spfd = selfpipe_init() ;
+  if (spfd < 0) strerr_diefu1sys(111, "init selfpipe") ;
+  {
+    sigset_t set ;
+    sigemptyset(&set) ;
+    sigaddset(&set, SIGTERM) ;
+    sigaddset(&set, SIGHUP) ;
+    if (selfpipe_trapset(&set) < 0) strerr_diefu1sys(111, "trap signals") ;
+  }
+  if (!gensetdyn_new(&queries, &sentinel))
+    strerr_diefu1sys(111, "initialize query structure") ;
+  *QUERY(sentinel) = dfquery_zero ;
+  QUERY(sentinel)->next = sentinel ;
+  if (!query_process_init())
+    strerr_diefu1sys(111, "initialize query processing") ;
+  tain_now_set_stopwatch_g() ;
+
+  if (notif >= 0)
+  {
+    fd_write(notif, "\n", 1) ;
+    fd_close(notif) ;
+  }
+                  
+  for (;;)                
+  {
+    tain_t deadline = TAIN_INFINITE ;
+    uint32_t i = QUERY(sentinel)->next ;
+    uint32_t j = 2 ;
+    int r ;
+    iopause_fd x[2 + inflight] ;
+  
+    x[0].fd = spfd ;
+    x[0].events = IOPAUSE_READ ;
+    x[1].fd = 0 ;
+    x[1].events = (cont ? IOPAUSE_READ : 0) | (dfanswer_pending() ? IOPAUSE_WRITE : 0) ;
+    if (!x[1].events && !inflight) break ;
+
+    while (i != sentinel)
+    {
+      dfquery_t *q = QUERY(i) ;
+      s6dns_engine_nextdeadline(&q->dt, &deadline) ;
+      x[j].fd = q->dt.fd ;
+      x[j].events = 0 ;
+      if (s6dns_engine_isreadable(&q->dt)) x[j].events |= IOPAUSE_READ ;
+      if (s6dns_engine_iswritable(&q->dt)) x[j].events |= IOPAUSE_WRITE ;
+      q->xindex = j++ ;
+      i = q->next ;
+    }
+
+    r = iopause_g(x, j, &deadline) ;
+    if (r < 0) strerr_diefu1sys(111, "iopause") ;
+
+    if (!r) 
+    {
+      i = QUERY(sentinel)->next ;
+      j = sentinel ;
+      while (i != sentinel)
+      {
+        dfquery_t *q = QUERY(i) ;
+        uint32_t k = q->next ;
+        if (s6dns_engine_timeout_g(&q->dt))
+        {
+          query_process_response_failure(ops, q) ;
+          QUERY(j)->next = k ;
+          stralloc_free(&q->dt.sa) ;
+          gensetdyn_delete(&queries, i) ;
+        }
+        else j = i ;
+        i = k ;
+      }
+      continue ;
+    }
+
+    if (x[0].revents & IOPAUSE_READ) handle_signals() ;
+
+    if (x[1].revents & IOPAUSE_WRITE)
+    {
+      int r = dfanswer_flush() ;
+      if (r < 0) strerr_diefu1sys(111, "send DNS answer to client") ;
+    }
+                        
+    i = QUERY(sentinel)->next ;
+    j = sentinel ;
+    while (i != sentinel)
+    {
+      dfquery_t *q = QUERY(i) ;
+      uint32_t k = q->next ;
+      int r = s6dns_engine_event_g(&q->dt) ;
+      if (r)
+      {
+        if (r > 0) query_process_response_success(ops, q) ;
+        else query_process_response_failure(ops, q) ;
+        QUERY(j)->next = k ;
+        if (r > 0) s6dns_engine_free(&q->dt) ;
+        else stralloc_free(&q->dt.sa) ;
+        gensetdyn_delete(&queries, i) ;
+      }
+      else j = i ;
+      i = k ;
+    }
+
+    if (x[0].revents & IOPAUSE_READ)
+    {
+      uint32_t n = DNSFUNNELD_INPUT_MAX ;
+      while (n--)
+      {
+        char ip[4] ;
+        uint16_t port ;
+        char buf[512] ;
+        ssize_t r = socket_recv4(0, buf, 512, ip, &port) ;
+        if (r < 0)
+          if (error_isagain(errno)) break ;
+          else strerr_diefu1sys(111, "socket_recv") ;
+        else if (!r) continue ;
+        else sanitize_and_new(buf, r, ip, port) ;
+      }
+    }
+  }
+  return 0 ;
+}
diff --git a/src/dnsfunnel/dnsfunneld.h b/src/dnsfunnel/dnsfunneld.h
new file mode 100644
index 0000000..9fc0bbf
--- /dev/null
+++ b/src/dnsfunnel/dnsfunneld.h
@@ -0,0 +1,44 @@
+/* ISC license. */
+
+#ifndef DNSFUNNELD_H
+#define DNSFUNNELD_H
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include <skalibs/gensetdyn.h>
+
+#include <s6-dns/s6dns-domain.h>
+#include <s6-dns/s6dns-engine.h>
+
+typedef struct dfquery_s dfquery_t, *dfquery_t_ref ;
+struct dfquery_s
+{
+  uint32_t next ;
+  uint32_t xindex ;
+  uint32_t procid ;
+  uint32_t ip ;
+  uint16_t port ;
+  uint16_t id ;
+  s6dns_engine_t dt ;
+} ;
+#define DFQUERY_ZERO { .next = 0, .xindex = 0, .procid = 0, .ip = 0, .port = 0, .id = 0, .dt = S6DNS_ENGINE_ZERO }
+
+extern unsigned int verbosity ;
+extern size_t dfanswer_pending (void) ;
+extern int dfanswer_flush (void) ;
+extern void dfanswer_fail (dfquery_t const *) ;
+extern void dfanswer_nxdomain (dfquery_t const *) ;
+extern void dfanswer_nodata (dfquery_t const *) ;
+extern void dfanswer_pass (dfquery_t const *, char *, unsigned int) ;
+
+
+extern void query_new (s6dns_domain_t const *, uint16_t, uint16_t, uint32_t, uint16_t, uint32_t) ;
+
+extern int query_process_init (void) ;
+extern void query_process_reload (void) ;
+extern void query_process_question (uint32_t, s6dns_domain_t const *, uint16_t, uint16_t, uint32_t, uint16_t) ;
+extern void query_process_response_failure (uint32_t, dfquery_t const *) ;
+extern void query_process_response_success (uint32_t, dfquery_t const *) ;
+
+#endif
diff --git a/src/dnsfunnel/dnsfunneld_answer.c b/src/dnsfunnel/dnsfunneld_answer.c
new file mode 100644
index 0000000..a6ee526
--- /dev/null
+++ b/src/dnsfunnel/dnsfunneld_answer.c
@@ -0,0 +1,132 @@
+/* ISC license. */
+
+#include <errno.h>
+#include <stdint.h>
+#include <string.h>
+
+#include <skalibs/uint16.h>
+#include <skalibs/uint32.h>
+#include <skalibs/error.h>
+#include <skalibs/strerr2.h>
+#include <skalibs/genqdyn.h>
+#include <skalibs/socket.h>
+
+#include <s6-dns/s6dns-message.h>
+
+#include "dnsfunneld.h"
+
+typedef struct dfanswer_s dfanswer_t, *dfanswer_t_ref ;
+struct dfanswer_s
+{
+  char buf[512] ;
+  char ip[4] ;
+  uint16_t len ;
+  uint16_t port ;
+} ;
+#define DFANSWER_ZERO { .buf = { 0 }, .ip = "\0\0\0", .len = 0, .port = 0 }
+
+static genqdyn dfanswers = GENQDYN_INIT(dfanswer_t, 1, 8) ;
+
+size_t dfanswer_pending ()
+{
+  return genqdyn_n(&dfanswers) ;
+}
+
+static void dfanswer_push (char const *s, size_t len, uint32_t ip, uint16_t port)
+{
+  if (len > 510)
+  {
+    if (verbosity)
+      strerr_warnw1x("answer too big, dropping - enable truncation to avoid this") ;
+  }
+  else
+  {
+    dfanswer_t ans = { .len = len, .port = port } ;
+    uint16_pack_big(ans.buf, ans.len) ;
+    memcpy(ans.buf, s+2, len) ;
+    uint32_pack_big(ans.ip, ip) ;
+    if (!genqdyn_push(&dfanswers, &ans))
+      strerr_diefu1sys(111, "queue answer to client") ;
+  }
+}
+
+int dfanswer_flush ()
+{
+  while (dfanswer_pending())
+  {
+    dfanswer_t *ans = GENQDYN_PEEK(dfanswer_t, &dfanswers) ;
+    if (socket_send4(0, ans->buf, ans->len, ans->ip, ans->port) < 0)
+      return error_isagain(errno) ? (errno = 0, 0) : -1 ;
+    genqdyn_pop(&dfanswers) ;
+  }
+  return 1 ;
+}
+
+void dfanswer_fail (dfquery_t const *q)
+{
+  char buf[510] ;
+  uint16_t len ;
+  s6dns_message_header_t hdr ;
+  uint16_unpack_big(q->dt.sa.s, &len) ;
+  memcpy(buf, q->dt.sa.s + 2, len) ;
+  s6dns_message_header_unpack(buf, &hdr) ;
+  hdr.id = q->id ;
+  hdr.qr = 1 ;
+  hdr.aa = 0 ;
+  hdr.tc = 0 ;
+  hdr.rd = 1 ;
+  hdr.ra = 1 ;
+  hdr.z = 0 ;
+  hdr.rcode = 2 ;  /* servfail */
+  s6dns_message_header_pack(buf, &hdr) ;
+  dfanswer_push(buf, len, q->ip, q->port) ;
+}
+
+void dfanswer_nxdomain (dfquery_t const *q)
+{
+  char buf[510] ;
+  uint16_t len ;
+  s6dns_message_header_t hdr ;
+  uint16_unpack_big(q->dt.sa.s, &len) ;
+  memcpy(buf, q->dt.sa.s + 2, len) ;
+  s6dns_message_header_unpack(buf, &hdr) ;
+  hdr.id = q->id ;
+  hdr.qr = 1 ;
+  hdr.aa = 1 ;
+  hdr.tc = 0 ;
+  hdr.rd = 1 ;
+  hdr.ra = 1 ;
+  hdr.z = 0 ;
+  hdr.rcode = 3 ;  /* nxdomain */
+  s6dns_message_header_pack(buf, &hdr) ;
+  dfanswer_push(buf, len, q->ip, q->port) ;
+}
+
+void dfanswer_nodata (dfquery_t const *q)
+{
+  char buf[510] ;
+  uint16_t len ;
+  s6dns_message_header_t hdr ;
+  uint16_unpack_big(q->dt.sa.s, &len) ;
+  memcpy(buf, q->dt.sa.s + 2, len) ;
+  s6dns_message_header_unpack(buf, &hdr) ;
+  hdr.id = q->id ;
+  hdr.qr = 1 ;
+  hdr.aa = 1 ;
+  hdr.tc = 0 ;
+  hdr.rd = 1 ;
+  hdr.ra = 1 ;
+  hdr.z = 0 ;
+  hdr.rcode = 0 ;  /* success */
+  s6dns_message_header_pack(buf, &hdr) ;
+  dfanswer_push(buf, len, q->ip, q->port) ;
+}
+
+void dfanswer_pass (dfquery_t const *q, char *s, unsigned int len)
+{
+  s6dns_message_header_t hdr ;
+  s6dns_message_header_unpack(s, &hdr) ;
+  hdr.id = q->id ;
+  s6dns_message_header_pack(s, &hdr) ;
+  dfanswer_push(s, len, q->ip, q->port) ;
+}
diff --git a/src/dnsfunnel/dnsfunneld_process.c b/src/dnsfunnel/dnsfunneld_process.c
new file mode 100644
index 0000000..9d90289
--- /dev/null
+++ b/src/dnsfunnel/dnsfunneld_process.c
@@ -0,0 +1,136 @@
+/* ISC license. */
+
+#include <stdint.h>
+
+#include <skalibs/uint16.h>
+#include <skalibs/strerr2.h>
+#include <skalibs/gensetdyn.h>
+
+#include <s6-dns/s6dns-constants.h>
+#include <s6-dns/s6dns-domain.h>
+#include <s6-dns/s6dns-message.h>
+#include <s6-dns/s6dns-engine.h>
+
+#include "dnsfunneld.h"
+
+static gensetdyn rinfo = GENSETDYN_INIT(uint8_t, 16, 3, 8) ;
+#define RINFO(i) GENSETDYN_P(uint8_t, &rinfo, i)
+
+int query_process_init ()
+{
+  return 1 ;
+}
+
+void query_process_reload ()
+{
+}
+
+void query_process_question (uint32_t ops, s6dns_domain_t const *d, uint16_t qtype, uint16_t id, uint32_t ip, uint16_t port)
+{
+  if (ops & 2 && (qtype == S6DNS_T_A || qtype == S6DNS_T_AAAA))
+  {
+    uint32_t i ;
+    if (!gensetdyn_new(&rinfo, &i)) strerr_diefu1sys(111, "process query") ;
+    *RINFO(i) = (qtype == S6DNS_T_AAAA) << 7 ;
+    query_new(d, S6DNS_T_A, id, ip, port, i+1) ;
+    query_new(d, S6DNS_T_AAAA, id, ip, port, i+1) ; 
+  }
+  else query_new(d, qtype, id, ip, port, 0) ;
+}
+
+static inline unsigned int truncate_packet (char *s, unsigned int olen)
+{
+  s6dns_message_header_t hdr ;
+  s6dns_message_counts_t counts ;
+  unsigned int section ;
+  unsigned int pos ;
+  if (!s6dns_message_parse_init(&hdr, &counts, s, olen, &pos)) return 0 ;
+  if (hdr.rcode) return 0 ;
+  section = s6dns_message_parse_skipqd(&counts, s, olen, &pos) ;
+  while (section)
+  {
+    s6dns_message_rr_t rr ;
+    s6dns_message_counts_t newcounts = counts ;
+    unsigned int tmp = pos ;
+    if (!s6dns_message_parse_getrr(&rr, s, olen, &tmp)) return 0 ;
+    section = s6dns_message_parse_next(&newcounts, &rr, s, olen, &tmp) ;
+    if (tmp > 510)
+    {
+      hdr.counts.qd -= counts.qd ;
+      hdr.counts.an -= counts.an ;
+      hdr.counts.ns -= counts.ns ;
+      hdr.counts.nr -= counts.nr ;
+      s6dns_message_header_pack(s, &hdr) ;
+      return pos ;
+    }
+    pos = tmp ;
+    counts = newcounts ;
+  }
+  return olen ;
+}
+
+static inline uint16_t extract_qtype (dfquery_t const *q)
+{
+  s6dns_domain_t name ;
+  uint16_t qtype ;
+  uint16_t len ;
+  s6dns_message_header_t hdr ;
+  s6dns_message_counts_t counts ;
+  unsigned int pos ;
+  uint16_unpack_big(q->dt.sa.s, &len) ;
+  if (!s6dns_message_parse_init(&hdr, &counts, q->dt.sa.s + 2, len, &pos)) return 0 ;
+  if (!s6dns_message_parse_question(&counts, &name, &qtype, q->dt.sa.s + 2, len, &pos)) return 0 ;
+  return qtype ;
+}
+
+static int isnxdomain (dfquery_t const *q)
+{
+  s6dns_message_header_t hdr ;
+  s6dns_message_counts_t counts ;
+  unsigned int pos ;
+  if (!s6dns_message_parse_init(&hdr, &counts, s6dns_engine_packet(&q->dt), s6dns_engine_packetlen(&q->dt), &pos)) return 0 ;
+  return hdr.rcode == 3 ;
+}
+
+static int input_event (dfquery_t const *q, unsigned int ev)
+{
+  static uint8_t const table[5][6] =
+  {
+    { 0x11, 0x03, 0x81, 0x02, 0x02, 0x04 },
+    { 0x06, 0x06, 0x06, 0x05, 0x05, 0x05 },
+    { 0x15, 0x25, 0x85, 0x06, 0x06, 0x06 },
+    { 0x06, 0x06, 0x06, 0x25, 0x25, 0x45 },
+    { 0x15, 0x45, 0x85, 0x06, 0x06, 0x06 }
+  } ;
+  uint8_t b = *RINFO(q->procid - 1) ;
+  uint8_t isaux = 3 * (b >> 7 != (extract_qtype(q) == S6DNS_T_AAAA)) ;
+  uint8_t state = (b >> isaux) & 7 ;
+  uint8_t c = table[state][ev + isaux] ;
+  state = c & 7 ;
+  *RINFO(q->procid - 1) = (b & ~(7 << isaux)) | (state << isaux) ;
+  if (c & 0x10) dfanswer_fail(q) ;
+  if (c & 0x20) dfanswer_nxdomain(q) ;
+  if (c & 0x40) dfanswer_nodata(q) ;
+  if (c & 0x80) dfanswer_pass(q, s6dns_engine_packet(&q->dt), s6dns_engine_packetlen(&q->dt)) ;
+  if (state >= 6) strerr_dief1x(101, "problem in main/aux transition table; please submit a bug-report.") ;
+  if (state == 5) gensetdyn_delete(&rinfo, q->procid - 1) ;
+  return !!(c & 0xf0) ;
+}
+
+void query_process_response_failure (uint32_t ops, dfquery_t const *q)
+{
+  if (ops & 2 && q->procid && input_event(q, 0)) return ;
+  else dfanswer_fail(q) ;
+}
+
+void query_process_response_success (uint32_t ops, dfquery_t const *q)
+{
+  if (ops & 2 && q->procid && input_event(q, 1 + !isnxdomain(q))) return ;
+  if (ops & 1 && s6dns_engine_packetlen(&q->dt) > 510)
+  {
+    unsigned int len = truncate_packet(s6dns_engine_packet(&q->dt), s6dns_engine_packetlen(&q->dt)) ;
+    if (!len) dfanswer_fail(q) ;
+    else dfanswer_pass(q, s6dns_engine_packet(&q->dt), len) ;
+  }
+  else dfanswer_pass(q, s6dns_engine_packet(&q->dt), s6dns_engine_packetlen(&q->dt)) ;
+}