chiark / gitweb /
Merge branches 'mdw/knock' and 'mdw/ipv6' into bleeding
authorMark Wooding <mdw@distorted.org.uk>
Fri, 25 Jan 2019 12:08:24 +0000 (12:08 +0000)
committerMark Wooding <mdw@distorted.org.uk>
Fri, 25 Jan 2019 12:08:24 +0000 (12:08 +0000)
* mdw/knock:
  Add notion of `ephemeral' associations and a goodbye protocol.
  Add new `knock' protocol.
  server/{keyexch,peer}.c: Maybe key-exchange messages come out of the blue.
  server/keyexch.c (kx_message): Squish vertically.
  server/keyexch.c: Abstract out the common message-handling behaviour.
  server/keymgmt.c: Track and find keys by their 32-bit IDs.
  server/test.c: Add a program to assist unit tests.
  server/servutil.c: Add utilities for plain asymmetric encryption.
  server/servutil.c: Add utilities for simple leaky-bucket rate limiting.
  server/keyexch.c: Rename kx_init => kx_setup.
  server/: Augment challenges to allow a payload.
  server/chal.c: Capture `master->algs.bulk' in a variable.
  server/chal.c: Rename bulk => bchal.
  server/: Expose and enhance the bulk-key-derivation protocol.

* mdw/ipv6: (64 commits)
  contrib/greet.in: Accept IPv6 addresses.
  contrib/tripe-ipif.in: Fixing for IPv6.
  svc/conntrack.in: Add IPv6 support.
  svc/conntrack.in: Split out a base class from `InetAddress'.
  svc/conntrack.in: Contemplate multiple address families.
  svc/conntrack.in: Allow multiple networks in a peer pattern.
  svc/conntrack.in (kickpeers): Refactor and reformat the search loop.
  svc/conntrack.in (kickpeers): Rename `map' variable.
  svc/conntrack.in: Process peer patterns in order.
  svc/conntrack.in: Maintain config groups in a dictionary.
  svc/conntrack.in: Make an `InetAddress' class to do address wrangling.
  svc/conntrack.in: Factor out network parsing.
  svc/conntrack.in: Gather address hacking functions into a new section.
  svc/conntrack.in: Introduce a function for parsing address strings.
  svc/conntrack.in (strmask): Consistently return a string object.
  svc/conntrack.in: Fix netmask parsing.
  svc/conntrack.in: Leave time for network configuration to settle.
  svc/conntrack.in: Hoist `netupdown' above `kickpeers'.
  server/, mon/: Introduce transport of TrIPE over IPv6.
  server/addrmap.c (hash): Visually tighten the arithmetic.
  ...

1  2 
mon/tripemon.in
peerdb/peers.in.5.in
py/tripe.py.in
server/Makefile.am
server/admin.c
server/peer.c
server/servutil.c
server/tripe-admin.5.in
server/tripe.c
server/tripe.h

diff --combined mon/tripemon.in
index 8666575a1fba5a6c5b182688341cd0638458b1b5,1c70d1ff0f446dbccf1787aaaf1e97f8aefbb58c..11ee6dc0b79aa410445647c450f712196c707afc
@@@ -323,13 -323,19 +323,19 @@@ class Peer (MonitorObject)
  
    def _setaddr(me, addr):
      """Set the peer's address."""
-     if addr[0] == 'INET':
-       ipaddr, port = addr[1:]
+     if addr[0] in ['INET', 'INET6']:
+       af, ipaddr, port = addr
        try:
-         name = S.gethostbyaddr(ipaddr)[0]
-         me.addr = 'INET %s:%s [%s]' % (name, port, ipaddr)
-       except S.herror:
-         me.addr = 'INET %s:%s' % (ipaddr, port)
+         name, _ = S.getnameinfo((ipaddr, int(port)),
+                                 S.NI_NUMERICSERV | S.NI_NAMEREQD)
+       except S.gaierror:
+         me.addr = '%s %s%s%s:%s' % (af,
+                                     af == 'INET6' and '[' or '',
+                                     ipaddr,
+                                     af == 'INET6' and ']' or '',
+                                     port)
+       else:
+         me.addr = '%s %s:%s [%s]' % (af, name, port, ipaddr)
      else:
        me.addr = ' '.join(addr)
  
