Merge branch '1.0.0pre19.x'
authorMark Wooding <mdw@distorted.org.uk>
Mon, 25 May 2020 15:33:19 +0000 (16:33 +0100)
committerMark Wooding <mdw@distorted.org.uk>
Mon, 25 May 2020 15:33:19 +0000 (16:33 +0100)
* 1.0.0pre19.x:
  svc/connect.in: Squash newlines to spaces in `info' output.
  server/admin.c: Fix `=' vs `==' error in assertion.
  svc/tripe-ifup.in: Don't set remote IPv6 address until interface is up.

1  2 
server/admin.c
svc/connect.in
svc/tripe-ifup.in

diff --combined server/admin.c
@@@ -9,18 -9,19 +9,18 @@@
   *
   * This file is part of Trivial IP Encryption (TrIPE).
   *
 - * TrIPE is free software; you can redistribute it and/or modify
 - * it under the terms of the GNU General Public License as published by
 - * the Free Software Foundation; either version 2 of the License, or
 - * (at your option) any later version.
 + * TrIPE is free software: you can redistribute it and/or modify it under
 + * the terms of the GNU General Public License as published by the Free
 + * Software Foundation; either version 3 of the License, or (at your
 + * option) any later version.
   *
 - * TrIPE is distributed in the hope that it will be useful,
 - * but WITHOUT ANY WARRANTY; without even the implied warranty of
 - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 - * GNU General Public License for more details.
 + * TrIPE is distributed in the hope that it will be useful, but WITHOUT
 + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 + * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 + * for more details.
   *
   * You should have received a copy of the GNU General Public License
 - * along with TrIPE; if not, write to the Free Software Foundation,
 - * Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 + * along with TrIPE.  If not, see <https://www.gnu.org/licenses/>.
   */
  
  /*----- Header files ------------------------------------------------------*/
@@@ -61,14 -62,10 +61,14 @@@ 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;
 -static const char *sockname;
 +static const char *sockname = 0;
  static sym_table a_svcs;
  static unsigned flags = 0;
  static admin *a_stdin = 0;
@@@ -260,9 -257,7 +260,9 @@@ static void a_flush(int fd, unsigned mo
   *
   *              * "?PEER" PEER -- peer's name
   *
 - *              * "?ERRNO" ERRNO -- system error code
 + *              * "?ERR" CODE -- system error code
 + *
 + *              * "?ERRNO" -- system error code from @errno@
   *
   *              * "[!]..." ... -- @dstr_putf@-like string as single token
   */
@@@ -278,38 -273,32 +278,38 @@@ 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 *);
        size_t n = va_arg(*ap, size_t);
 -      base64_ctx b64;
 +      codec *b64 = base64_class.encoder(CDCF_NOEQPAD, "", 0);
        dstr_putc(d, ' ');
 -      base64_init(&b64);
 -      b64.indent = "";
 -      b64.maxline = 0;
 -      base64_encode(&b64, p, n, d);
 -      base64_encode(&b64, 0, 0, d);
 -      while (d->len && d->buf[d->len - 1] == '=') d->len--;
 +      b64->ops->code(b64, p, n, d);
 +      b64->ops->code(b64, 0, 0, d);
 +      b64->ops->destroy(b64);
        } else if (strcmp(fmt, "?TOKENS") == 0) {
        const char *const *av = va_arg(*ap, const char *const *);
        while (*av) u_quotify(d, *av++);
        } else if (strcmp(fmt, "?PEER") == 0)
        u_quotify(d, p_name(va_arg(*ap, peer *)));
 -      else if (strcmp(fmt, "?ERRNO") == 0) {
 +      else if (strcmp(fmt, "?ERR") == 0) {
 +      int e = va_arg(*ap, int);
 +      dstr_putf(d, " E%d", e);
 +      u_quotify(d, strerror(e));
 +      } else if (strcmp(fmt, "?ERRNO") == 0) {
        dstr_putf(d, " E%d", errno);
        u_quotify(d, strerror(errno));
        } else
@@@ -553,6 -542,24 +553,6 @@@ void a_notify(const char *fmt, ...
    va_end(ap);
  }
  
 -/* --- @a_quit@ --- *
 - *
 - * Arguments: ---
 - *
 - * Returns:   ---
 - *
 - * Use:               Shuts things down nicely.
 - */
 -
 -void a_quit(void)
 -{
 -  close(sock.fd);
 -  unlink(sockname);
 -  FOREACH_PEER(p, { p_destroy(p); });
 -  ps_quit();
 -  exit(0);
 -}
 -
  /* --- @a_sigdie@ --- *
   *
   * Arguments: @int sig@ = signal number
@@@ -577,7 -584,7 +577,7 @@@ static void a_sigdie(int sig, void *v
        break;
    }
    a_warn("SERVER", "quit", "signal", "%s", p, A_END);
 -  a_quit();
 +  lp_end();
  }
  
  /* --- @a_sighup@ --- *
@@@ -890,7 -897,7 +890,7 @@@ static void a_jobdestroy(admin_svcop *s
    IF_TRACING(T_ADMIN, {
      trace(T_ADMIN, "admin: destroying job %s (%u)", a_jobidencode(svc), i);
    })
-   assert(j->v[i].u.op = svc);
+   assert(j->v[i].u.op == svc);
    j->v[i].u.next = j->free;
    j->v[i].seq++;
    j->free = i;
@@@ -1003,101 -1010,6 +1003,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
@@@ -1112,17 -1024,13 +1112,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
@@@ -1149,11 -1055,7 +1149,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);
  }
@@@ -1175,11 -1077,7 +1175,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@ --- *
@@@ -1202,39 -1100,21 +1202,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].sf.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->sasz = sizeof(r->sa.sin);
    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].sf.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].sf.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].sf.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 ----------------------------------------------------*/
@@@ -1396,6 -1227,7 +1396,6 @@@ static void a_doadd(admin_resop *r, in
    T( trace(T_ADMIN, "admin: done add op %s", BGTAG(add)); )
  
    if (rc == ARES_OK) {
 -    add->peer.sasz = add->r.sasz;
      add->peer.sa = add->r.sa;
      if (p_findbyaddr(&add->r.sa))
        a_bgfail(&add->r.bg, "peer-addr-exists", "?ADDR", &add->r.sa, A_END);
    }
  
    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);
  }
  
@@@ -1428,7 -1258,6 +1428,7 @@@ static void acmd_add(admin *a, unsigne
  {
    const char *tag = 0;
    admin_addop *add;
 +  const tunnel_ops *tops;
  
    /* --- Set stuff up --- */
  
    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.tops = p_dflttun();
    add->peer.f = 0;
  
    /* --- Parse options --- */
    OPTIONS(ac, av, {
      OPTARG("-background", arg, { tag = arg; })
      OPTARG("-tunnel", arg, {
 -      unsigned i;
 -      for (i = 0;; i++) {
 -      if (!tunnels[i]) {
 -        a_fail(a, "unknown-tunnel", "%s", arg, A_END);
 -        goto fail;
 -      }
 -      if (mystrieq(arg, tunnels[i]->name)) {
 -        add->peer.tops = tunnels[i];
 -        break;
 -      }
 -      }
 +      if ((tops = p_findtun(arg)) == 0)
 +      { a_fail(a, "unknown-tunnel", "%s", arg, A_END); goto fail; }
 +      add->peer.tops = tops;
      })
      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);
 +      if (add->peer.tag) xfree(add->peer.tag);
        add->peer.tag = xstrdup(arg);
      })
      OPT("-mobile", { add->peer.f |= PSF_MOBILE; })
      OPTARG("-priv", arg, {
 -      if (add->peer.privtag)
 -      xfree(add->peer.privtag);
 +      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);
 +    })
    });
  
    /* --- Make sure someone's not got there already --- */
@@@ -1492,7 -1325,6 +1492,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;
  }
@@@ -1841,27 -1673,7 +1841,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].sf.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].sf.fd >= 0) goto found;
 +    abort();
 +  }
 +  a_info(a, "%u", udpsock[i].port, A_END);
 +  a_ok(a);
 +}
  
  static void acmd_daemon(admin *a, unsigned ac, char *av[])
  {
@@@ -1971,50 -1783,42 +1971,50 @@@ 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);
  }
  
  static void acmd_checkchal(admin *a, unsigned ac, char *av[])
  {
 -  base64_ctx b64;
 +  codec *b64 = base64_class.decoder(CDCF_NOEQPAD);
 +  int err;
    buf b;
    dstr d = DSTR_INIT;
  
 -  base64_init(&b64);
 -  base64_decode(&b64, av[0], strlen(av[0]), &d);
 -  base64_decode(&b64, 0, 0, &d);
 -  buf_init(&b, d.buf, d.len);
 -  if (c_check(&b) || BBAD(&b) || BLEFT(&b))
 -    a_fail(a, "invalid-challenge", A_END);
 -  else
 -    a_ok(a);
 +  if ((err = b64->ops->code(b64, av[0], strlen(av[0]), &d)) != 0 ||
 +      (err = b64->ops->code(b64, 0, 0, &d)) != 0)
 +    a_fail(a, "bad-base64", "%s", codec_strerror(err), A_END);
 +  else {
 +    buf_init(&b, d.buf, d.len);
 +    if (c_check(0, 0, &b) || BBAD(&b) || BLEFT(&b))
 +      a_fail(a, "invalid-challenge", A_END);
 +    else
 +      a_ok(a);
 +  }
 +  b64->ops->destroy(b64);
    dstr_destroy(&d);
  }
  
  static void acmd_greet(admin *a, unsigned ac, char *av[])
  {
    peer *p;
 -  base64_ctx b64;
 +  int err;
 +  codec *b64;
    dstr d = DSTR_INIT;
  
 -  if ((p = a_findpeer(a, av[0])) != 0) {
 -    base64_init(&b64);
 -    base64_decode(&b64, av[1], strlen(av[1]), &d);
 -    base64_decode(&b64, 0, 0, &d);
 +  if ((p = a_findpeer(a, av[0])) == 0) return;
 +  b64 = base64_class.decoder(CDCF_NOEQPAD);
 +  if ((err = b64->ops->code(b64, av[1], strlen(av[1]), &d)) != 0 ||
 +      (err = b64->ops->code(b64, 0, 0, &d)) != 0)
 +    a_fail(a, "bad-base64", "%s", codec_strerror(err), A_END);
 +  else {
      p_greet(p, d.buf, d.len);
 -    dstr_destroy(&d);
      a_ok(a);
    }
 +  b64->ops->destroy(b64);
 +  dstr_destroy(&d);
  }
  
  static void acmd_addr(admin *a, unsigned ac, char *av[])
  
    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);
    }