@@@ -1042,6 -1048,8 +1048,8 @@@ class AddPeerDialog (MyDialog)
      * e_name, e_addr, e_port, c_keepalive, l_tunnel: widgets in the dialog
    """
  
+   AFS = ['ANY', 'INET', 'INET6']
    def __init__(me):
      """Initialize the dialogue."""
      MyDialog.__init__(me, 'Add peer',
      table = GridPacker()
      me.vbox.pack_start(table, True, True, 0)
      me.e_name = table.labelled('Name',
 -                               ValidatingEntry(r'^[^\s.:]+$', '', 16),
 +                               ValidatingEntry(r'^[^\s:]+$', '', 16),
                                 width = 3)
+     me.l_af = table.labelled('Family', combo_box_text(),
+                              newlinep = True, width = 3)
+     for af in me.AFS:
+       me.l_af.append_text(af)
+     me.l_af.set_active(0)
      me.e_addr = table.labelled('Address',
-                                ValidatingEntry(r'^[a-zA-Z0-9.-]+$', '', 24),
+                                ValidatingEntry(r'^[a-zA-Z0-9.-:]+$', '', 24),
                                 newlinep = True)
      me.e_port = table.labelled('Port',
                                 ValidatingEntry(numericvalidate(0, 65535),
      me.c_mobile = G.CheckButton('Mobile')
      table.pack(me.c_mobile, newlinep = True, width = 4, xopt = G.FILL)
  
 +    me.c_ephem = G.CheckButton('Ephemeral')
 +    table.pack(me.c_ephem, newlinep = True, width = 4, xopt = G.FILL)
 +
      me.c_peerkey, me.e_peerkey = \
        optional_entry('Peer key tag', r'^[^.:\s]+$', 16)
      me.c_privkey, me.e_privkey = \
        optional_entry('Private key tag', r'^[^.:\s]+$', 16)
  
 +    me.c_knock, me.e_knock = \
 +      optional_entry('Knock string', r'^[^:\s]+$', 16)
 +
      me.show_all()
  
    def ok(me):
      """Handle an OK press: create the peer."""
      try:
        t = me.l_tunnel.get_active()
+       afix = me.l_af.get_active()
        me._addpeer(me.e_name.get_text(),
+                   me.AFS[afix],
                    me.e_addr.get_text(),
                    me.e_port.get_text(),
                    keepalive = (me.c_keepalive.get_active() and
                    tunnel = t and me.tuns[t] or None,
                    cork = me.c_cork.get_active() or None,
                    mobile = me.c_mobile.get_active() or None,
 +                  ephemeral = me.c_ephem.get_active() or None,
                    key = (me.c_peerkey.get_active() and
                           me.e_peerkey.get_text() or None),
                    priv = (me.c_privkey.get_active() and
 -                          me.e_privkey.get_text() or None))
 +                          me.e_privkey.get_text() or None),
 +                  knock = (me.c_knock.get_active() and
 +                           me.e_knock.get_text() or None))
      except ValidationError:
        GDK.beep()
        return
diff --combined peerdb/peers.in.5.in
index ac27b49cabd5c819363e958d35ff2a037b7f28ce,554a8d196b987ad0b942f0207a6080824c699da7..d638b07821da4b04247558f924aba83c2368d159
@@@ -83,14 -83,33 +83,33 @@@ is replaced by the value assigned to th
  .IR key .
  .hP \*o
  An occurrence of
- .BI $[ host ]
+ .BI $ flags [ host ]
  is replaced by the IP address of the named
  .IR host .
  Note that
  .I host
  may itself contain
  .BI $( key )
- substitutions.
+ substitutions.  The
+ .I flags
+ consist of zero or more of the following characters:
+ .RB ` 4 '
+ looks up the
+ .IR host 's
+ IPv4 address(es);
+ .RB ` 6 '
+ looks up the
+ .IR host 's
+ IPv6 address(es);
+ and
+ .RB ` * '
+ returns all of the found addresses, separated by spaces, rather than
+ just the first one.  If neither address family is requested, then
+ .RB ` 46 '
+ is assumed.  IPv6 address lookup of names, rather than address literals,
+ depends on the external
+ .BR adnshost (1)
+ program; if it is not present then only IPv4 lookups will be performed.
  .PP
  There is a simple concept of
  .I inheritance
@@@ -175,12 -194,6 +194,12 @@@ Don't initiate immediate key exchange
  Shell command for closing down connection to this peer.  Used by
  .BR connect (8).
  .TP
 +.B ephemeral
 +Mark the peer as ephemeral: see
 +.BR tripe-admin (5)
 +for what this means.  Used by
 +.BR connect (8).
 +.TP
  .B every
  Interval for checking that the peer is still alive and well.  Used by
  .BR connect (8).
@@@ -213,10 -226,6 +232,10 @@@ Interval for sending keepalive pings.  
  Key tag to use to authenticate the peer.  Used by
  .BR connect (8).
  .TP
 +.B knock
 +Knock string to send when establishing a dynamic connection.  Used by
 +.BR connect (8).
 +.TP
  .B mobile
  Peer's IP address is highly volatile.  Used by
  .BR connect (8).
diff --combined py/tripe.py.in
index 0126dc586991c1357f71e0679b85c3c34cba0b72,29911b0ea5f1556ff83e04cbfbed1f41c0b7c332..a9be66872cfbbc0b1e4223272238db4cbef9719c
@@@ -838,8 -838,7 +838,8 @@@ class TripeCommandDispatcher (TripeConn
                                *['ADD'] +
                                _kwopts(kw, ['tunnel', 'keepalive',
                                             'key', 'priv', 'cork',
 -                                           'mobile']) +
 +                                           'mobile', 'knock',
 +                                           'ephemeral']) +
                                [peer] +
                                list(addr)))
    def addr(me, peer):
                                 *['PING'] +
                                 _kwopts(kw, ['timeout']) +
                                 [peer]))
-   def port(me):
-     return _oneline(me.command('PORT', filter = _tokenjoin))
+   def port(me, af = None):
+     return _oneline(me.command('PORT',
+                                *((af is not None) and [af] or []),
+                                filter = _tokenjoin))
    def quit(me):
      return _simple(me.command('QUIT'))
    def reload(me):
diff --combined server/Makefile.am
index 2a1ff28ac20d2caac630c843a4069a540d93e9c1,1e6d71bc6225e61e7f779bbefb0c71ab12710754..f569b4559bbf3c96f08e41a8ca705fcea757b1a2
  include $(top_srcdir)/vars.am
  
  sbin_PROGRAMS          =
 +noinst_PROGRAMS                =
  man_MANS               =
  
- LDADD                  = $(libtripe) $(libpriv) $(catacomb_LIBS)
+ LDADD                  = $(libtripe) $(libpriv) \
+                               $(catacomb_LIBS) $(ADNS_LIBS)
  
  ###--------------------------------------------------------------------------
  ### The main server.
@@@ -73,26 -73,4 +74,26 @@@ man_MANS            += tripe-service.7trip
  CLEANFILES            += tripe-service.7tripe
  EXTRA_DIST            += tripe-service.7.in
  
 +###--------------------------------------------------------------------------
 +### Unit-test program.
 +
 +noinst_PROGRAMS               += tripe-test
 +
 +tripe_test_SOURCES     = test.c
 +
 +tripe_test_SOURCES    += admin.c
 +tripe_test_SOURCES    += addrmap.c
 +tripe_test_SOURCES    += bulkcrypto.c
 +tripe_test_SOURCES    += chal.c
 +tripe_test_SOURCES    += dh.c
 +tripe_test_SOURCES    += keyexch.c
 +tripe_test_SOURCES    += keymgmt.c
 +tripe_test_SOURCES    += keyset.c
 +tripe_test_SOURCES    += peer.c
 +tripe_test_SOURCES    += privsep.c
 +tripe_test_SOURCES    += servutil.c
 +
 +tripe_test_SOURCES    += tun-std.c
 +tripe_test_SOURCES    += tun-slip.c
 +
  ###----- That's all, folks --------------------------------------------------
diff --combined server/admin.c
index 54883afa448cf15926c78c5e5d0c8748ea0276d1,8a3e62ca77f26d4b82fab6b7e2e5d2864694fb99..87bb90530653529c39902db80e770584d8fef373
@@@ -61,6 -61,10 +61,10 @@@ static const trace_opt w_opts[] = 
  
  /*----- Static variables --------------------------------------------------*/
  
+ #ifdef HAVE_LIBADNS
+   static adns_state ads;
+   sel_hook hook;
+ #endif
  static admin *admins;
  static admin *a_dead;
  static sel_file sock;
@@@ -272,14 -276,19 +276,19 @@@ void a_vformat(dstr *d, const char *fmt
      } else if (*fmt == '?') {
        if (strcmp(fmt, "?ADDR") == 0) {
        const addr *a = va_arg(*ap, const addr *);
-       switch (a->sa.sa_family) {
-         case AF_INET:
-           u_quotify(d, "INET");
-           u_quotify(d, inet_ntoa(a->sin.sin_addr));
-           dstr_putf(d, " %u", (unsigned)ntohs(a->sin.sin_port));
-           break;
-         default:
-           abort();
+       char name[NI_MAXHOST], serv[NI_MAXSERV];
+       int ix, err;
+       if ((err = getnameinfo(&a->sa, addrsz(a),
+                              name, sizeof(name), serv, sizeof(serv),
+                              (NI_NUMERICHOST | NI_NUMERICSERV |
+                               NI_DGRAM)))) {
+         dstr_putf(d, " E%d", err);
+         u_quotify(d, gai_strerror(err));
+       } else {
+         ix = afix(a->sa.sa_family); assert(ix >= 0);
+         u_quotify(d, aftab[ix].name);
+         u_quotify(d, name);
+         u_quotify(d, serv);
        }
        } else if (strcmp(fmt, "?B64") == 0) {
        const octet *p = va_arg(*ap, const octet *);
@@@ -551,7 -560,7 +560,7 @@@ void a_quit(void
  {
    close(sock.fd);
    unlink(sockname);
 -  FOREACH_PEER(p, { p_destroy(p); });
 +  FOREACH_PEER(p, { p_destroy(p, 1); });
    ps_quit();
    exit(0);
  }
@@@ -1006,6 -1015,101 +1015,101 @@@ static void a_svcrelease(admin_service 
  
  /*----- Name resolution operations ----------------------------------------*/
  
+ #ifdef HAVE_LIBADNS
+ /* --- @before_select@ --- *
+  *
+  * Arguments: @sel_state *s@ = the @sel@ multiplexor (unused)
+  *            @sel_args *a@ = input to @select@, to be updated
+  *            @void *p@ = a context pointer (unused)
+  *
+  * Returns:   ---
+  *
+  * Use:               An I/O multiplexor hook, called just before waiting for I/O
+  *            events.
+  *
+  *            Currently its main purpose is to wire ADNS into the event
+  *            loop.
+  */
+ static void before_select(sel_state *s, sel_args *a, void *p)
+ {
+   struct timeval now;
+   adns_query q;
+   adns_answer *n;
+   admin_resop *r;
+   int any = 0;
+   /* --- Check for name-resolution progress --- *
+    *
+    * If there is any, then clobber the timeout: one of the resolver
+    * callbacks might have renewed its interest in a file descriptor, but too
+    * late to affect this @select@ call.
+    *
+    * I think, strictly, this is an mLib bug, but it's cheap enough to hack
+    * around here.  Fixing it will wait for mLib 3.
+    */
+   for (;;) {
+     q = 0;
+     if (adns_check(ads, &q, &n, &p)) break;
+     r = p;
+     any = 1;
+     if (n->status != adns_s_ok) {
+       T( trace(T_ADMIN, "admin: resop %s failed: %s",
+              BGTAG(r), adns_strerror(n->status)); )
+       a_bgfail(&r->bg, "resolve-error", "%s", r->addr, A_END);
+       r->func(r, ARES_FAIL);
+     } else {
+       T( trace(T_ADMIN, "admin: resop %s ok", BGTAG(r)); )
+       assert(n->type == adns_r_addr);
+       assert(n->nrrs > 0);
+       assert(n->rrs.addr[0].len <= sizeof(r->sa));
+       memcpy(&r->sa, &n->rrs.addr[0].addr, n->rrs.addr[0].len);
+       setport(&r->sa, r->port);
+       r->func(r, ARES_OK);
+     }
+     free(n);
+     sel_rmtimer(&r->t);
+     xfree(r->addr);
+     a_bgrelease(&r->bg);
+   }
+   if (any) { a->tvp = &a->tv; a->tv.tv_sec = 0; a->tv.tv_usec = 0; }
+   gettimeofday(&now, 0);
+   adns_beforeselect(ads, &a->maxfd,
+                   &a->fd[SEL_READ], &a->fd[SEL_WRITE], &a->fd[SEL_EXC],
+                   &a->tvp, &a->tv, &now);
+ }
+ /* --- @after_select@ --- *
+  *
+  * Arguments: @sel_state *s@ = the @sel@ multiplexor (unused)
+  *            @sel_args *a@ = input to @select@, to be updated
+  *            @void *p@ = a context pointer (unused)
+  *
+  * Returns:   ---
+  *
+  * Use:               An I/O multiplexor hook, called just after waiting for I/O
+  *            events.
+  *
+  *            Currently its main purpose is to wire ADNS into the event
+  *            loop.
+  */
+ static void after_select(sel_state *s, sel_args *a, void *p)
+ {
+   struct timeval now;
+   gettimeofday(&now, 0);
+   adns_afterselect(ads, a->maxfd,
+                  &a->fd[SEL_READ], &a->fd[SEL_WRITE], &a->fd[SEL_EXC],
+                  &now);
+ }
+ #else
  /* --- @a_resolved@ --- *
   *
   * Arguments: @struct hostent *h@ = pointer to resolved hostname
@@@ -1020,13 -1124,17 +1124,17 @@@ static void a_resolved(struct hostent *
  {
    admin_resop *r = v;
  
-   T( trace(T_ADMIN, "admin: resop %s resolved", BGTAG(r)); )
    QUICKRAND;
    if (!h) {
+     T( trace(T_ADMIN, "admin: resop %s failed: %s",
+            BGTAG(r), hstrerror(h_errno)); )
      a_bgfail(&r->bg, "resolve-error", "%s", r->addr, A_END);
      r->func(r, ARES_FAIL);
    } else {
+     T( trace(T_ADMIN, "admin: resop %s ok", BGTAG(r)); )
+     r->sa.sin.sin_family = AF_INET;
      memcpy(&r->sa.sin.sin_addr, h->h_addr, sizeof(struct in_addr));
+     setport(&r->sa, r->port);
      r->func(r, ARES_OK);
    }
    sel_rmtimer(&r->t);
    a_bgrelease(&r->bg);
  }
  
+ #endif
  /* --- @a_restimer@ --- *
   *
   * Arguments: @struct timeval *tv@ = timer
@@@ -1051,7 -1161,11 +1161,11 @@@ static void a_restimer(struct timeval *
    T( trace(T_ADMIN, "admin: resop %s timeout", BGTAG(r)); )
    a_bgfail(&r->bg, "resolver-timeout", "%s", r->addr, A_END);
    r->func(r, ARES_FAIL);
+ #ifdef HAVE_LIBADNS
+   adns_cancel(r->q);
+ #else
    bres_abort(&r->r);
+ #endif
    xfree(r->addr);
    a_bgrelease(&r->bg);
  }
@@@ -1073,7 -1187,11 +1187,11 @@@ static void a_rescancel(admin_bgop *bg
    r->func(r, ARES_FAIL);
    sel_rmtimer(&r->t);
    xfree(r->addr);
+ #ifdef HAVE_LIBADNS
+   adns_cancel(r->q);
+ #else
    bres_abort(&r->r);
+ #endif
  }
  
  /* --- @a_resolve@ --- *
@@@ -1096,20 -1214,39 +1214,39 @@@ static void a_resolve(admin *a, admin_r
  {
    struct timeval tv;
    unsigned long pt;
+   int af = AF_UNSPEC;
+   const char *fam = "ANY";
    char *p;
-   int i = 0;
+   int i = 0, j;
+   struct addrinfo *ai, *ailist, aihint = { 0 };
+ #ifdef HAVE_LIBADNS
+   int err;
+   adns_queryflags qf;
+ #endif
  
    /* --- Fill in the easy bits of address --- */
  
    r->bg.tag = "<starting>";
    r->addr = 0;
    r->func = func;
-   if (mystrieq(av[i], "inet")) i++;
+   if (mystrieq(av[i], "any"))
+     { fam = "ANY"; af = AF_UNSPEC; i++; }
+   else for (j = 0; j < NADDRFAM; j++) {
+     if (mystrieq(av[i], aftab[j].name)) {
+       if (udpsock[j].fd < 0) {
+       a_fail(a, "disabled-address-family", "%s", aftab[j].name, A_END);
+       goto fail;
+       }
+       fam = aftab[j].name;
+       af = aftab[j].af;
+       i++;
+       break;
+     }
+   }
    if (ac - i != 1 && ac - i != 2) {
-     a_fail(a, "bad-addr-syntax", "[inet] ADDRESS [PORT]", A_END);
+     a_fail(a, "bad-addr-syntax", "[FAMILY] ADDRESS [PORT]", A_END);
      goto fail;
    }
-   r->sa.sin.sin_family = AF_INET;
    r->addr = xstrdup(av[i]);
    if (!av[i + 1])
      pt = TRIPE_PORT;
      a_fail(a, "invalid-port", "%lu", pt, A_END);
      goto fail;
    }
-   r->sa.sin.sin_port = htons(pt);
+   r->port = pt;
  
    /* --- Report backgrounding --- *
     *
  
    if (a_bgadd(a, &r->bg, tag, a_rescancel))
      goto fail;
-   T( trace(T_ADMIN, "admin: %u, resop %s, hostname `%s'",
-          a->seq, BGTAG(r), r->addr); )
+   T( trace(T_ADMIN, "admin: %u, resop %s, hostname `%s', family `%s'",
+          a->seq, BGTAG(r), r->addr, fam); )
  
    /* --- If the name is numeric, do it the easy way --- */
  
-   if (inet_aton(av[i], &r->sa.sin.sin_addr)) {
-     T( trace(T_ADMIN, "admin: resop %s done the easy way", BGTAG(r)); )
-     func(r, ARES_OK);
+   aihint.ai_family = af;
+   aihint.ai_socktype = SOCK_DGRAM;
+   aihint.ai_protocol = IPPROTO_UDP;
+   aihint.ai_flags = AI_NUMERICHOST;
+   if (!getaddrinfo(av[i], 0, &aihint, &ailist)) {
+     for (ai = ailist; ai; ai = ai->ai_next) {
+       if ((j = afix(ai->ai_family)) >= 0 && udpsock[j].fd >= 0)
+       break;
+     }
+     if (!ai) {
+       T( trace(T_ADMIN, "admin: resop %s failed: "
+              "no suitable addresses returned", BGTAG(r)); )
+       a_bgfail(&r->bg, "resolve-error", "%s" , r->addr, A_END);
+       func(r, ARES_FAIL);
+     } else {
+       T( trace(T_ADMIN, "admin: resop %s done the easy way", BGTAG(r)); )
+       assert(ai->ai_addrlen <= sizeof(r->sa));
+       memcpy(&r->sa, ai->ai_addr, ai->ai_addrlen);
+       setport(&r->sa, r->port);
+       func(r, ARES_OK);
+     }
+     freeaddrinfo(ailist);
      xfree(r->addr);
      a_bgrelease(&r->bg);
      return;
    gettimeofday(&tv, 0);
    tv.tv_sec += T_RESOLVE;
    sel_addtimer(&sel, &r->t, &tv, a_restimer, r);
+ #ifdef HAVE_LIBADNS
+   qf = adns_qf_search;
+   for (j = 0; j < NADDRFAM; j++) {
+     if ((af == AF_UNSPEC || af == aftab[i].af) && udpsock[j].fd >= 0)
+       qf |= aftab[j].qf;
+   }
+   if ((err = adns_submit(ads, r->addr, adns_r_addr, qf, r, &r->q)) != 0) {
+     T( trace(T_ADMIN, "admin: resop %s adns_submit failed: %s",
+            BGTAG(r), strerror(err)); )
+     a_bgfail(&r->bg, "resolve-error", "%s", r->addr, A_END);
+     goto fail_release;
+   }
+ #else
+   if (af != AF_UNSPEC && af != AF_INET) {
+     T( trace(T_ADMIN, "admin: resop %s failed: unsupported address family",
+            BGTAG(r)); )
+     a_bgfail(&r->bg, "resolve-error", "%s", r->addr, A_END);
+     goto fail_release;
+   }
+   if (udpsock[AFIX_INET].fd < 0) {
+     a_bgfail(&r->bg, "disabled-address-family", "INET", A_END);
+     goto fail_release;
+   }
    bres_byname(&r->r, r->addr, a_resolved, r);
+ #endif
    return;
  
  fail:
    func(r, ARES_FAIL);
    if (r->addr) xfree(r->addr);
    xfree(r);
+   return;
+ fail_release:
+   func(r, ARES_FAIL);
+   xfree(r->addr);
+   a_bgrelease(&r->bg);
  }
  
  /*----- Option parsing ----------------------------------------------------*/
@@@ -1235,7 -1421,6 +1421,7 @@@ static void a_doadd(admin_resop *r, in
  
    if (add->peer.tag) xfree(add->peer.tag);
    if (add->peer.privtag) xfree(add->peer.privtag);
 +  if (add->peer.knock) xfree(add->peer.knock);
    xfree(add->peer.name);
  }
  
@@@ -1261,7 -1446,6 +1447,7 @@@ static void acmd_add(admin *a, unsigne
    add->peer.name = 0;
    add->peer.tag = 0;
    add->peer.privtag = 0;
 +  add->peer.knock = 0;
    add->peer.t_ka = 0;
    add->peer.tops = tun_default;
    add->peer.f = 0;
      })
      OPTTIME("-keepalive", t, { add->peer.t_ka = t; })
      OPT("-cork", { add->peer.f |= KXF_CORK; })
 +    OPT("-ephemeral", { add->peer.f |= PSF_EPHEM; })
      OPTARG("-key", arg, {
        if (add->peer.tag) xfree(add->peer.tag);
        add->peer.tag = xstrdup(arg);
      })
 -    OPT("-mobile", { add->peer.f |= PSF_MOBILE; })
 +    OPT("-mobile", { add->peer.f |= PSF_MOBILE | PSF_EPHEM; })
      OPTARG("-priv", arg, {
        if (add->peer.privtag) xfree(add->peer.privtag);
        add->peer.privtag = xstrdup(arg);
      })
 +    OPTARG("-knock", arg, {
 +      if (add->peer.knock) xfree(add->peer.knock);
 +      add->peer.knock = xstrdup(arg);
 +      add->peer.f |= PSF_EPHEM;
 +    })
    });
  
    /* --- Make sure someone's not got there already --- */
@@@ -1326,7 -1504,6 +1512,7 @@@ fail
    if (add->peer.name) xfree(add->peer.name);
    if (add->peer.tag) xfree(add->peer.tag);
    if (add->peer.privtag) xfree(add->peer.privtag);
 +  if (add->peer.knock) xfree(add->peer.knock);
    xfree(add);
    return;
  }
@@@ -1675,7 -1852,27 +1861,27 @@@ static void acmd_warn(admin *a, unsigne
    { alertcmd(a, AF_WARN, AF_WARN, "WARN", av); }
  
  static void acmd_port(admin *a, unsigned ac, char *av[])
-   { a_info(a, "%u", p_port(), A_END); a_ok(a); }
+ {
+   int i;
+   if (ac) {
+     for (i = 0; i < NADDRFAM; i++)
+       if (mystrieq(av[0], aftab[i].name)) goto found;
+     a_fail(a, "unknown-address-family", "%s", av[0], A_END);
+     return;
+   found:
+     if (udpsock[i].fd < 0) {
+       a_fail(a, "disabled-address-family", "%s", aftab[i].name, A_END);
+       return;
+     }
+   } else {
+     for (i = 0; i < NADDRFAM; i++)
+       if (udpsock[i].fd >= 0) goto found;
+     abort();
+   }
+   a_info(a, "%u", p_port(i), A_END);
+   a_ok(a);
+ }
  
  static void acmd_daemon(admin *a, unsigned ac, char *av[])
  {
@@@ -1785,7 -1982,7 +1991,7 @@@ static void acmd_getchal(admin *a, unsi
    buf b;
  
    buf_init(&b, buf_i, PKBUFSZ);
 -  c_new(&b);
 +  c_new(0, 0, &b);
    a_info(a, "?B64", BBASE(&b), (size_t)BLEN(&b), A_END);
    a_ok(a);
  }
@@@ -1802,7 -1999,7 +2008,7 @@@ static void acmd_checkchal(admin *a, un
      a_fail(a, "bad-base64", "%s", codec_strerror(err), A_END);
    else {
      buf_init(&b, d.buf, d.len);
 -    if (c_check(&b) || BBAD(&b) || BLEFT(&b))
 +    if (c_check(0, 0, &b) || BBAD(&b) || BLEFT(&b))
        a_fail(a, "invalid-challenge", A_END);
      else
        a_ok(a);
@@@ -1838,7 -2035,6 +2044,6 @@@ static void acmd_addr(admin *a, unsigne
  
    if ((p = a_findpeer(a, av[0])) != 0) {
      ad = p_addr(p);
-     assert(ad->sa.sa_family == AF_INET);
      a_info(a, "?ADDR", ad, A_END);
      a_ok(a);
    }
@@@ -1853,7 -2049,6 +2058,7 @@@ static void acmd_peerinfo(admin *a, uns
    if ((p = a_findpeer(a, av[0])) != 0) {
      ps = p_spec(p);
      a_info(a, "tunnel=%s", ps->tops->name, A_END);
 +    if (ps->knock) a_info(a, "knock=%s", ps->knock, A_END);
      a_info(a, "key=%s", p_tag(p),
           "current-key=%s", p->kx.kpub->tag, A_END);
      if ((ptag = p_privtag(p)) == 0) ptag = "(default)";
      a_info(a, "keepalive=%lu", ps->t_ka, A_END);
      a_info(a, "corked=%s", BOOL(p->kx.f&KXF_CORK),
           "mobile=%s", BOOL(ps->f&PSF_MOBILE),
 +         "ephemeral=%s", BOOL(ps->f&PSF_EPHEM),
           A_END);
      a_ok(a);
    }
@@@ -1918,7 -2112,7 +2123,7 @@@ static void acmd_kill(admin *a, unsigne
    peer *p;
  
    if ((p = a_findpeer(a, av[0])) != 0) {
 -    p_destroy(p);
 +    p_destroy(p, 1);
      a_ok(a);
    }
  }
@@@ -1988,7 -2182,7 +2193,7 @@@ static const acmd acmdtab[] = 
    { "notify", "MESSAGE ...",          1,      0xffff, acmd_notify },
    { "peerinfo",       "PEER",                 1,      1,      acmd_peerinfo },
    { "ping",   "[OPTIONS] PEER",       1,      0xffff, acmd_ping },
-   { "port",   0,                      0,      0,      acmd_port },
+   { "port",   "[FAMILY]",             0,      1,      acmd_port },
    { "quit",   0,                      0,      0,      acmd_quit },
    { "reload", 0,                      0,      0,      acmd_reload },
    { "servinfo",       0,                      0,      0,      acmd_servinfo },
@@@ -2297,6 -2491,9 +2502,9 @@@ void a_init(const char *name, uid_t u, 
    struct sigaction sa;
    size_t sz;
    mode_t omask;
+ #ifdef HAVE_LIBADNS
+   int err;
+ #endif
  
    /* --- Create services table --- */
  
@@@ -2363,7 -2560,17 +2571,17 @@@ again
    sel_initfile(&sel, &sock, fd, SEL_READ, a_accept, 0);
    sel_addfile(&sock);
    sockname = name;
+ #ifdef HAVE_LIBADNS
+   if ((err = adns_init(&ads,
+                      (adns_if_permit_ipv4 | adns_if_permit_ipv6 |
+                       adns_if_noserverwarn | adns_if_nosigpipe |
+                       adns_if_noautosys),
+                      0)) != 0)
+     die(EXIT_FAILURE, "failed to initialize ADNS: %s", strerror(errno));
+   sel_addhook(&sel, &hook, before_select, after_select, 0);
+ #else
    bres_init(&sel);
+ #endif
    T( trace_custom(a_trace, 0);
       trace(T_ADMIN, "admin: enabled custom tracing"); )
    flags |= F_INIT;
diff --combined server/peer.c
index 606b2038d0c76f93a64af77d539566786de0fe7f,94629213ca4e37ea85e30bfc33fd00fba9265fff..a8099e41adb7776a026f283b10bf625d810eb65c
  
  #include "tripe.h"
  
+ /*----- Global state ------------------------------------------------------*/
+ sel_file udpsock[NADDRFAM];
  /*----- Static variables --------------------------------------------------*/
  
  static sym_table byname;
  static addrmap byaddr;
- static sel_file sock;
  static unsigned nmobile;
  
  /*----- Tunnel table ------------------------------------------------------*/
@@@ -181,6 -184,7 +184,7 @@@ int p_updateaddr(peer *p, const addr *a
  {
    peer *q;
    peer_byaddr *pa, *qa;
+   int ix;
    unsigned f;
  
    /* --- Figure out how to proceed --- *
      T( trace(T_PEER, "peer: updating address for `%s'", p_name(p)); )
      am_remove(&byaddr, p->byaddr);
      p->byaddr = pa; p->spec.sa = *a; pa->p = p;
+     p->afix = afix(p->spec.sa.sa.sa_family); assert(p->afix >= 0);
      a_notify("NEWADDR", "?PEER", p, "?ADDR", a, A_END);
      return (0);
    } else {
             p_name(p), p_name(q)); )
      q->byaddr = qa; qa->p = q; q->spec.sa = p->spec.sa;
      p->byaddr = pa; pa->p = p; p->spec.sa = *a;
+     ix = p->afix; p->afix = q->afix; q->afix = ix;
      a_notify("NEWADDR", "?PEER", p, "?ADDR", a, A_END);
      a_notify("NEWADDR", "?PEER", q, "?ADDR", &q->spec.sa, A_END);
      return (0);
@@@ -330,6 -336,10 +336,10 @@@ static void p_read(int fd, unsigned mod
    ssize_t n;
    int ch;
    buf b, bb;
+ #ifndef NTRACE
+   int ix = -1;
+   char name[NI_MAXHOST], svc[NI_MAXSERV];
+ #endif
  
    /* --- Read the data --- */
  
      a_warn("PEER", "-", "socket-read-error", "?ERRNO", A_END);
      return;
    }
+   IF_TRACING(T_PEER, {
+     ix = afix(a.sa.sa_family);
+     getnameinfo(&a.sa, sz, name, sizeof(name), svc, sizeof(svc),
+               NI_NUMERICHOST | NI_NUMERICSERV);
+   })
  
    /* --- If the packet is a greeting, don't check peers --- */
  
    if (n && buf_i[0] == (MSG_MISC | MISC_GREET)) {
      IF_TRACING(T_PEER, {
-       trace(T_PEER, "peer: greeting received from INET %s %u",
-           inet_ntoa(a.sin.sin_addr),
-           (unsigned)ntohs(a.sin.sin_port));
+       trace(T_PEER, "peer: greeting received from %s %s %s",
+           aftab[ix].name, name, svc);
        trace_block(T_PACKET, "peer: greeting contents", buf_i, n);
      })
      buf_init(&b, buf_i, n);
      buf_getbyte(&b);
 -    if (c_check(&b) || BLEFT(&b)) {
 +    if (c_check(0, 0, &b) || BLEFT(&b)) {
        a_warn("PEER", "-", "invalid-greeting", A_END);
        return;
      }
    IF_TRACING(T_PEER, {
      if (p) {
        trace(T_PEER,
-           "peer: packet received from `%s' from address INET %s %d",
-           p_name(p), inet_ntoa(a.sin.sin_addr), ntohs(a.sin.sin_port));
+           "peer: packet received from `%s' from address %s %s %s",
+           p_name(p), aftab[ix].name, name, svc);
      } else {
-       trace(T_PEER, "peer: packet received from unknown address INET %s %d",
-           inet_ntoa(a.sin.sin_addr), ntohs(a.sin.sin_port));
+       trace(T_PEER, "peer: packet received from unknown address %s %s %s",
+           aftab[ix].name, name, svc);
      }
      trace_block(T_PACKET, "peer: packet contents", buf_i, n);
    })
        }
        break;
      case MSG_KEYEXCH:
 -      if (!p) goto unexp;
 -      p_rxupdstats(p, n);
 -      kx_message(&p->kx, ch & MSG_TYPEMASK, &b);
 +      if (p) p_rxupdstats(p, n);
 +      if (kx_message(p ? &p->kx : 0, &a, ch & MSG_TYPEMASK, &b)) goto unexp;
        break;
      case MSG_MISC:
        switch (ch & MSG_TYPEMASK) {
            p_ponged(p, MISC_EPONG, &bb);
          }
          break;
 +      case MISC_BYE:
 +        buf_init(&bb, buf_t, sizeof(buf_t));
 +        if (p_decrypt(&p, &a, n, ch, &b, &bb)) return;
 +        if (!(p->spec.f&PSF_EPHEM)) return;
 +        if (BOK(&bb)) {
 +          buf_flip(&bb);
 +          if (BSZ(&bb)) return;
 +          p_destroy(p, 0);
 +        }
 +        break;
        }
        break;
      default:
@@@ -498,30 -503,6 +512,30 @@@ buf *p_txstart(peer *p, unsigned msg
    return (&p->b);
  }
  
 +/* --- @p_txaddr@ --- *
 + *
 + * Arguments: @const addr *a@ = recipient address
 + *            @const void *p@ = pointer to packet to send
 + *            @size_t sz@ = length of packet
 + *
 + * Returns:   Zero if successful, nonzero on error.
 + *
 + * Use:               Sends a packet to an address which (possibly) isn't a current
 + *            peer.
 + */
 +
 +int p_txaddr(const addr *a, const void *p, size_t sz)
 +{
 +  socklen_t sasz = addrsz(a);
 +
 +  IF_TRACING(T_PEER, trace_block(T_PACKET, "peer: sending packet", p, sz); )
 +  if (sendto(sock.fd, p, sz, 0, &a->sa, sasz) < 0) {
 +    a_warn("PEER", "?ADDR", a, "socket-write-error", "?ERRNO", A_END);
 +    return (-1);
 +  }
 +  return (0);
 +}
 +
  /* --- @p_txend@ --- *
   *
   * Arguments: @peer *p@ = pointer to peer block
@@@ -543,7 -524,7 +557,7 @@@ static int p_dotxend(peer *p
    }
    IF_TRACING(T_PEER, trace_block(T_PACKET, "peer: sending packet",
                                 BBASE(&p->b), BLEN(&p->b)); )
-   if (sendto(sock.fd, BBASE(&p->b), BLEN(&p->b),
+   if (sendto(udpsock[p->afix].fd, BBASE(&p->b), BLEN(&p->b),
             0, &p->spec.sa.sa, sasz) < 0) {
      a_warn("PEER", "?PEER", p, "socket-write-error", "?ERRNO", A_END);
      return (0);
@@@ -799,45 -780,68 +813,68 @@@ const addr *p_addr(peer *p) { return (&
  
  /* --- @p_init@ --- *
   *
-  * Arguments: @struct in_addr addr@ = address to bind to
-  *            @unsigned port@ = port number to listen to
+  * Arguments: @struct addrinfo *ailist@ = addresses to bind to
   *
   * Returns:   ---
   *
   * Use:               Initializes the peer system; creates the socket.
   */
  
- void p_init(struct in_addr addr, unsigned port)
+ void p_init(struct addrinfo *ailist)
  {
    int fd;
-   struct sockaddr_in sin;
    int len = PKBUFSZ;
+   int yes = 1;
+   int i;
+   struct addrinfo *ai;
+   unsigned port, lastport = 0;
+   addr a;
+   socklen_t sz;
  
-   /* --- Note on socket buffer sizes --- *
-    *
-    * For some bizarre reason, Linux 2.2 (at least) doubles the socket buffer
-    * sizes I pass to @setsockopt@.  I'm not putting special-case code here
-    * for Linux: BSD (at least TCPv2) does what I tell it rather than second-
-    * guessing me.
-    */
-   if ((fd = socket(PF_INET, SOCK_DGRAM, 0)) < 0)
-     die(EXIT_FAILURE, "socket creation failed: %s", strerror(errno));
-   BURN(sin);
-   sin.sin_family = AF_INET;
-   sin.sin_addr = addr;
-   sin.sin_port = htons(port);
-   if (bind(fd, (struct sockaddr *)&sin, sizeof(sin)))
-     die(EXIT_FAILURE, "bind failed: %s", strerror(errno));
-   if (setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &len, sizeof(len)) ||
-       setsockopt(fd, SOL_SOCKET, SO_SNDBUF, &len, sizeof(len))) {
-     die(EXIT_FAILURE, "failed to set socket buffer sizes: %s",
-       strerror(errno));
+   for (i = 0; i < NADDRFAM; i++) udpsock[i].fd = -1;
+   for (ai = ailist; ai; ai = ai->ai_next) {
+     if ((i = afix(ai->ai_family)) < 0) continue;
+     if (udpsock[i].fd != -1) continue;
+     /* --- Note on socket buffer sizes --- *
+      *
+      * For some bizarre reason, Linux 2.2 (at least) doubles the socket
+      * buffer sizes I pass to @setsockopt@.  I'm not putting special-case
+      * code here for Linux: BSD (at least TCPv2) does what I tell it rather
+      * than second-guessing me.
+      */
+     if ((fd = socket(ai->ai_family, SOCK_DGRAM, 0)) < 0)
+       die(EXIT_FAILURE, "socket creation failed: %s", strerror(errno));
+     if (i == AFIX_INET6 &&
+       setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &yes, sizeof(yes))) {
+       die(EXIT_FAILURE, "failed to set IPv6-only state: %s",
+         strerror(errno));
+     }
+     assert(ai->ai_addrlen <= sizeof(a));
+     memcpy(&a, ai->ai_addr, ai->ai_addrlen);
+     if ((port = getport(&a)) == 0 && lastport) setport(&a, lastport);
+     if (bind(fd, &a.sa, addrsz(&a)))
+       die(EXIT_FAILURE, "bind failed: %s", strerror(errno));
+     if (setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &len, sizeof(len)) ||
+       setsockopt(fd, SOL_SOCKET, SO_SNDBUF, &len, sizeof(len))) {
+       die(EXIT_FAILURE, "failed to set socket buffer sizes: %s",
+         strerror(errno));
+     }
+     fdflags(fd, O_NONBLOCK, O_NONBLOCK, FD_CLOEXEC, FD_CLOEXEC);
+     sel_initfile(&sel, &udpsock[i], fd, SEL_READ, p_read, 0);
+     sel_addfile(&udpsock[i]);
+     T( trace(T_PEER, "peer: created %s socket", aftab[i].name); )
+     if (!port) {
+       sz = sizeof(a);
+       if (getsockname(fd, &a.sa, &sz)) {
+       die(EXIT_FAILURE, "failed to read local socket address: %s",
+           strerror(errno));
+       }
+       lastport = getport(&a);
+     }
    }
-   fdflags(fd, O_NONBLOCK, O_NONBLOCK, FD_CLOEXEC, FD_CLOEXEC);
-   sel_initfile(&sel, &sock, fd, SEL_READ, p_read, 0);
-   sel_addfile(&sock);
-   T( trace(T_PEER, "peer: created socket"); )
  
    sym_create(&byname);
    am_create(&byaddr);
  
  /* --- @p_port@ --- *
   *
-  * Arguments: ---
+  * Arguments: @int i@ = address family index to retrieve
   *
   * Returns:   Port number used for socket.
   */
  
- unsigned p_port(void)
+ unsigned p_port(int i)
  {
    addr a;
    socklen_t sz = sizeof(addr);
  
-   if (getsockname(sock.fd, &a.sa, &sz))
+   if (getsockname(udpsock[i].fd, &a.sa, &sz))
      die(EXIT_FAILURE, "couldn't read port number: %s", strerror(errno));
-   assert(a.sa.sa_family == AF_INET);
-   return (ntohs(a.sin.sin_port));
+   return (getport(&a));
  }
  
  /* --- @p_keepalive@ --- *
@@@ -928,10 -931,10 +964,11 @@@ peer *p_create(peerspec *spec
    p->spec.name = (/*unconst*/ char *)SYM_NAME(p->byname);
    if (spec->tag) p->spec.tag = xstrdup(spec->tag);
    if (spec->privtag) p->spec.privtag = xstrdup(spec->privtag);
 +  if (spec->knock) p->spec.knock = xstrdup(spec->knock);
    p->ks = 0;
    p->pings = 0;
    p->ifname = 0;
+   p->afix = afix(p->spec.sa.sa.sa_family); assert(p->afix >= 0);
    memset(&p->st, 0, sizeof(stats));
    p->st.t_start = time(0);
    if (!(tops->flags & TUNF_PRIVOPEN))
    T( trace(T_TUNNEL, "peer: attached interface %s to peer `%s'",
           p->ifname, p_name(p)); )
    p_setkatimer(p);
 -  if (kx_init(&p->kx, p, &p->ks, p->spec.f & PSF_KXMASK))
 +  if (kx_setup(&p->kx, p, &p->ks, p->spec.f & PSF_KXMASK))
      goto tidy_4;
    a_notify("ADD",
           "?PEER", p,
@@@ -1054,28 -1057,17 +1091,28 @@@ peer *p_find(const char *name
  /* --- @p_destroy@ --- *
   *
   * Arguments: @peer *p@ = pointer to a peer
 + *            @int bye@ = say goodbye to the peer?
   *
   * Returns:   ---
   *
   * Use:               Destroys a peer.
   */
  
 -void p_destroy(peer *p)
 +void p_destroy(peer *p, int bye)
  {
    ping *pg, *ppg;
 +  buf *b, bb;
  
    T( trace(T_PEER, "peer: destroying peer `%s'", p->spec.name); )
 +
 +  if (bye && (p->spec.f&PSF_EPHEM)) {
 +    b = p_txstart(p, MSG_MISC | MISC_BYE);
 +    buf_init(&bb, buf_t, sizeof(buf_t));
 +    assert(BOK(&bb)); buf_flip(&bb);
 +    p_encrypt(p, MSG_MISC | MISC_BYE, &bb, b);
 +    p_txend(p);
 +  }
 +
    a_notify("KILL", "%s", p->spec.name, A_END);
    ksl_free(&p->ks);
    kx_free(&p->kx);
    if (p->ifname) xfree(p->ifname);
    if (p->spec.tag) xfree(p->spec.tag);
    if (p->spec.privtag) xfree(p->spec.privtag);
 +  if (p->spec.knock) xfree(p->spec.knock);
    p->t->ops->destroy(p->t);
    if (p->spec.t_ka) sel_rmtimer(&p->tka);
    for (pg = p->pings; pg; pg = ppg) {
diff --combined server/servutil.c
index 72b5b80e229961c136d526d3653a6ac3a73b1ff5,f95541a8af609216a7f55ebeea32f365d441ba18..703e448ec8dcea95cf9405ff5378e1aebe1e97d4
@@@ -82,180 -82,6 +82,180 @@@ int seq_check(seqwin *s, uint32 q, cons
    return (0);
  }
  
 +/*----- Rate limiting -----------------------------------------------------*/
 +
 +/* --- @ratelim_init@ --- *
 + *
 + * Arguments: @ratelim *r@ = rate-limiting state to fill in
 + *            @unsigned persec@ = credit to accumulate per second
 + *            @unsigned max@ = maximum credit to retain
 + *
 + * Returns:   ---
 + *
 + * Use:               Initialize a rate-limiting state.
 + */
 +
 +void ratelim_init(ratelim *r, unsigned persec, unsigned max)
 +{
 +  r->n = r->max = max;
 +  r->persec = persec;
 +  gettimeofday(&r->when, 0);
 +}
 +
 +/* --- @ratelim_withdraw@ --- *
 + *
 + * Arguments: @ratelim *r@ = rate-limiting state
 + *            @unsigned n@ = credit to withdraw
 + *
 + * Returns:   Zero if successful; @-1@ if there is unsufficient credit
 + *
 + * Use:               Updates the state with any accumulated credit.  Then, if
 + *            there there are more than @n@ credits available, withdraw @n@
 + *            and return successfully; otherwise, report failure.
 + */
 +
 +int ratelim_withdraw(ratelim *r, unsigned n)
 +{
 +  struct timeval now, delta;
 +  unsigned long d;
 +
 +  gettimeofday(&now, 0);
 +  TV_SUB(&delta, &now, &r->when);
 +  d = (unsigned long)r->persec*delta.tv_sec +
 +    (unsigned long)r->persec*delta.tv_usec/MILLION;
 +  if (d < r->max - r->n) r->n += d;
 +  else r->n = r->max;
 +  r->when = now;
 +
 +  if (n > r->n) return (-1);
 +  else { r->n -= n; return (0); }
 +}
 +
 +/*----- Crypto ------------------------------------------------------------*/
 +
 +/* --- @ies_encrypt@ --- *
 + *
 + * Arguments: @kdata *kpub@ = recipient's public key
 + *            @unsigned ty@ = message type octet
 + *            @buf *b@ = input message buffer
 + *            @buf *bb@ = output buffer for the ciphertext
 + *
 + * Returns:   On error, returns a @KSERR_...@ code or breaks the buffer;
 + *            on success, returns zero and the buffer is good.
 + *
 + * Use:               Encrypts a message for a recipient, given their public key.
 + *            This does not (by itself) provide forward secrecy or sender
 + *            authenticity.  The ciphertext is self-delimiting (unlike
 + *            @ks_encrypt@).
 + */
 +
 +int ies_encrypt(kdata *kpub, unsigned ty, buf *b, buf *bb)
 +{
 +  dhgrp *g = kpub->grp;
 +  dhsc *u = g->ops->randsc(g);
 +  dhge *U = g->ops->mul(g, u, 0), *Z = g->ops->mul(g, u, kpub->K);
 +  bulkalgs *algs = kpub->algs.bulk;
 +  octet *len;
 +  bulkctx *bulk;
 +  deriveargs a;
 +  size_t n;
 +  buf bk;
 +  int rc = 0;
 +
 +  IF_TRACING(T_CRYPTO, {
 +    trace(T_CRYPTO,
 +        "crypto: encrypting IES message (type 0x%02x) for recipient `%s'",
 +        ty, kpub->tag);
 +    trace_block(T_CRYPTO, "crypto: plaintext message", BCUR(b), BLEFT(b));
 +  })
 +
 +  a.hc = kpub->algs.h; a.what = "tripe:ecies-"; a.f = DF_OUT;
 +  buf_init(&bk, buf_u, sizeof(buf_u)); a.k = BBASE(&bk);
 +  g->ops->stge(g, &bk, U, DHFMT_HASH); a.x = a.y = BLEN(&bk);
 +  g->ops->stge(g, &bk, Z, DHFMT_HASH); a.z = BLEN(&bk);
 +  assert(BOK(&bk));
 +  T( trace_block(T_CRYPTO, "crypto: KEM clue", a.k, a.x);
 +     trace_block(T_CRYPTO, "crypto: shared secret", a.k + a.y, a.z - a.y); )
 +
 +  len = BCUR(bb); buf_get(bb, 2);
 +  bulk = algs->ops->genkeys(algs, &a);
 +  bulk->ops = algs->ops;
 +  g->ops->stge(g, bb, U, DHFMT_VAR); if (BBAD(bb)) goto end;
 +  rc = bulk->ops->encrypt(bulk, ty, b, bb, 0);
 +  if (rc || BBAD(bb)) goto end;
 +  n = BCUR(bb) - len - 2; assert(n <= MASK16); STORE16(len, n);
 +
 +end:
 +  bulk->ops->freectx(bulk);
 +  g->ops->freesc(g, u);
 +  g->ops->freege(g, U);
 +  g->ops->freege(g, Z);
 +  return (rc);
 +}
 +
 +/* --- @ies_decrypt@ --- *
 + *
 + * Arguments: @kdata *kpub@ = private key key
 + *            @unsigned ty@ = message type octet
 + *            @buf *b@ = input ciphertext buffer
 + *            @buf *bb@ = output buffer for the message
 + *
 + * Returns:   On error, returns a @KSERR_...@ code; on success, returns
 + *            zero and the buffer is good.
 + *
 + * Use:               Decrypts a message encrypted using @ies_encrypt@, given our
 + *            private key.
 + */
 +
 +int ies_decrypt(kdata *kpriv, unsigned ty, buf *b, buf *bb)
 +{
 +  dhgrp *g = kpriv->grp;
 +  bulkalgs *algs = kpriv->algs.bulk;
 +  bulkctx *bulk = 0;
 +  T( const octet *m; )
 +  dhge *U = 0, *Z = 0;
 +  deriveargs a;
 +  uint32 seq;
 +  buf bk, bc;
 +  int rc;
 +
 +  IF_TRACING(T_CRYPTO, {
 +    trace(T_CRYPTO,
 +        "crypto: decrypting IES message (type 0x%02x) to recipient `%s'",
 +        ty, kpriv->tag);
 +    trace_block(T_CRYPTO, "crypto: ciphertext message", BCUR(b), BLEFT(b));
 +  })
 +
 +  if (buf_getbuf16(b, &bc) ||
 +      (U = g->ops->ldge(g, &bc, DHFMT_VAR)) == 0 ||
 +      g->ops->checkge(g, U))
 +    { rc = KSERR_MALFORMED; goto end; }
 +  Z = g->ops->mul(g, kpriv->k, U);
 +
 +  a.hc = kpriv->algs.h; a.what = "tripe:ecies-"; a.f = DF_IN;
 +  buf_init(&bk, buf_u, sizeof(buf_u)); a.k = BBASE(&bk); a.x = 0;
 +  g->ops->stge(g, &bk, U, DHFMT_HASH); a.y = BLEN(&bk);
 +  g->ops->stge(g, &bk, Z, DHFMT_HASH); a.z = BLEN(&bk);
 +  T( trace_block(T_CRYPTO, "crypto: KEM clue", a.k + a.x, a.y - a.x);
 +     trace_block(T_CRYPTO, "crypto: shared secret", a.k + a.y, a.z - a.y); )
 +  assert(BOK(&bk));
 +
 +  bulk = algs->ops->genkeys(algs, &a);
 +  bulk->ops = algs->ops;
 +  T( m = BCUR(bb); )
 +  rc = bulk->ops->decrypt(bulk, ty, &bc, bb, &seq);
 +  if (rc) goto end;
 +  if (seq) { rc = KSERR_SEQ; goto end; }
 +  assert(BOK(bb));
 +  T( trace_block(T_CRYPTO, "crypto: decrypted message", m, BCUR(bb) - m); )
 +
 +end:
 +  if (bulk) bulk->ops->freectx(bulk);
 +  g->ops->freege(g, U);
 +  g->ops->freege(g, Z);
 +  return (rc);
 +}
 +
  /*----- Random odds and sods ----------------------------------------------*/
  
  /* --- @timestr@ --- *
@@@ -295,6 -121,34 +295,34 @@@ int mystrieq(const char *x, const char 
    }
  }
  
+ /*----- Address handling --------------------------------------------------*/
+ const struct addrfam aftab[] = {
+ #ifdef HAVE_LIBADNS
+ #  define DEF(af, qf) { AF_##af, #af, adns_qf_##qf },
+ #else
+ #  define DEF(af, qf) { AF_##af, #af },
+ #endif
+   ADDRFAM(DEF)
+ #undef DEF
+ };
+ /* --- @afix@ --- *
+  *
+  * Arguments: @int af@ = an address family code
+  *
+  * Returns:   The index of the address family's record in @aftab@, or @-1@.
+  */
+ int afix(int af)
+ {
+   int i;
+   for (i = 0; i < NADDRFAM; i++)
+     if (af == aftab[i].af) return (i);
+   return (-1);
+ }
  /* --- @addrsz@ --- *
   *
   * Arguments: @const addr *a@ = a network address
@@@ -306,6 -160,35 +334,35 @@@ socklen_t addrsz(const addr *a
  {
    switch (a->sa.sa_family) {
      case AF_INET: return (sizeof(a->sin));
+     case AF_INET6: return (sizeof(a->sin6));
+     default: abort();
+   }
+ }
+ /* --- @getport@, @setport@ --- *
+  *
+  * Arguments: @addr *a@ = a network address
+  *            @unsigned port@ = port number to set
+  *
+  * Returns:   ---
+  *
+  * Use:               Retrieves or sets the port number in an address structure.
+  */
+ unsigned getport(addr *a)
+ {
+   switch (a->sa.sa_family) {
+     case AF_INET: return (ntohs(a->sin.sin_port)); break;
+     case AF_INET6: return (ntohs(a->sin6.sin6_port)); break;
+     default: abort();
+   }
+ }
+ void setport(addr *a, unsigned port)
+ {
+   switch (a->sa.sa_family) {
+     case AF_INET: a->sin.sin_port = htons(port); break;
+     case AF_INET6: a->sin6.sin6_port = htons(port); break;
      default: abort();
    }
  }
diff --combined server/tripe-admin.5.in
index c81dc111b84e1d4677e8633da27e7f26596a89aa,81dd570d68ccb344bb109670ce33d8a52a8109b7..f066ae6dcd280a97e9e5347ee004fb3584d0df6f
@@@ -251,21 -251,50 +251,50 @@@ the meanings of the subsequent tokens d
  Address family tokens are not case-sensitive on input; on output, they
  are always in upper-case.
  .PP
- At present, only one address family is understood.
+ The following address families are recognized.
+ .TP
+ .BI "ANY " address " \fR[" port \fR]
+ An address and port number for any supported address family.  On output,
+ .B tripe
+ never uses this form.  On input, the
+ .I address
+ is examined: if it is a numeric address for some recognized address
+ family, then it is interpreted as such; otherwise it is looked up using
+ the DNS (in the background).  The background resolver's address-sorting
+ rules apply, and
+ .B tripe
+ simply takes the first address in the returned list which is of a
+ supported address family.  Symbolic port numbers are permitted; if
+ omitted, the default port 4070 is used.
  .TP
  .BI "INET " address " \fR[" port \fR]
  An Internet socket, naming an IPv4 address and UDP port.  On output, the
- address is always in numeric dotted-quad form, and the port is given as
- a plain number.  On input, DNS hostnames and symbolic port names are
- permitted; if omitted, the default port 4070 is used.  Name resolution
- does not block the main server, but will block the requesting client,
- unless the command is run in the background.
+ .I address
+ is always in numeric dotted-quad form, and the
+ .I port
+ is given as a plain decimal number.  On input, DNS hostnames and
+ symbolic port names are permitted; if omitted, the default port 4070 is
+ used.
+ .TP
+ .BI "INET6 " address " \fR[" port \fR]
+ An Internet socket, naming an IPv6 address and UDP port.  On output, the
+ .I address
+ is always in numeric hex-and-colons form, and the
+ .I port
+ is given as a plain decimal number.  On input, DNS hostnames and
+ symbolic port names may be permitted, depending on how
+ .B tripe
+ was compiled; if omitted, the default port 4070 is used.
  .PP
  If, on input, no recognized address family token is found, the following
  tokens are assumed to represent an
- .B INET
+ .B ANY
  address.  Addresses output by the server always have an address family
- token.
+ token, and do not use
+ .BR ANY .
+ .PP
+ Name resolution never blocks the main server, but will block the
+ requesting client, unless the command is run in the background.
  .SS "Key-value output"
  Some commands (e.g.,
  .B STATS
@@@ -332,21 -361,6 +361,21 @@@ Run the command in the background, usin
  Don't send an immediate challenge to the peer; instead, wait until it
  sends us something before responding.
  .TP
 +.B "\-ephemeral"
 +The association with the peer is not intended to persist indefinitely.
 +If a peer marked as ephemeral is killed, or the
 +.BR tripe (8)
 +daemon is shut down, send a
 +.B bye
 +packet to the peer so that it forgets about us; if a peer marked as
 +ephemeral sends us a
 +.B bye
 +packet then it is killed (but in this case no further
 +.B bye
 +packet is sent).  Peers not marked as ephemeral exhibit neither of these
 +behaviours; each peer must have the other marked as ephemeral for the
 +association to be fully torn down if either end kills the other.
 +.TP
  .BI "\-keepalive " time
  Send a no-op packet if we've not sent a packet to the peer in the last
  .I time
@@@ -368,26 -382,6 +397,26 @@@ Use the public ke
  to authenticate the peer.  The default is to use the key tagged
  .IR peer .
  .TP
 +.BI "\-knock \fR[" prefix .\fR] tag
 +Send the string
 +.RI [ prefix\fB. ] tag
 +in
 +.B token-rq
 +and
 +.B knock
 +messages to the peer during key-exchange.  The string as a whole should
 +name the local machine to the peer, and
 +.I tag
 +should name its public key.  When such messages are received from a
 +currently unknown peer,
 +.BR tripe (8)
 +emits a
 +.B KNOCK
 +notification stating the peer's (claimed) name and address.  The server
 +will already have verified that the sender is using the peer's private
 +key by this point.  This option implies
 +.BR \-ephemeral .
 +.TP
  .B "\-mobile"
  The peer is a mobile device, and is likely to change address rapidly.
  If a packet arrives from an unknown address, the server's usual response
@@@ -396,8 -390,7 +425,8 @@@ peers, however, it will attempt to decr
  and if one succeeds, the server will update its idea of the peer's
  address and emit an
  .B NEWADDR
 -notification.
 +notification.  This option implies
 +.BR \-ephemeral .
  .TP
  .BI "\-priv " tag
  Use the private key
@@@ -507,12 -500,16 +536,16 @@@ tunnel interface.  I
  is the MTU of the path to the peer, then the tunnel MTU should be
  .IP
  .I MTU
- \- 29 \-
+ \-
+ .I header-length
+ \- 9 \-
  .I bulk-overhead
  .PP
- allowing 20 bytes of IP header, 8 bytes of UDP header, a packet type
- octet, and the bulk-crypto transform overhead (which includes the
- sequence number).
+ allowing
+ .I header-length
+ = 20 (IPv4) or 40 (IPv6) bytes of IP header, 8 bytes of UDP header, a
+ packet type octet, and the bulk-crypto transform overhead (which
+ includes the sequence number).
  .RE
  .SP
  .BI "BGCANCEL " tag
@@@ -608,16 -605,6 +641,16 @@@ The tunnel driver used for this peer
  The keepalive interval, in seconds, or zero if no keepalives are to be
  sent.
  .TP
 +.B knock
 +If present, the string sent to the peer to set up the association; see
 +the
 +.B \-knock
 +option to
 +.BR ADD ,
 +and the
 +.B KNOCK
 +notification.
 +.TP
  .B key
  The (short) key tag being used for the peer, as passed to the
  .B ADD
  .B nil
  depending on whether or not (respectively) the peer is expected to
  change its address unpredictably.
 +.TP
 +.B ephemeral
 +Either
 +.B t
 +or
 +.B nil
 +depending on whether the association with the peer is expected to be
 +temporary or persistent (respectively).
  .RE
  .SP
  .BI "PING \fR[" options "\fR] " peer
@@@ -717,12 -696,18 +750,18 @@@ given, seconds are assumed
  .RE
  .SP
  .B "PORT"
+ .RI [ family ]
  Emits an
  .B INFO
  line containing just the number of the UDP port used by the
  .B tripe
- server.  If you've allowed your server to allocate a port dynamically,
- this is how to find out which one it chose.
+ server, for the given address
+ .I family
+ (or one chosen arbitrarily if omitted -- though
+ .B tripe
+ tries to use the same port number consistently so this is not a likely
+ problem in practice).  If you've allowed your server to allocate a port
+ dynamically, this is how to find out which one it chose.
  .SP
  .B "RELOAD"
  Instructs the server to recheck its keyring files.  The server checks
@@@ -1048,6 -1033,15 +1087,15 @@@ An unknown watch option was requested
  An error occurred during the attempt to become a daemon, as reported by
  .IR message .
  .SP
+ .BI "disabled-address-family " afam
+ (For
+ .B ADD
+ and
+ .BR PORT .)
+ The address family
+ .I afam
+ is supported, but was disabled using command-line arguments.
+ .SP
  .BI "invalid-port " number
  (For
  .BR ADD .)
@@@ -1133,6 -1127,13 +1181,13 @@@ is available, which does not meet the s
  .I tag
  is already the tag of an outstanding job.
  .SP
+ .BI "unknown-address-family " afam
+ (For
+ .BR PORT .)
+ The address family
+ .I afam
+ is unrecognized.
+ .SP
  .BI "unknown-command " token
  The command
  .I token
@@@ -1222,12 -1223,6 +1277,12 @@@ The pee
  .I peer
  has been killed.
  .SP
 +.BI "KNOCK " peer " " address
 +The currently unknown
 +.I peer
 +is attempting to connect from
 +.IR address .
 +.SP
  .BI "KXDONE " peer
  Key exchange with
  .I peer
@@@ -1444,11 -1439,6 +1499,11 @@@ A key name
  .I tag
  couldn't be found in the keyring.
  .SP
 +.BI "KEYMGMT " which "-keyring " file " unknown-key-id 0x" keyid
 +A key with the given
 +.I keyid
 +(in hex) was requested but not found.
 +.SP
  .BI "KEYMGMT " which "-keyring " file " line " line " " message
  The contents of the keyring file are invalid.  There may well be a bug
  in the
@@@ -1470,11 -1460,8 +1525,11 @@@ is one of the token
  .BR challenge ,
  .BR reply ,
  .BR switch-rq ,
 -or
  .BR switch-ok .
 +.BR token-rq ,
 +.BR token ,
 +or
 +.BR knock .
  .SP
  .BI "KX " peer " algorithms-mismatch local-private-key " privtag " peer-public-key " pubtag
  The algorithms specified in the peer's public key
@@@ -1589,10 -1576,6 +1644,10 @@@ An error occurred trying to read an inc
  An error occurred attempting to send a network packet.  We lost that
  one.
  .SP
 +.BI "PEER " address\fR... " socket-write-error " ecode " " message
 +An error occurred attempting to send a network packet.  We lost that
 +one.
 +.SP
  .BI "PEER " peer " unexpected-encrypted-ping 0x" id
  The peer sent an encrypted ping response whose id doesn't match any
  outstanding ping.  Maybe it was delayed for longer than the server was
diff --combined server/tripe.c
index d50757bc2baf2d1c2e1ae45ea7d895924ee08ef7,565c83dd2d16b6f12d0de7d0ee59e6bbb5e60ae0..b84388573d1e02dd4b038d210733543ddd9be806
@@@ -91,6 -91,8 +91,8 @@@ Options:\n
  -u, --usage           Display pointless usage message.\n\
      --tunnels         Display IP tunnel drivers and exit.\n\
  \n\
+ -4, --ipv4            Transport over IPv4 only.\n\
+ -6, --ipv6            Transport over IPv6 only.\n\
  -D, --daemon          Run in the background.\n\
  -F, --foreground      Quit when stdin reports end-of-file.\n\
  -d, --directory=DIR   Switch to directory DIR [default " CONFIGDIR "].\n\
@@@ -119,11 -121,11 +121,11 @@@ int main(int argc, char *argv[]
    int csockmode = 0600;
    const char *dir = CONFIGDIR;
    const char *p;
-   unsigned port = TRIPE_PORT;
-   struct in_addr baddr = { INADDR_ANY };
+   const char *bindhost = 0, *bindsvc = STR(TRIPE_PORT);
+   struct addrinfo aihint = { 0 }, *ailist;
    unsigned f = 0;
    int i;
-   int selerr = 0;
+   int err, selerr = 0;
    unsigned af;
    struct timeval tv;
    uid_t u = -1;
    if ((p = getenv("TRIPESOCK")) != 0)
      csock = p;
    tun_default = tunnels[0];
+   aihint.ai_family = AF_UNSPEC;
  
    for (;;) {
      static const struct option opts[] = {
        { "usage",      0,              0,      'u' },
        { "tunnels",    0,              0,      '0' },
  
+       { "ipv4",               0,              0,      '4' },
+       { "ipv6",               0,              0,      '6' },
        { "daemon",     0,              0,      'D' },
        { "foreground", 0,              0,      'F' },
        { "uid",                OPTF_ARGREQ,    0,      'U' },
        { 0,            0,              0,      0 }
      };
  
-     i = mdwopt(argc, argv, "hvuDFU:G:b:n:p:d:k:K:t:a:m:" T("T:"),
+     i = mdwopt(argc, argv, "hvu46DFU:G:b:n:p:d:k:K:t:a:m:" T("T:"),
               opts, 0, 0, 0);
      if (i < 0)
        break;
        usage(stdout);
        exit(0);
  
+       case '4':
+       aihint.ai_family = AF_INET;
+       break;
+       case '6':
+       aihint.ai_family = AF_INET6;
+       break;
        case 'D':
        f |= f_daemon;
        break;
        f |= f_foreground;
        break;
  
-       case 'b': {
-       struct hostent *h = gethostbyname(optarg);
-       if (!h)
-         die(EXIT_FAILURE, "unknown host name `%s'", optarg);
-       memcpy(&baddr, h->h_addr, sizeof(struct in_addr));
-       } break;
-       case 'p': {
-       char *p;
-       unsigned long i = strtoul(optarg, &p, 0);
-       if (*p) {
-         struct servent *s = getservbyname(optarg, "udp");
-         if (!s)
-           die(EXIT_FAILURE, "unknown service name `%s'", optarg);
-         i = ntohs(s->s_port);
-       }
-       if (i >= 65536)
-         die(EXIT_FAILURE, "bad port number %lu", i);
-       port = i;
-       } break;
+       case 'b':
+       bindhost = optarg;
+       break;
+       case 'p':
+       bindsvc = optarg;
+       break;
        case 'n': {
        int i;
        for (i = 0;; i++) {
    if (!(~f & (f_daemon | f_foreground)))
      die(EXIT_FAILURE, "foreground operation for a daemon is silly");
  
+   aihint.ai_protocol = IPPROTO_UDP;
+   aihint.ai_socktype = SOCK_DGRAM;
+   aihint.ai_flags = AI_PASSIVE | AI_ADDRCONFIG;
+   if ((err = getaddrinfo(bindhost, bindsvc, &aihint, &ailist)) != 0) {
+     die(EXIT_FAILURE, "couldn't resolve hostname %c%s%c, port `%s': %s",
+       bindhost ? '`' : '<',
+       bindhost ? bindhost : "nil",
+       bindhost ? '\'' : '>',
+       bindsvc, gai_strerror(err));
+   }
    if (chdir(dir)) {
      die(EXIT_FAILURE, "can't set current directory to `%s': %s",
        dir, strerror(errno));
    signal(SIGPIPE, SIG_IGN);
    for (i = 0; tunnels[i]; i++)
      tunnels[i]->init();
-   p_init(baddr, port);
+   p_init(ailist); freeaddrinfo(ailist);
    if (!(f & f_daemon)) {
      af = AF_WARN;
  #ifndef NTRACE
    a_init(csock, u, g, csockmode);
    u_setugid(u, g);
    km_init(kr_priv, kr_pub, tag_priv);
 +  kx_init();
    if (f & f_daemon) {
      if (daemonize())
        die(EXIT_FAILURE, "couldn't become a daemon: %s", strerror(errno));
diff --combined server/tripe.h
index a2907be1a6bff9a030d9dd4879c8eed1e96e14e6,29403993af5a60839745238277ecbf4e9a3b1e76..10a03f5fd79900201b2878a97a1bd76d32b00503
  #include <pwd.h>
  #include <grp.h>
  
+ #ifdef HAVE_LIBADNS
+ #  define ADNS_FEATURE_MANYAF
+ #  include <adns.h>
+ #endif
  #include <mLib/alloc.h>
  #include <mLib/arena.h>
  #include <mLib/base64.h>
- #include <mLib/bres.h>
+ #ifndef HAVE_LIBADNS
+ #  include <mLib/bres.h>
+ #endif
  #include <mLib/codec.h>
  #include <mLib/daemonize.h>
  #include <mLib/dstr.h>
@@@ -181,16 -188,6 +188,16 @@@ enum 
    DHFMT_VAR                  /* Variable-width-format, mostly a bad idea */
  };
  
 +typedef struct deriveargs {
 +  const char *what;                   /* Operation name (hashed) */
 +  unsigned f;                         /* Flags */
 +#define DF_IN 1u                      /*   Make incoming key */
 +#define DF_OUT 2u                     /*   Make outgoing key */
 +  const gchash *hc;                   /* Hash class */
 +  const octet *k;                     /* Pointer to contributions */
 +  size_t x, y, z;                     /* Markers in contributions */
 +} deriveargs;
 +
  typedef struct bulkalgs {
    const struct bulkops *ops;
  } bulkalgs;
@@@ -204,6 -201,8 +211,6 @@@ typedef struct bulkchal 
    size_t tagsz;
  } bulkchal;
  
 -struct rawkey;
 -
  typedef struct dhops {
    const char *name;
  
@@@ -333,17 -332,9 +340,17 @@@ typedef struct bulkops 
         * after which the keys must no longer be used.
         */
  
 -  bulkctx *(*genkeys)(const bulkalgs */*a*/, const struct rawkey */*rk*/);
 +  bulkctx *(*genkeys)(const bulkalgs */*a*/, const deriveargs */*a*/);
        /* Generate session keys and construct and return an appropriate
 -       * context for using them, by calling @ks_derive@.
 +       * context for using them.  The offsets @a->x@, @a->y@ and @a->z@
 +       * separate the key material into three parts.  Between @a->k@ and
 +       * @a->k + a->x@ is `my' contribution to the key material; between
 +       * @a->k + a->x@ and @a->k + a->y@ is `your' contribution; and
 +       * between @a->k + a->y@ and @a->k + a->z@ is a shared value we made
 +       * together.  These are used to construct (up to) two collections of
 +       * symmetric keys: one for outgoing messages, the other for incoming
 +       * messages.  If @a->x == 0@ (or @a->y == a->x@) then my (or your)
 +       * contribution is omitted.
         */
  
    bulkchal *(*genchal)(const bulkalgs */*a*/);
        /* Release a bulk encryption context and the resources it holds. */
  
    int (*chaltag)(bulkchal */*bc*/, const void */*m*/, size_t /*msz*/,
 -               void */*t*/);
 -      /* Calculate a tag for the challenge in @m@, @msz@, and write it to
 -       * @t@.  Return @-1@ on error, zero on success.
 +               uint32 /*seq*/, void */*t*/);
 +      /* Calculate a tag for the challenge in @m@, @msz@, with the sequence
 +       * number @seq@, and write it to @t@.  Return @-1@ on error, zero on
 +       * success.
         */
  
    int (*chalvrf)(bulkchal */*bc*/, const void */*m*/, size_t /*msz*/,
 -               const void */*t*/);
 -      /* Check the tag @t@ on @m@, @msz@: return zero if the tag is OK,
 -       * nonzero if it's bad.
 +               uint32 /*seq*/, const void */*t*/);
 +      /* Check the tag @t@ on @m@, @msz@ and @seq@: return zero if the tag
 +       * is OK, nonzero if it's bad.
         */
  
    void (*freechal)(bulkchal */*bc*/);
@@@ -402,7 -392,6 +409,7 @@@ struct algswitch 
  struct kdata {
    unsigned ref;                               /* Reference counter */
    struct knode *kn;                   /* Pointer to cache entry */
 +  uint32 id;                          /* The underlying key's id */
    char *tag;                          /* Full tag name of the key */
    dhgrp *grp;                         /* The group we work in */
    dhsc *k;                            /* The private key (or null) */
@@@ -428,6 -417,27 +435,27 @@@ extern const bulkops bulktab[]
  
  /*----- Data structures ---------------------------------------------------*/
  
+ /* --- The address-family table --- */
+ #define ADDRFAM(_)                                                    \
+   _(INET,     want_ipv4)                                              \
+   _(INET6,    want_ipv6)
+ enum {
+ #define ENUM(af, qf) AFIX_##af,
+   ADDRFAM(ENUM)
+ #undef ENUM
+   NADDRFAM
+ };
+ extern const struct addrfam {
+   int af;
+   const char *name;
+ #ifdef HAVE_LIBADNS
+   adns_queryflags qf;
+ #endif
+ } aftab[NADDRFAM];
  /* --- Socket addresses --- *
   *
   * A magic union of supported socket addresses.
  typedef union addr {
    struct sockaddr sa;
    struct sockaddr_in sin;
+   struct sockaddr_in6 sin6;
  } addr;
  
  /* --- Mapping keyed on addresses --- */
@@@ -610,14 -621,12 +639,14 @@@ typedef struct peerspec 
    char *name;                         /* Peer's name */
    char *privtag;                      /* Private key tag */
    char *tag;                          /* Public key tag */
 +  char *knock;                                /* Knock string, or null */
    const tunnel_ops *tops;             /* Tunnel operations */
    unsigned long t_ka;                 /* Keep alive interval */
    addr sa;                            /* Socket address to speak to */
    unsigned f;                         /* Flags for the peer */
  #define PSF_KXMASK 255u                       /*   Key-exchange flags to set */
  #define PSF_MOBILE 256u                       /*   Address may change rapidly */
 +#define PSF_EPHEM 512u                        /*   Association is ephemeral */
  } peerspec;
  
  typedef struct peer_byname {
@@@ -635,6 -644,7 +664,7 @@@ typedef struct peer 
    peer_byaddr *byaddr;                        /* Lookup-by-address block */
    struct ping *pings;                 /* Pings we're waiting for */
    peerspec spec;                      /* Specifications for this peer */
+   int afix;                           /* Index of address family */
    tunnel *t;                          /* Tunnel for local packets */
    char *ifname;                               /* Interface name for tunnel */
    keyset *ks;                         /* List head for keysets */
@@@ -691,9 -701,14 +721,14 @@@ typedef struct admin_bgop 
  typedef struct admin_resop {
    admin_bgop bg;                      /* Background operation header */
    char *addr;                         /* Hostname to be resolved */
+ #ifdef HAVE_LIBADNS
+   adns_query q;
+ #else
    bres_client r;                      /* Background resolver task */
+ #endif
    sel_timer t;                                /* Timer for resolver */
    addr sa;                            /* Socket address */
+   unsigned port;                      /* Port number chosen */
    size_t sasz;                                /* Socket address size */
    void (*func)(struct admin_resop *, int); /* Handler */
  } admin_resop;
@@@ -777,6 -792,7 +812,7 @@@ extern sel_state sel;                      /* Global I/O e
  extern octet buf_i[PKBUFSZ], buf_o[PKBUFSZ], buf_t[PKBUFSZ], buf_u[PKBUFSZ];
  extern const tunnel_ops *tunnels[];   /* Table of tunnels (0-term) */
  extern const tunnel_ops *tun_default; /* Default tunnel to use */
+ extern sel_file udpsock[NADDRFAM];    /* The master UDP sockets */
  extern kdata *master;                 /* Default private key */
  extern const char *tag_priv;          /* Default private key tag */
  
@@@ -830,19 -846,6 +866,19 @@@ extern int km_reload(void)
  extern kdata *km_findpub(const char */*tag*/);
  extern kdata *km_findpriv(const char */*tag*/);
  
 +/* --- @km_findpubbyid@, @km_findprivbyid@ --- *
 + *
 + * Arguments: @uint32 id@ = key id to load
 + *
 + * Returns:   Pointer to the kdata object if successful, or null on error.
 + *
 + * Use:               Fetches a public or private key from the keyring given its
 + *            numeric id.
 + */
 +
 +extern kdata *km_findpubbyid(uint32 /*id*/);
 +extern kdata *km_findprivbyid(uint32 /*id*/);
 +
  /* --- @km_samealgsp@ --- *
   *
   * Arguments: @const kdata *kdx, *kdy@ = two key data objects
@@@ -905,18 -908,16 +941,18 @@@ extern void kx_start(keyexch */*kx*/, i
  /* --- @kx_message@ --- *
   *
   * Arguments: @keyexch *kx@ = pointer to key exchange context
 + *            @const addr *a@ = sender's IP address and port
   *            @unsigned msg@ = the message code
   *            @buf *b@ = pointer to buffer containing the packet
   *
 - * Returns:   ---
 + * Returns:   Nonzero if the sender's address was unknown.
   *
   * Use:               Reads a packet containing key exchange messages and handles
   *            it.
   */
  
 -extern void kx_message(keyexch */*kx*/, unsigned /*msg*/, buf */*b*/);
 +extern int kx_message(keyexch */*kx*/, const addr */*a*/,
 +                    unsigned /*msg*/, buf */*b*/);
  
  /* --- @kx_free@ --- *
   *
@@@ -943,7 -944,7 +979,7 @@@ extern void kx_free(keyexch */*kx*/)
  
  extern void kx_newkeys(keyexch */*kx*/);
  
 -/* --- @kx_init@ --- *
 +/* --- @kx_setup@ --- *
   *
   * Arguments: @keyexch *kx@ = pointer to key exchange context
   *            @peer *p@ = pointer to peer context
   *            exchange.
   */
  
 -extern int kx_init(keyexch */*kx*/, peer */*p*/,
 -                 keyset **/*ks*/, unsigned /*f*/);
 -
 -/*----- Keysets and symmetric cryptography --------------------------------*/
 +extern int kx_setup(keyexch */*kx*/, peer */*p*/,
 +                  keyset **/*ks*/, unsigned /*f*/);
  
 -/* --- @ks_drop@ --- *
 +/* --- @kx_init@ --- *
   *
 - * Arguments: @keyset *ks@ = pointer to a keyset
 + * Arguments: ---
   *
   * Returns:   ---
   *
 - * Use:               Decrements a keyset's reference counter.  If the counter hits
 - *            zero, the keyset is freed.
 + * Use:               Initializes the key-exchange logic.
   */
  
 -extern void ks_drop(keyset */*ks*/);
 +extern void kx_init(void);
  
 -/* --- @ks_derivekey@ --- *
 +/*----- Keysets and symmetric cryptography --------------------------------*/
 +
 +/* --- @ks_drop@ --- *
   *
 - * Arguments: @octet *k@ = pointer to an output buffer of at least
 - *                    @MAXHASHSZ@ bytes
 - *            @size_t ksz@ = actual size wanted (for tracing)
 - *            @const struct rawkey *rk@ = a raw key, as passed into
 - *                    @genkeys@
 - *            @int dir@ = direction for the key (@DIR_IN@ or @DIR_OUT@)
 - *            @const char *what@ = label for the key (input to derivation)
 + * Arguments: @keyset *ks@ = pointer to a keyset
   *
   * Returns:   ---
   *
 - * Use:               Derives a session key, for use on incoming or outgoing data.
 - *            This function is part of a private protocol between @ks_gen@
 - *            and the bulk crypto transform @genkeys@ operation.
 + * Use:               Decrements a keyset's reference counter.  If the counter hits
 + *            zero, the keyset is freed.
   */
  
 -extern void ks_derivekey(octet */*k*/, size_t /*ksz*/,
 -                       const struct rawkey */*rk*/,
 -                       int /*dir*/, const char */*what*/);
 +extern void ks_drop(keyset */*ks*/);
  
  /* --- @ks_gen@ --- *
   *
 - * Arguments: @const void *k@ = pointer to key material
 - *            @size_t x, y, z@ = offsets into key material (see below)
 + * Arguments: @deriveargs *a@ = key derivation parameters (modified)
   *            @peer *p@ = pointer to peer information
   *
   * Returns:   A pointer to the new keyset.
   *
 - * Use:               Derives a new keyset from the given key material.  The
 - *            offsets @x@, @y@ and @z@ separate the key material into three
 - *            parts.  Between the @k@ and @k + x@ is `my' contribution to
 - *            the key material; between @k + x@ and @k + y@ is `your'
 - *            contribution; and between @k + y@ and @k + z@ is a shared
 - *            value we made together.  These are used to construct two
 - *            collections of symmetric keys: one for outgoing messages, the
 - *            other for incoming messages.
 + * Use:               Derives a new keyset from the given key material.  This will
 + *            set the @what@, @f@, and @hc@ members in @*a@; other members
 + *            must be filled in by the caller.
   *
   *            The new key is marked so that it won't be selected for output
   *            by @ksl_encrypt@.  You can still encrypt data with it by
   *            calling @ks_encrypt@ directly.
   */
  
 -extern keyset *ks_gen(const void */*k*/,
 -                    size_t /*x*/, size_t /*y*/, size_t /*z*/,
 -                    peer */*p*/);
 +extern keyset *ks_gen(deriveargs */*a*/, peer */*p*/);
  
  /* --- @ks_activate@ --- *
   *
@@@ -1140,29 -1159,25 +1176,29 @@@ extern int ksl_decrypt(keyset **/*ksroo
  
  /* --- @c_new@ --- *
   *
 - * Arguments: @buf *b@ = where to put the challenge
 + * Arguments: @const void *m@ = pointer to associated message, or null
 + *            @size_t msz@ = length of associated message
 + *            @buf *b@ = where to put the challenge
   *
   * Returns:   Zero if OK, nonzero on error.
   *
   * Use:               Issues a new challenge.
   */
  
 -extern int c_new(buf */*b*/);
 +extern int c_new(const void */*m*/, size_t /*msz*/, buf */*b*/);
  
  /* --- @c_check@ --- *
   *
 - * Arguments: @buf *b@ = where to find the challenge
 + * Arguments: @const void *m@ = pointer to associated message, or null
 + *            @size_t msz@ = length of associated message
 + *            @buf *b@ = where to find the challenge
   *
   * Returns:   Zero if OK, nonzero if it didn't work.
   *
   * Use:               Checks a challenge.  On failure, the buffer is broken.
   */
  
 -extern int c_check(buf */*b*/);
 +extern int c_check(const void */*m*/, size_t /*msz*/, buf */*b*/);
  
  /*----- Administration interface ------------------------------------------*/
  
@@@ -1456,20 -1471,6 +1492,20 @@@ extern int p_updateaddr(peer */*p*/, co
  
  extern buf *p_txstart(peer */*p*/, unsigned /*msg*/);
  
 +/* --- @p_txaddr@ --- *
 + *
 + * Arguments: @const addr *a@ = recipient address
 + *            @const void *p@ = pointer to packet to send
 + *            @size_t sz@ = length of packet
 + *
 + * Returns:   Zero if successful, nonzero on error.
 + *
 + * Use:               Sends a packet to an address which (possibly) isn't a current
 + *            peer.
 + */
 +
 +extern int p_txaddr(const addr */*a*/, const void */*p*/, size_t /*sz*/);
 +
  /* --- @p_txend@ --- *
   *
   * Arguments: @peer *p@ = pointer to peer block
@@@ -1600,24 -1601,23 +1636,23 @@@ extern const addr *p_addr(peer */*p*/)
  
  /* --- @p_init@ --- *
   *
-  * Arguments: @struct in_addr addr@ = address to bind to
-  *            @unsigned port@ = port number to listen to
+  * Arguments: @struct addrinfo *ailist@ = addresses to bind to
   *
   * Returns:   ---
   *
   * Use:               Initializes the peer system; creates the socket.
   */
  
- extern void p_init(struct in_addr /*addr*/, unsigned /*port*/);
+ extern void p_init(struct addrinfo */*ailist*/);
  
  /* --- @p_port@ --- *
   *
-  * Arguments: ---
+  * Arguments: @int i@ = address family index to retrieve
   *
   * Returns:   Port number used for socket.
   */
  
unsigned p_port(void);
extern unsigned p_port(int /*i*/);
  
  /* --- @p_create@ --- *
   *
@@@ -1694,14 -1694,13 +1729,14 @@@ extern peer *p_find(const char */*name*
  /* --- @p_destroy@ --- *
   *
   * Arguments: @peer *p@ = pointer to a peer
 + *            @int bye@ = say goodbye to the peer?
   *
   * Returns:   ---
   *
   * Use:               Destroys a peer.
   */
  
 -extern void p_destroy(peer */*p*/);
 +extern void p_destroy(peer */*p*/, int /*bye*/);
  
  /* --- @FOREACH_PEER@ --- *
   *
@@@ -1778,6 -1777,15 +1813,15 @@@ extern const char *timestr(time_t /*t*/
  
  extern int mystrieq(const char */*x*/, const char */*y*/);
  
+ /* --- @afix@ --- *
+  *
+  * Arguments: @int af@ = an address family code
+  *
+  * Returns:   The index of the address family's record in @aftab@, or @-1@.
+  */
+ extern int afix(int af);
  /* --- @addrsz@ --- *
   *
   * Arguments: @const addr *a@ = a network address
  
  extern socklen_t addrsz(const addr */*a*/);
  
+ /* --- @getport@, @setport@ --- *
+  *
+  * Arguments: @addr *a@ = a network address
+  *            @unsigned port@ = port number to set
+  *
+  * Returns:   ---
+  *
+  * Use:               Retrieves or sets the port number in an address structure.
+  */
+ extern unsigned getport(addr */*a*/);
+ extern void setport(addr */*a*/, unsigned /*port*/);
  /* --- @seq_reset@ --- *
   *
   * Arguments: @seqwin *s@ = sequence-checking window
@@@ -1812,75 -1833,6 +1869,75 @@@ extern void seq_reset(seqwin */*s*/)
  
  extern int seq_check(seqwin */*s*/, uint32 /*q*/, const char */*service*/);
  
 +typedef struct ratelim {
 +  unsigned n, max, persec;
 +  struct timeval when;
 +} ratelim;
 +
 +/* --- @ratelim_init@ --- *
 + *
 + * Arguments: @ratelim *r@ = rate-limiting state to fill in
 + *            @unsigned persec@ = credit to accumulate per second
 + *            @unsigned max@ = maximum credit to retain
 + *
 + * Returns:   ---
 + *
 + * Use:               Initialize a rate-limiting state.
 + */
 +
 +extern void ratelim_init(ratelim */*r*/,
 +                       unsigned /*persec*/, unsigned /*max*/);
 +
 +/* --- @ratelim_withdraw@ --- *
 + *
 + * Arguments: @ratelim *r@ = rate-limiting state
 + *            @unsigned n@ = credit to withdraw
 + *
 + * Returns:   Zero if successful; @-1@ if there is unsufficient credit
 + *
 + * Use:               Updates the state with any accumulated credit.  Then, if
 + *            there there are more than @n@ credits available, withdraw @n@
 + *            and return successfully; otherwise, report failure.
 + */
 +
 +extern int ratelim_withdraw(ratelim */*r*/, unsigned /*n*/);
 +
 +/* --- @ies_encrypt@ --- *
 + *
 + * Arguments: @kdata *kpub@ = recipient's public key
 + *            @unsigned ty@ = message type octet
 + *            @buf *b@ = input message buffer
 + *            @buf *bb@ = output buffer for the ciphertext
 + *
 + * Returns:   On error, returns a @KSERR_...@ code or breaks the buffer;
 + *            on success, returns zero and the buffer is good.
 + *
 + * Use:               Encrypts a message for a recipient, given their public key.
 + *            This does not (by itself) provide forward secrecy or sender
 + *            authenticity.  The ciphertext is self-delimiting (unlike
 + *            @ks_encrypt@).
 + */
 +
 +extern int ies_encrypt(kdata */*kpub*/, unsigned /*ty*/,
 +                     buf */*b*/, buf */*bb*/);
 +
 +/* --- @ies_decrypt@ --- *
 + *
 + * Arguments: @kdata *kpub@ = private key key
 + *            @unsigned ty@ = message type octet
 + *            @buf *b@ = input ciphertext buffer
 + *            @buf *bb@ = output buffer for the message
 + *
 + * Returns:   On error, returns a @KSERR_...@ code; on success, returns
 + *            zero and the buffer is good.
 + *
 + * Use:               Decrypts a message encrypted using @ies_encrypt@, given our
 + *            private key.
 + */
 +
 +extern int ies_decrypt(kdata */*kpriv*/, unsigned /*ty*/,
 +                     buf */*b*/, buf */*bb*/);
 +
  /*----- That's all, folks -------------------------------------------------*/
  
  #ifdef __cplusplus