@@@ -2038,17 -1843,12 +2038,17 @@@ 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, "private-key=%s", ptag,
           "current-private-key=%s", p->kx.kpriv->tag, A_END);
      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);
    }
  }
@@@ -2103,7 -1903,7 +2103,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);
    }
  }
@@@ -2125,7 -1925,7 +2125,7 @@@ static void acmd_quit(admin *a, unsigne
  {
    a_warn("SERVER", "quit", "admin-request", A_END);
    a_ok(a);
 -  a_quit();
 +  lp_end();
  }
  
  static void acmd_version(admin *a, unsigned ac, char *av[])
  
  static void acmd_tunnels(admin *a, unsigned ac, char *av[])
  {
 -  int i;
 -
 -  for (i = 0; tunnels[i]; i++)
 -    a_info(a, "%s", tunnels[i]->name, A_END);
 +  FOREACH_TUN(tops, { a_info(a, "%s", tops->name, A_END); });
    a_ok(a);
  }
  
@@@ -2170,12 -1973,11 +2170,12 @@@ 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 },
    { "setifname", "PEER NEW-NAME",     2,      2,      acmd_setifname },
 +  { "stats",  "PEER",                 1,      1,      acmd_stats },
    { "svcclaim",       "SERVICE VERSION",      2,      2,      acmd_svcclaim },
    { "svcensure", "SERVICE [VERSION]", 1,      2,      acmd_svcensure },
    { "svcfail",        "JOBID TOKENS...",      1,      0xffff, acmd_svcfail },
    { "svcrelease", "SERVICE",          1,      1,      acmd_svcrelease },
    { "svcsubmit", "[OPTIONS] SERVICE TOKENS...",
                                        2,      0xffff, acmd_svcsubmit },
 -  { "stats",  "PEER",                 1,      1,      acmd_stats },
  #ifndef NTRACE
    { "trace",  "[OPTIONS]",            0,      1,      acmd_trace },
  #endif
@@@ -2250,7 -2053,7 +2250,7 @@@ static void a_destroypending(void
        if (a->f & AF_FOREGROUND) {
        T( trace(T_ADMIN, "admin: foreground client quit: shutting down"); )
        a_warn("SERVER", "quit", "foreground-eof", A_END);
 -      a_quit();
 +      lp_end();
        }
  
        /* --- Abort any background jobs in progress --- */
@@@ -2381,10 -2184,7 +2381,10 @@@ static void a_line(char *p, size_t len
   *
   * Returns:   ---
   *
 - * Use:               Creates a new admin connection.
 + * Use:               Creates a new admin connection.  It's safe to call this
 + *            before @a_init@ -- and, indeed, this makes sense if you also
 + *            call @a_switcherr@ to report initialization errors through
 + *            the administration machinery.
   */
  
  void a_create(int fd_in, int fd_out, unsigned f)
@@@ -2462,33 -2262,36 +2462,33 @@@ void a_preselect(void) { if (a_dead) a_
  
  void a_daemon(void) { flags |= F_DAEMON; }
  
 -/* --- @a_init@ --- *
 +/* --- @a_listen@ --- *
   *
   * Arguments: @const char *name@ = socket name to create
   *            @uid_t u@ = user to own the socket
   *            @gid_t g@ = group to own the socket
   *            @mode_t m@ = permissions to set on the socket
   *
 - * Returns:   ---
 + * Returns:   Zero on success, @-1@ on failure.
   *
   * Use:               Creates the admin listening socket.
   */
  
 -void a_init(const char *name, uid_t u, gid_t g, mode_t m)
 +int a_listen(const char *name, uid_t u, gid_t g, mode_t m)
  {
    int fd;
    int n = 5;
    struct sockaddr_un sun;
 -  struct sigaction sa;
    size_t sz;
    mode_t omask;
  
 -  /* --- Create services table --- */
 -
 -  sym_create(&a_svcs);
 -
    /* --- Set up the socket address --- */
  
    sz = strlen(name) + 1;
 -  if (sz > sizeof(sun.sun_path))
 -    die(EXIT_FAILURE, "socket name `%s' too long", name);
 +  if (sz > sizeof(sun.sun_path)) {
 +    a_warn("ADMIN", "admin-socket", "%s", name, "name-too-long", A_END);
 +    goto fail_0;
 +  }
    BURN(sun);
    sun.sun_family = AF_UNIX;
    memcpy(sun.sun_path, name, sz);
  
    omask = umask(0077);
  again:
 -  if ((fd = socket(PF_UNIX, SOCK_STREAM, 0)) < 0)
 -    die(EXIT_FAILURE, "couldn't create socket: %s", strerror(errno));
 +  if ((fd = socket(PF_UNIX, SOCK_STREAM, 0)) < 0) {
 +    a_warn("ADMIN", "admin-socket", "%s", sun.sun_path,
 +         "create-failed", "?ERRNO", A_END);
 +    goto fail_1;
 +  }
    if (bind(fd, (struct sockaddr *)&sun, sz) < 0) {
      struct stat st;
      int e = errno;
      if (errno != EADDRINUSE) {
 -      die(EXIT_FAILURE, "couldn't bind to address `%s': %s",
 -        sun.sun_path, strerror(e));
 +      a_warn("ADMIN", "admin-socket", "%s", sun.sun_path,
 +           "bind-failed", "?ERRNO", A_END);
 +      goto fail_2;
 +    }
 +    if (!n) {
 +      a_warn("ADMIN", "admin-socket", "%s", sun.sun_path,
 +           "too-many-retries", A_END);
 +      goto fail_2;
      }
 -    if (!n)
 -      die(EXIT_FAILURE, "too many retries; giving up");
      n--;
      if (!connect(fd, (struct sockaddr *)&sun, sz)) {
 -      die(EXIT_FAILURE, "server already listening on admin socket `%s'",
 -        sun.sun_path);
 +      a_warn("ADMIN", "admin-socket", "%s", sun.sun_path,
 +           "already-in-use", A_END);
 +      goto fail_2;
 +    }
 +    if (errno != ECONNREFUSED) {
 +      a_warn("ADMIN", "admin-socket", "%s", sun.sun_path,
 +           "bind-failed", "?ERR", e, A_END);
 +      goto fail_2;
      }
 -    if (errno != ECONNREFUSED)
 -      die(EXIT_FAILURE, "couldn't bind to address: %s", strerror(e));
      if (stat(sun.sun_path, &st)) {
 -      die(EXIT_FAILURE, "couldn't stat `%s': %s",
 -        sun.sun_path, strerror(errno));
 +      if (errno == ENOENT) { close(fd); goto again; }
 +      a_warn("ADMIN", "admin-socket", "%s", sun.sun_path,
 +           "stat-failed", "?ERRNO", A_END);
 +      goto fail_2;
 +    }
 +    if (!S_ISSOCK(st.st_mode)) {
 +      a_warn("ADMIN", "admin-socket", "%s", sun.sun_path,
 +           "not-a-socket", A_END);
 +      goto fail_2;
      }
 -    if (!S_ISSOCK(st.st_mode))
 -      die(EXIT_FAILURE, "object `%s' isn't a socket", sun.sun_path);
      T( trace(T_ADMIN, "admin: stale socket found; removing it"); )
      unlink(sun.sun_path);
      close(fd);
      goto again;
    }
    if (chown(sun.sun_path, u, g)) {
 -    die(EXIT_FAILURE, "failed to set socket owner: %s",
 -      strerror(errno));
 +    a_warn("ADMIN", "admin-socket", "%s", sun.sun_path,
 +         "chown-failed", "?ERRNO", A_END);
 +    goto fail_3;
    }
    if (chmod(sun.sun_path, m)) {
 -    die(EXIT_FAILURE, "failed to set socket permissions: %s",
 -      strerror(errno));
 +    a_warn("ADMIN", "admin-socket", "%s", sun.sun_path,
 +         "chmod-failed", "?ERRNO", A_END);
 +    goto fail_3;
    }
 -  umask(omask);
    fdflags(fd, O_NONBLOCK, O_NONBLOCK, FD_CLOEXEC, FD_CLOEXEC);
 -  if (listen(fd, 5))
 -    die(EXIT_FAILURE, "couldn't listen on socket: %s", strerror(errno));
 +  if (listen(fd, 5)) {
 +    a_warn("ADMIN", "admin-socket", "%s", sun.sun_path,
 +         "listen-failed", "?ERRNO", A_END);
 +    goto fail_3;
 +  }
 +  umask(omask);
  
    /* --- Listen to the socket --- */
  
    sel_initfile(&sel, &sock, fd, SEL_READ, a_accept, 0);
    sel_addfile(&sock);
    sockname = name;
 -  bres_init(&sel);
 +
 +  return (0);
 +
 +  /* --- Clean up if things go sideways --- */
 +
 +fail_3:
 +  unlink(sun.sun_path);
 +fail_2:
 +  close(fd);
 +fail_1:
 +  umask(omask);
 +fail_0:
 +  return (-1);
 +}
 +
 +/* --- @a_unlisten@ --- *
 + *
 + * Arguments: ---
 + *
 + * Returns:   ---
 + *
 + * Use:               Stops listening to the administration socket and removes it.
 + */
 +
 +void a_unlisten(void)
 +{
 +  if (!sockname) return;
 +  sel_rmfile(&sock);
 +  unlink(sockname);
 +  close(sock.fd);
 +}
 +
 +/* --- @a_switcherr@ --- *
 + *
 + * Arguments: ---
 + *
 + * Returns:   ---
 + *
 + * Use:               Arrange to report warnings, trace messages, etc. to
 + *            administration clients rather than the standard-error stream.
 + *
 + *            Obviously this makes no sense unless there is at least one
 + *            client established.  Calling @a_listen@ won't help with this,
 + *            because the earliest a new client can connect is during the
 + *            first select-loop iteration, which is too late: some initial
 + *            client must have been added manually using @a_create@.
 + */
 +
 +void a_switcherr(void)
 +{
    T( trace_custom(a_trace, 0);
       trace(T_ADMIN, "admin: enabled custom tracing"); )
    flags |= F_INIT;
 +}
  
 -  /* --- Set up signal handlers --- */
 +/* --- @a_signals@ --- *
 + *
 + * Arguments: ---
 + *
 + * Returns:   ---
 + *
 + * Use:               Establishes handlers for the obvious signals.
 + */
 +
 +void a_signals(void)
 +{
 +  struct sigaction sa;
  
    sig_add(&s_term, SIGTERM, a_sigdie, 0);
    sig_add(&s_hup, SIGHUP, a_sighup, 0);
 -  signal(SIGPIPE, SIG_IGN);
    sigaction(SIGINT, 0, &sa);
    if (sa.sa_handler != SIG_IGN)
      sig_add(&s_int, SIGINT, a_sigdie, 0);
  }
  
 +/* --- @a_init@ --- *
 + *
 + * Arguments: ---
 + *
 + * Returns:   Zero on success, @-1@ on failure.
 + *
 + * Use:               Creates the admin listening socket.
 + */
 +
 +int a_init(void)
 +{
 +#ifdef HAVE_LIBADNS
 +  int err;
 +#endif
 +
 +  /* --- Prepare the background name resolver --- */
 +
 +#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) {
 +    a_warn("ADMIN", "adns-init-failed", "?ERRNO", A_END);
 +    return (-1);
 +  }
 +  sel_addhook(&sel, &hook, before_select, after_select, 0);
 +#else
 +  bres_init(&sel);
 +#endif
 +
 +  /* --- Create services table --- */
 +
 +  sym_create(&a_svcs);
 +
 +  /* --- All done --- */
 +
 +  return (0);
 +}
 +
  /*----- That's all, folks -------------------------------------------------*/
diff --combined svc/connect.in
  ###
  ### This file is part of Trivial IP Encryption (TrIPE).
  ###
 -### TrIPE is free software; you can redistribute it and/or modify
 -### it under the terms of the GNU General Public License as published by
 -### the Free Software Foundation; either version 2 of the License, or
 -### (at your option) any later version.
 +### TrIPE is free software: you can redistribute it and/or modify it under
 +### the terms of the GNU General Public License as published by the Free
 +### Software Foundation; either version 3 of the License, or (at your
 +### option) any later version.
  ###
 -### TrIPE is distributed in the hope that it will be useful,
 -### but WITHOUT ANY WARRANTY; without even the implied warranty of
 -### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 -### GNU General Public License for more details.
 +### TrIPE is distributed in the hope that it will be useful, but WITHOUT
 +### ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 +### FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 +### for more details.
  ###
  ### You should have received a copy of the GNU General Public License
 -### along with TrIPE; if not, write to the Free Software Foundation,
 -### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 +### along with TrIPE.  If not, see <https://www.gnu.org/licenses/>.
  
  VERSION = '@VERSION@'
  
@@@ -33,11 -34,9 +33,11 @@@ import tripe as 
  import os as OS
  import signal as SIG
  import errno as E
 +from math import sqrt
  import cdb as CDB
  import mLib as M
  import re as RX
 +import sys as SYS
  from time import time
  import subprocess as PROC
  
@@@ -296,15 -295,12 +296,15 @@@ class Peer (object)
      a FILTER function is given then apply it to the information from the
      database before returning it.
      """
 -    attr = me.__dict__.get(key, default)
 -    if attr is _magic:
 -      raise T.TripeJobError('malformed-peer', me.name, 'missing-key', key)
 -    elif filter is not None:
 -      attr = filter(attr)
 -    return attr
 +    try:
 +      attr = me.__dict__[key]
 +    except KeyError:
 +      if default is _magic:
 +        raise T.TripeJobError('malformed-peer', me.name, 'missing-key', key)
 +      return default
 +    else:
 +      if filter is not None: attr = filter(attr)
 +      return attr
  
    def has(me, key):
      """
@@@ -400,13 -396,6 +400,13 @@@ class PingPeer (object)
      me.seq = _pingseq
      _pingseq += 1
      me._failures = 0
 +    me._sabotage = False
 +    me._last = '-'
 +    me._nping = 0
 +    me._nlost = 0
 +    me._sigma_t = 0
 +    me._sigma_t2 = 0
 +    me._min = me._max = '-'
      if pingnow:
        me._timer = None
        me._ping()
      me._timeout = peer.get('timeout', filter = T.timespec, default = 10)
      me._retries = peer.get('retries', filter = int, default = 5)
      me._connectp = peer.has('connect')
 +    me._knockp = peer.has('knock')
      return me
  
    def _ping(me):
         me._peer]))
  
    def _reconnect(me):
 -    peer = Peer(me._peer)
 -    if me._connectp:
 -      S.warn('connect', 'reconnecting', me._peer)
 -      S.forcekx(me._peer)
 -      T.spawn(run_connect, peer, peer.get('connect'))
 -      me._timer = M.SelTimer(time() + me._every, me._time)
 -    else:
 -      S.kill(me._peer)
 +    try:
 +      peer = Peer(me._peer)
 +      if me._connectp or me._knockp:
 +        S.warn('connect', 'reconnecting', me._peer)
 +        S.forcekx(me._peer)
 +        if me._connectp: T.spawn(run_connect, peer, peer.get('connect'))
 +        me._timer = M.SelTimer(time() + me._every, me._time)
 +        me._sabotage = False
 +      else:
 +        S.kill(me._peer)
 +    except TripeError, e:
 +      if e.args[0] == 'unknown-peer': me._pinger.kill(me._peer)
  
    def event(me, code, stuff):
      """
        me._ping()
      elif code == 'FAIL':
        S.notify('connect', 'ping-failed', me._peer, *stuff)
 -      if not stuff:
 -        pass
 -      elif stuff[0] == 'unknown-peer':
 -        me._pinger.kill(me._peer)
 -      elif stuff[0] == 'ping-send-failed':
 -        me._reconnect()
 +      if not stuff: pass
 +      elif stuff[0] == 'unknown-peer': me._pinger.kill(me._peer)
 +      elif stuff[0] == 'ping-send-failed': me._reconnect()
      elif code == 'INFO':
 -      if stuff[0] == 'ping-ok':
 -        if me._failures > 0:
 -          S.warn('connect', 'ping-ok', me._peer)
 +      outcome = stuff[0]
 +      if outcome == 'ping-ok' and me._sabotage:
 +        outcome = 'ping-timeout'
 +      if outcome == 'ping-ok':
 +        if me._failures > 0: S.warn('connect', 'ping-ok', me._peer)
 +        t = float(stuff[1])
 +        me._last = '%.1fms' % t
 +        me._sigma_t += t
 +        me._sigma_t2 += t*t
 +        me._nping += 1
 +        if me._min == '-' or t < me._min: me._min = t
 +        if me._max == '-' or t > me._max: me._max = t
          me._timer = M.SelTimer(time() + me._every, me._time)
 -      elif stuff[0] == 'ping-timeout':
 +      elif outcome == 'ping-timeout':
          me._failures += 1
 +        me._nlost += 1
          S.warn('connect', 'ping-timeout', me._peer,
                 'attempt', str(me._failures), 'of', str(me._retries))
          if me._failures < me._retries:
            me._ping()
 +          me._last = 'timeout'
          else:
            me._reconnect()
 -      elif stuff[0] == 'ping-peer-died':
 +          me._last = 'reconnect'
 +      elif outcome == 'ping-peer-died':
          me._pinger.kill(me._peer)
  
 +  def sabotage(me):
 +    """Sabotage the peer, for testing purposes."""
 +    me._sabotage = True
 +    if me._timer: me._timer.kill()
 +    T.defer(me._time)
 +
 +  def info(me):
 +    if not me._nping:
 +      mean = sd = '-'
 +    else:
 +      mean = me._sigma_t/me._nping
 +      sd = sqrt(me._sigma_t2/me._nping - mean*mean)
 +    n = me._nping + me._nlost
 +    if not n: pclost = '-'
 +    else: pclost = '%d' % ((100*me._nlost + n//2)//n)
 +    return { 'last-ping': me._last,
 +             'mean-ping': '%.1fms' % mean,
 +             'sd-ping': '%.1fms' % sd,
 +             'n-ping': '%d' % me._nping,
 +             'n-lost': '%d' % me._nlost,
 +             'percent-lost': pclost,
 +             'min-ping': '%.1fms' % me._min,
 +             'max-ping': '%.1fms' % me._max,
 +             'state': me._timer and 'idle' or 'check',
 +             'failures': me._failures }
 +
    @T._callback
    def _time(me):
      """
@@@ -568,9 -517,7 +568,9 @@@ class Pinger (T.Coroutine)
      while True:
        (peer, seq), code, stuff = me._q.get()
        if peer in me._peers and seq == me._peers[peer].seq:
 -        me._peers[peer].event(code, stuff)
 +        try: me._peers[peer].event(code, stuff)
 +        except Exception, e:
 +          SYS.excepthook(*SYS.exc_info())
  
    def add(me, peer, pingnow):
      """
  
    def kill(me, peername):
      """Remove PEER from the peers being watched by the Pinger."""
 -    del me._peers[peername]
 +    try: del me._peers[peername]
 +    except KeyError: pass
      return me
  
    def rescan(me, startup):
      """
      return me._peers.keys()
  
 +  def find(me, name):
 +    """Return the PingPeer with the given name."""
 +    return me._peers[name]
 +
  ###--------------------------------------------------------------------------
  ### New connections.
  
@@@ -717,26 -659,23 +717,26 @@@ def disownpeer(peer)
      T.Coroutine(run_ifupdown, name = 'ifdown %s' % peer.name) \
          .switch('ifdown', peer)
  
 -def addpeer(peer, addr):
 +def addpeer(peer, addr, ephemp):
    """
    Process a connect request from a new peer PEER on address ADDR.
  
 -  Any existing peer with this name is disconnected from the server.
 +  Any existing peer with this name is disconnected from the server.  EPHEMP
 +  is the default ephemeral-ness state for the new peer.
    """
    if peer.name in S.list():
      S.kill(peer.name)
    try:
 -    booltrue = ['t', 'true', 'y', 'yes', 'on']
      S.add(peer.name,
 -          tunnel = peer.get('tunnel', None),
 -          keepalive = peer.get('keepalive', None),
 -          key = peer.get('key', None),
 -          priv = peer.get('priv', None),
 -          mobile = peer.get('mobile', 'nil') in booltrue,
 -          cork = peer.get('cork', 'nil') in booltrue,
 +          tunnel = peer.get('tunnel', default = None),
 +          keepalive = peer.get('keepalive', default = None),
 +          key = peer.get('key', default = None),
 +          priv = peer.get('priv', default = None),
 +          mobile = peer.get('mobile', filter = boolean, default = False),
 +          knock = peer.get('knock', default = None),
 +          cork = peer.get('cork', filter = boolean, default = False),
 +          ephemeral = peer.get('ephemeral', filter = boolean,
 +                               default = ephemp),
            *addr)
    except T.TripeError, exc:
      raise T.TripeJobError(*exc.args)
@@@ -765,23 -704,6 +765,23 @@@ def notify(_, code, *rest)
      try: cr = chalmap[chal]
      except KeyError: pass
      else: cr.switch(rest[1:])
 +  elif code == 'KNOCK':
 +    try: p = Peer(rest[0])
 +    except KeyError:
 +      S.warn(['connect', 'knock-unknown-peer', rest[0]])
 +      return
 +    if p.get('peer') != 'PASSIVE':
 +      S.warn(['connect', 'knock-active-peer', p.name])
 +      return
 +    dot = p.name.find('.')
 +    if dot >= 0: kname = p.name[dot + 1:]
 +    else: kname = p.name
 +    ktag = p.get('key', p.name)
 +    if kname != ktag:
 +      S.warn(['connect', 'knock-tag-mismatch',
 +              'peer', pname, 'public-key-tag', ktag])
 +      return
 +    T.spawn(addpeer, p, rest[1:], True)
  
  ###--------------------------------------------------------------------------
  ### Command implementation.
@@@ -790,13 -712,11 +790,13 @@@ def cmd_kick(name)
    """
    kick NAME: Force a new connection attempt for the NAMEd peer.
    """
 -  if name not in pinger.adopted():
 -    raise T.TripeJobError('peer-not-adopted', name)
 +  try: pp = pinger.find(name)
 +  except KeyError: raise T.TripeJobError('peer-not-adopted', name)
    try: peer = Peer(name)
    except KeyError: raise T.TripeJobError('unknown-peer', name)
 -  T.spawn(run_connect, peer, peer.get('connect'))
 +  conn = peer.get('connect', None)
 +  if conn: T.spawn(run_connect, peer, peer.get('connect'))
 +  else: T.spawn(lambda p: S.forcekx(p.name), peer)
  
  def cmd_adopted():
    """
@@@ -816,7 -736,7 +816,7 @@@ def cmd_active(name)
    addr = peer.get('peer')
    if addr == 'PASSIVE':
      raise T.TripeJobError('passive-peer', name)
 -  addpeer(peer, M.split(addr, quotep = True)[0])
 +  addpeer(peer, M.split(addr, quotep = True)[0], True)
  
  def cmd_listactive():
    """
@@@ -833,16 -753,10 +833,16 @@@ def cmd_info(name)
    """
    try: peer = Peer(name)
    except KeyError: raise T.TripeJobError('unknown-peer', name)
 -  items = list(peer.list())
 +  d = {}
 +  try: pp = pinger.find(name)
 +  except KeyError: pass
 +  else: d.update(pp.info())
 +  items = list(peer.list()) + d.keys()
    items.sort()
    for i in items:
 -    T.svcinfo('%s=%s' % (i, peer.get(i).replace('\n', ' ')))
 +    try: v = d[i]
 +    except KeyError: v = peer.get(i)
-     T.svcinfo('%s=%s' % (i, v))
++    T.svcinfo('%s=%s' % (i, v.replace('\n', ' ')))
  
  def cmd_userpeer(user):
    """
@@@ -878,18 -792,10 +878,18 @@@ def cmd_passive(*args)
      addr = cr.parent.switch()
      if addr is None:
        raise T.TripeJobError('connect-timeout')
 -    addpeer(peer, addr)
 +    addpeer(peer, addr, True)
    finally:
      del chalmap[chal]
  
 +def cmd_sabotage(name):
 +  """
 +  sabotage NAME: Sabotage the NAMEd peer so that we think it can't be pinged.
 +  """
 +  try: pp = pinger.find(name)
 +  except KeyError: raise T.TripeJobError('unknown-peer', name)
 +  pp.sabotage()
 +
  ###--------------------------------------------------------------------------
  ### Start up.
  
@@@ -914,7 -820,7 +914,7 @@@ def setup()
      for name in M.split(autos)[0]:
        try:
          peer = Peer(name, cdb)
 -        addpeer(peer, M.split(peer.get('peer'), quotep = True)[0])
 +        addpeer(peer, M.split(peer.get('peer'), quotep = True)[0], False)
        except T.TripeJobError, err:
          S.warn('connect', 'auto-add-failed', name, *err.args)
  
@@@ -973,13 -879,11 +973,13 @@@ service_info = [('connect', T.VERSION, 
    'active': (1, 1, 'PEER', cmd_active),
    'info': (1, 1, 'PEER', cmd_info),
    'list-active': (0, 0, '', cmd_listactive),
 -  'userpeer': (1, 1, 'USER', cmd_userpeer)
 +  'userpeer': (1, 1, 'USER', cmd_userpeer),
 +  'sabotage': (1, 1, 'PEER', cmd_sabotage)
  })]
  
  if __name__ == '__main__':
    opts = parse_options()
 +  OS.environ['TRIPESOCK'] = opts.tripesock
    T.runservices(opts.tripesock, service_info,
                  init = init, setup = setup,
                  daemon = opts.daemon)
diff --combined svc/tripe-ifup.in
@@@ -46,16 -46,11 +46,16 @@@ f
  peer=$1 ifname=$2 family=$3; shift 3
  
  ## Parse the address family.
 +case "$family" in
 +  INET) ipsz=20 ;;
 +  INET6) ipsz=40 ;;
 +  *) echo >&2 "$0: unknown address family $family"; exit 1 ;;
 +esac
  case "$family,$#" in
 -  INET,1) addr=$1 port=4070 ;;
 -  INET,2) addr=$1 port=$2 ;;
 -  INET,*) echo >&2 "$0: bad INET address"; exit 1 ;;
 -  *)      echo >&2 "$0: unknown address family $family"; exit 1 ;;
 +  INET,1 | INET6,1) addr=$1 port=4070 ;;
 +  INET,2 | INET6,2) addr=$1 port=$2 ;;
 +  INET,* | INET6,*) echo >&2 "$0: bad $family address"; exit 1 ;;
 +  *) echo >&2 "$0: unknown address family $family"; exit 1 ;;
  esac
  
  ###--------------------------------------------------------------------------
@@@ -124,9 -119,6 +124,6 @@@ case $have6,$# i
        try ip addr add "$a" dev "$ifname"
        haveaddr6=t
      done
-     case ${r6addr+set} in
-       set) try ip route add $r6addr proto static dev "$ifname" ;;
-     esac
      ;;
  esac
  
@@@ -142,7 -134,7 +139,7 @@@ case $haveaddr4,$haveaddr6 i
        mtu=$P_MTU;;
        *)
        pathmtu=$(pathmtu "$addr")
 -      mtu=$(expr "$pathmtu" - 29 - $A_BULK_OVERHEAD)
 +      mtu=$(( $pathmtu - $ipsz - 9 - $A_BULK_OVERHEAD ))
        ;;
      esac
      try ip link set dev "$ifname" up mtu "$mtu"
  esac
  
  ###--------------------------------------------------------------------------
+ ### Set the peer IPv6 address if any.
+ ## IPv6 point-to-point links seem broken in Linux.  Attach the local and
+ ## remote addresses by hand.
+ set -- $l6addr
+ case $have6,$#,${r6addr+set} in
+   t,[1-9]*,set)
+     try ip route add $r6addr proto static dev "$ifname"
+     ;;
+ esac
+ ###--------------------------------------------------------------------------
  ### Set up routing.
  
  ## Split the routes into v4 and v6 lists.