wireshark/: Replace ancient dissector with a new one written in Lua.
authorMark Wooding <mdw@distorted.org.uk>
Mon, 10 Jul 2017 10:12:11 +0000 (11:12 +0100)
committerMark Wooding <mdw@distorted.org.uk>
Mon, 10 Jul 2017 10:12:11 +0000 (11:12 +0100)
This is mostly a good thing.

  + It work with both Wireshark 1.11 and 2.  The old C code would have
    to choose between the two, and I couldn't easily tell how shiny a
    version of Wireshark I'd be pinning my colours to.

  + It actually dissects the TrIPE protocol as it currently is,
    including all of the group element encodings and bulk crypto
    transforms.

  + It'll be relatively easy to /keep/ the new dissector up-to-date
    relative to protocol changes.

  - It won't run as quickly -- but Lua has a reputation for being quite
    quick, and I'm not expecting to stress it very much.

In theory, I'd be able to put this in an architecture-independent
package, which would greatly shorten cross-build times.  Alas, the
plugin directory encodes the architecture name, so it'll have to be
built separately for each architecture anyway.

Lots of changes:

  * Eliminate the old `packet-tripe.c' dissector.  Add the new one.  Get
    the build system to install it in the right place.

  * Include a (rather shoddy) script for running `tripe' and capturing
    the conversation with `tshark', so I can test the dissector against
    it; and some small example captures.  This might even turn into a
    proper test at some point, but for now it's just something I can do
    by hand.

  * Hack the `configure' script not to need all of the C compile-time
    machinery for building Wireshark plugins.

13 files changed:
configure.ac
debian/control
debian/rules
wireshark/Makefile.am
wireshark/cap
wireshark/cap.dh [new file with mode: 0644]
wireshark/cap.ec [new file with mode: 0644]
wireshark/cap.x25519 [new file with mode: 0644]
wireshark/cap.x448 [new file with mode: 0644]
wireshark/capture-session [new file with mode: 0755]
wireshark/keyring [new file with mode: 0644]
wireshark/packet-tripe.c [deleted file]
wireshark/tripe.lua [new file with mode: 0644]

index f3b75ea..33ce67d 100644 (file)
@@ -210,17 +210,7 @@ AC_ARG_WITH([wireshark],
            esac],
            [wantshark=yes mustshark=no])
 
-case "$wantshark" in
-  yes)
-    PKG_CHECK_MODULES([WIRESHARK], [wireshark >= 1.12.1],
-       [haveshark=yes], [haveshark=no])
-    ;;
-  *)
-    haveshark=no
-    ;;
-esac
-
-case "$haveshark,$wireshark_plugindir" in
+case "$wantshark,$wireshark_plugindir" in
   yes,unknown)
     AC_CACHE_CHECK([where to put Wireshark plugins],
       [mdw_cv_wireshark_plugin_dir], [
@@ -249,11 +239,9 @@ case "$haveshark,$wireshark_plugindir" in
        ;;
     esac
     ;;
-esac
-
-dnl If we're still interested, find Glib.
-case "$haveshark" in
-  yes) AM_PATH_GLIB_2_0([2.4.0], [], [haveshark=false], [gmodule]) ;;
+  no,*)
+    haveshark=no
+    ;;
 esac
 
 case "$haveshark,$needshark" in
@@ -261,8 +249,6 @@ case "$haveshark,$needshark" in
     AC_MSG_ERROR([failed to configure Wireshark plugin])
     ;;
   yes,*)
-    WIRESHARK_CFLAGS="$GLIB_CFLAGS $WIRESHARK_CFLAGS"
-    AC_SUBST(WIRESHARK_CFLAGS)
     AC_SUBST(wireshark_plugindir)
     ;;
 esac
index 2404759..17a7451 100644 (file)
@@ -56,7 +56,7 @@ Description: Trivial IP Encryption: a simple virtual private network
 
 Package: tripe-wireshark
 Architecture: any
-Depends: wireshark-common (= ${tripe:Wireshark-Version}), ${shlibs:Depends}
+Depends: wireshark-common (= ${tripe:Wireshark-Version})
 Description: Trivial IP Encryption: a simple virtual private network
  TrIPE is a simple VPN protocol.  It uses cryptography to ensure secrecy
  and authenticity of packets it sends and receives.
index 01625b2..b45a6be 100755 (executable)
@@ -28,9 +28,6 @@ dh-gencontrol-hook::
                sed -n 's/^Version: */tripe:Wireshark-Version=/p' \
                >> debian/tripe-wireshark.substvars
 
-OVERRIDES              += shlibdeps
-dh_shlibdeps_OPTS      += -Xwireshark/plugins
-
 ###--------------------------------------------------------------------------
 ### The startup script and related machinery.
 
index 6342178..36866dd 100644 (file)
@@ -28,12 +28,7 @@ include $(top_srcdir)/vars.am
 ###--------------------------------------------------------------------------
 ### Wireshark plugin.
 
-AM_CFLAGS                      += $(WIRESHARK_CFLAGS)
-LIBS                            =
-
-wireshark_plugin_LTLIBRARIES    = tripe.la
-
-tripe_la_LDFLAGS                = -module -avoid-version
-tripe_la_SOURCES                = packet-tripe.c
+wireshark_plugin_DATA   = tripe.lua
+EXTRA_DIST             += tripe.lua
 
 ###----- That's all, folks --------------------------------------------------
index 1bd4c45..8e5cdc6 100644 (file)
Binary files a/wireshark/cap and b/wireshark/cap differ
diff --git a/wireshark/cap.dh b/wireshark/cap.dh
new file mode 100644 (file)
index 0000000..fcf40e6
Binary files /dev/null and b/wireshark/cap.dh differ
diff --git a/wireshark/cap.ec b/wireshark/cap.ec
new file mode 100644 (file)
index 0000000..a9ecf1d
Binary files /dev/null and b/wireshark/cap.ec differ
diff --git a/wireshark/cap.x25519 b/wireshark/cap.x25519
new file mode 100644 (file)
index 0000000..6b65c11
Binary files /dev/null and b/wireshark/cap.x25519 differ
diff --git a/wireshark/cap.x448 b/wireshark/cap.x448
new file mode 100644 (file)
index 0000000..0dd5d94
Binary files /dev/null and b/wireshark/cap.x448 differ
diff --git a/wireshark/capture-session b/wireshark/capture-session
new file mode 100755 (executable)
index 0000000..4b5d81a
--- /dev/null
@@ -0,0 +1,77 @@
+#! /bin/sh -e
+###
+### A simple script for capturing TrIPE sessions, for testing the Wireshark
+### dissector.
+
+ty=${1?ty} param=${2-$ty}
+tripe=${TRIPE-tripe}
+rm -rf captmp
+mkdir captmp
+cd captmp
+
+cp ../keyring .
+for i in alice bob; do
+  key add -eforever -a$ty -t$i -pparam-$param tripe
+done
+cp keyring keyring.pub
+
+for i in alice bob; do
+  mkfifo $i.in $i.out
+  TRIPE_SLIPIF=/usr/bin/tripe-uslip \
+    $tripe -d. -as.$i -F -nslip -t$i -p0 <$i.in >$i.out 2>$i.err&
+done
+exec 3>alice.in 4<alice.out; alice_in=3 alice_out=4
+exec 5>bob.in 6<bob.out; bob_in=5 bob_out=6
+
+docmd () {
+  who=$1; shift
+  eval in=\$${who}_in out=\$${who}_out
+  echo "$*" >&$in
+  while read tag tail; do
+    : "$tag $tail"
+    case $tag in
+      INFO) echo $tail ;;
+      FAIL) echo >&2 "command \`$*' failed: $tail"; exit 10 ;;
+      OK) break ;;
+    esac
+  done <&$out
+}
+
+await () {
+  who=$1
+  eval out=\$${who}_out
+  while read tag kind rest; do
+    : "$tag $kind $rest"
+    case $tag,$kind in
+      NOTE,KXDONE) break ;;
+    esac
+  done <&$out
+}
+
+docmd alice watch n-tw
+docmd bob watch n-tw
+
+p_alice=$(docmd alice port)
+p_bob=$(docmd bob port)
+
+tshark -ilo -f"udp port $p_alice or udp port $p_bob" \
+       -w../cap.$param& shark=$!
+pause
+
+docmd alice add -cork bob 127.0.0.1 $p_bob
+c=$(docmd bob getchal)
+docmd alice greet bob $c
+docmd bob add alice 127.0.0.1 $p_alice
+await alice& walice=$!
+await bob& wbob=$!
+wait $walice
+wait $wbob
+
+echo ping | tripe-uslip -p bob
+x=$(tripe-uslip -g alice)
+
+pause
+
+exec 3>&- 4>&- 5>&- 6>&-
+kill $shark
+wait
diff --git a/wireshark/keyring b/wireshark/keyring
new file mode 100644 (file)
index 0000000..84b6133
--- /dev/null
@@ -0,0 +1,4 @@
+86249683:tripe-param:param-ec struct:[curve=string,shared:nist%2dp256] forever forever hash=sha256&kx-group=ec&cipher=rijndael-cbc&bulk=iiv
+fae23925:tripe-param:param-x25519 string,shared:%2e forever forever hash=sha256&kx-group=x25519&cipher=salsa20&bulk=naclbox
+a0fafc6b:tripe-param:param-dh struct:[p=integer,shared:635937223373484887991140560420669529960468634418212194527199243018802509220923645480563049852948379631872315326364181219863391536284352632127476573990043945919830000350264391397346335414535975554209931971547284463207275833747975949070870172306582775948778222246682185331862354083029804303222690541851195763223409045953191584611635790362191424339883737168342809190665629632289528654983812904611647818546832860081714363504512892885600580448186423533085059295328609139522690627823102925150246378111221377628009667319528150747320084312110336288028683700603719073769314245952464113622780032073817850603131463761017417455806035107984621889773217138911836476354442521466189648565925624512100087534918369360007697883847802211797655614684090408428715299537509886614640500137409891381649298835563011151796284848165977247286439820050405175697064122759870661294968492275655767983096727976056361878300352234381528837008128941375285251387,g=integer,shared:293025651734044707050351995205142657657141955131408425452312245943331353163428637443799381119408990271553007202915223601376791534187122912295000993316207089139736477148501917083144416322685925261624358336253378688806309805096283386698409139605996997308008266491782991490291726095895573563845937760385377670846794792328581579165150512160132493375389995617711312966986681865792948443127326586291576561351005982500369077653940283008076332825908419690031303502192009860824429031526378047535596212801179578879818504990773143072529887215206211778546705082334239130823956177702608427222251531070457604183947814624087379251368161182770323100074409926426428526795865215674068539423364010352920515052672366811052717283396264950182005583498061313136436293898863077084485762289185337928869086731032780517039420605615746259170222257438835029958473402842467194409561307509136616954789698181252705567549441508247668319257495617674881793988,q=integer,shared:89271282791461757245617785540129155142212959423277129581512553253419075634703] forever forever hash=sha256&kx-group=dh&cipher=twofish-cbc&bulk=v0
+8f879aef:tripe-param:param-x448 string,shared:%2e forever forever mgf=shake256-xof&hash=shake256&kx-group=x448&cipher=chacha20&bulk=naclbox
diff --git a/wireshark/packet-tripe.c b/wireshark/packet-tripe.c
deleted file mode 100644 (file)
index 28bfc83..0000000
+++ /dev/null
@@ -1,479 +0,0 @@
-/* -*-c-*-
- *
- * TrIPE protocol dissector for Wireshark
- *
- * (c) 2003 Straylight/Edgeware
- */
-
-/*----- Licensing notice --------------------------------------------------*
- *
- * 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 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.
- */
-
-/*----- Header files ------------------------------------------------------*/
-
-#include "config.h"
-
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-
-#include <netinet/in.h>
-
-#include <glib.h>
-#include <gmodule.h>
-#include <wireshark/config.h>
-#include <wireshark/epan/packet.h>
-#include <wireshark/epan/prefs.h>
-
-#include "protocol.h"
-
-/*----- Static variables --------------------------------------------------*/
-
-static int proto_tripe = -1;
-
-static guint hashsz = 20, tagsz = 10, ivsz = 8;
-
-typedef struct hfmp {
-  int hf, hf_len, hf_val, tt;
-} hfmp;
-typedef struct hfge {
-  int hf, hf_len, hf_val, hfx_len, hfx_val, hfy_len, hfy_val, tt;
-} hfge;
-
-static int hf_tripe_cat = -1;
-static int hf_tripe_packet_type = -1;
-static int hf_tripe_ct = -1;
-static int hf_tripe_ct_seq = -1;
-static int hf_tripe_ct_iv = -1;
-static int hf_tripe_ct_ct = -1;
-static int hf_tripe_ct_tag = -1;
-static int hf_tripe_misc_type = -1;
-static int hf_tripe_misc_payload = -1;
-static int hf_tripe_kx_type = -1;
-static hfge hf_tripe_kx_mychal = { -1, -1, -1, -1, -1, -1, -1, -1 };
-static int hf_tripe_kx_mycookie = -1;
-static int hf_tripe_kx_yourcookie = -1;
-static hfmp hf_tripe_kx_check = { -1, -1, -1, -1 };
-static int hf_tripe_huh = -1;
-
-static int tt_tripe = -1;
-static int tt_tripe_ct = -1;
-
-G_MODULE_EXPORT const gchar version[] = VERSION;
-
-/*----- Main code ---------------------------------------------------------*/
-
-static void prefcb(void) { }
-
-static gint gethash(proto_tree *tt, int hf, tvbuff_t *b, gint off)
-{
-  proto_tree_add_item(tt, hf, b, off, hashsz, FALSE);
-  return (off + hashsz);
-}
-
-static gint getmp(proto_tree *tt, const hfmp *hf, tvbuff_t *b, gint off)
-{
-  guint16 len = tvb_get_ntohs(b, off);
-  proto_item *ti = proto_tree_add_item(tt, hf->hf, b, off, len + 2, FALSE);
-  tt = proto_item_add_subtree(ti, hf->tt);
-  proto_tree_add_item(tt, hf->hf_len, b, off, 2, FALSE);
-  proto_tree_add_item(tt, hf->hf_val, b, off + 2, len, FALSE);
-  return (off + 2 + len);
-}
-
-static gint getge(proto_tree *tt, const hfge *hf, tvbuff_t *b, gint off)
-{
-  guint16 len = tvb_get_ntohs(b, off), len2;
-  guint r;
-  proto_item *ti;
-  r = tvb_length_remaining(b, off);
-  if (r < 4 + len ||
-      (len2 = tvb_get_ntohs(b, off + 2 + len), r < 4 + len + len2)) {
-    ti = proto_tree_add_item(tt, hf->hf, b, off, len + 2, FALSE);
-    tt = proto_item_add_subtree(ti, hf->tt);
-    proto_tree_add_item(tt, hf->hf_len, b, off, 2, FALSE);
-    proto_tree_add_item(tt, hf->hf_val, b, off + 2, len, FALSE);
-    r = off + len + 2;
-  } else {
-    ti = proto_tree_add_item(tt, hf->hf, b, off, len + len2 + 4, FALSE);
-    tt = proto_item_add_subtree(ti, hf->tt);
-    proto_tree_add_item(tt, hf->hfx_len, b, off, 2, FALSE);
-    proto_tree_add_item(tt, hf->hfx_val, b, off + 2, len, FALSE);
-    proto_tree_add_item(tt, hf->hfy_len, b, off + 2 + len, 2, FALSE);
-    proto_tree_add_item(tt, hf->hfy_val, b, off + 4 + len, len2, FALSE);
-    r = off + len + len2 + 4;
-  }
-  return (r);
-}
-
-static void dissect_tripe(tvbuff_t *b, packet_info *p, proto_tree *t)
-{
-  proto_item *ti;
-  proto_tree *tt;
-  guint8 ty;
-  gint off = 0;
-  guint32 seq;
-
-  /* --- Initialize the summary cells --- */
-
-  col_set_str(p->cinfo, COL_PROTOCOL, "TrIPE");
-  col_clear(p->cinfo, COL_INFO);
-  ty = tvb_get_guint8(b, 0);
-  col_clear(p->cinfo, COL_INFO);
-  switch (ty & MSG_CATMASK) {
-    case MSG_PACKET:
-      switch (ty & MSG_TYPEMASK) {
-       case 0:
-         col_set_str(p->cinfo, COL_INFO, "Packet data");
-         break;
-       default:
-         col_add_fstr(p->cinfo, COL_INFO,
-                      "Packet data, unknown type code %u",
-                      ty & MSG_TYPEMASK);
-         break;
-      }
-      break;
-    case MSG_KEYEXCH:
-      switch (ty & MSG_TYPEMASK) {
-       case KX_PRECHAL:
-         col_set_str(p->cinfo, COL_INFO, "Key exchange, prechallenge");
-         break;
-       case KX_CHAL:
-         col_set_str(p->cinfo, COL_INFO, "Key exchange, challenge");
-         break;
-       case KX_REPLY:
-         col_set_str(p->cinfo, COL_INFO, "Key exchange, reply");
-         break;
-       case KX_SWITCH:
-         col_set_str(p->cinfo, COL_INFO, "Key exchange, switch request");
-         break;
-       case KX_SWITCHOK:
-         col_set_str(p->cinfo, COL_INFO, "Key exchange, switch response");
-         break;
-       default:
-         col_add_fstr(p->cinfo, COL_INFO,
-                      "Key exchange, unknown type code %u",
-                      ty & MSG_TYPEMASK);
-         break;
-      }
-      break;
-    case MSG_MISC:
-      switch (ty & MSG_TYPEMASK) {
-       case MISC_NOP:
-         col_set_str(p->cinfo, COL_INFO, "Miscellaneous, no-operation");
-         break;
-       case MISC_PING:
-         col_set_str(p->cinfo, COL_INFO, "Miscellaneous, transport ping");
-         break;
-       case MISC_PONG:
-         col_set_str(p->cinfo, COL_INFO,
-                     "Miscellaneous, transport ping reply");
-         break;
-       case MISC_EPING:
-         col_set_str(p->cinfo, COL_INFO, "Miscellaneous, encrypted ping");
-         break;
-       case MISC_EPONG:
-         col_set_str(p->cinfo, COL_INFO,
-                     "Miscellaneous, encrypted ping reply");
-         break;
-       case MISC_GREET:
-         col_set_str(p->cinfo, COL_INFO,
-                     "Miscellaneous, greeting");
-         break;
-       default:
-         col_add_fstr(p->cinfo, COL_INFO,
-                      "Miscellaneous, unknown type code %u",
-                      ty & MSG_TYPEMASK);
-         break;
-      }
-      break;
-    default:
-      col_add_fstr(p->cinfo, COL_INFO,
-                  "Unknown category code %u, unknown type code %u",
-                  ty & MSG_CATMASK, ty & MSG_TYPEMASK);
-      break;
-  }
-
-  /* --- Fill in the tree --- */
-
-  if (t) {
-    ti = proto_tree_add_item(t, proto_tripe, b, 0, -1, FALSE);
-    tt = proto_item_add_subtree(ti, tt_tripe);
-
-    proto_tree_add_item(tt, hf_tripe_cat, b, 0, 1, FALSE);
-
-    off = 1;
-    switch (ty & MSG_CATMASK) {
-      case MSG_PACKET:
-       proto_tree_add_item(tt, hf_tripe_packet_type, b, 0, 1, FALSE);
-       switch (ty & MSG_TYPEMASK) {
-         case 0:
-           goto ct;
-         default:
-           goto huh;
-       }
-       break;
-      case MSG_KEYEXCH:
-       proto_tree_add_item(tt, hf_tripe_kx_type, b, 0, 1, FALSE);
-       switch (ty & MSG_TYPEMASK) {
-         case KX_PRECHAL:
-           off = getge(tt, &hf_tripe_kx_mychal, b, off);
-           goto tail;
-         case KX_CHAL:
-           off = getge(tt, &hf_tripe_kx_mychal, b, off);
-           off = gethash(tt, hf_tripe_kx_yourcookie, b, off);
-           off = getmp(tt, &hf_tripe_kx_check, b, off);
-           goto tail;
-         case KX_REPLY:
-           off = getge(tt, &hf_tripe_kx_mychal, b, off);
-           off = gethash(tt, hf_tripe_kx_yourcookie, b, off);
-           off = getmp(tt, &hf_tripe_kx_check, b, off);
-           goto ct;
-         case KX_SWITCH:
-           off = gethash(tt, hf_tripe_kx_mycookie, b, off);
-           off = gethash(tt, hf_tripe_kx_yourcookie, b, off);
-           goto ct;
-         case KX_SWITCHOK:
-           goto ct;
-         default:
-           goto huh;
-       }
-       break;
-      case MSG_MISC:
-       proto_tree_add_item(tt, hf_tripe_misc_type, b, 0, 1, FALSE);
-       switch (ty & MSG_TYPEMASK) {
-         case MISC_NOP:
-         case MISC_PING:
-         case MISC_PONG:
-         case MISC_GREET:
-           proto_tree_add_item(tt, hf_tripe_misc_payload,
-                               b, off, -1, FALSE);
-           goto done;
-         case MISC_EPING:
-         case MISC_EPONG:
-           goto ct;
-         default:
-           goto huh;
-       }
-       break;
-      default:
-       goto huh;
-    }
-  tail:
-    if (tvb_offset_exists(b, off))
-      goto huh;
-    goto done;
-  huh:
-    proto_tree_add_item(tt, hf_tripe_huh, b, off, -1, FALSE);
-    goto done;
-  ct:
-    ti = proto_tree_add_item(tt, hf_tripe_ct, b, off, -1, FALSE);
-    seq = tvb_get_ntohl(b, off + tagsz);
-    proto_item_set_text(ti, "Encrypted ciphertext (sequence number %lu)",
-                       (unsigned long)seq);
-    tt = proto_item_add_subtree(ti, tt_tripe_ct);
-    if (tagsz) {
-      proto_tree_add_item(tt, hf_tripe_ct_tag, b, off, tagsz, FALSE);
-      off += tagsz;
-    }
-    proto_tree_add_item(tt, hf_tripe_ct_seq, b, off, 4, FALSE);
-    off += 4;
-    if (ivsz) {
-      proto_tree_add_item(tt, hf_tripe_ct_iv, b, off, ivsz, FALSE);
-      off += ivsz;
-    }
-    proto_tree_add_item(ti, hf_tripe_ct_ct, b, off, -1, FALSE);
-    goto done;
-  done:;
-  }
-}
-
-void proto_register_tripe(void)
-{
-  module_t *mod;
-
-  static value_string vs_kxtype[] = {
-    { KX_PRECHAL,      "KX_PRECHAL (prechallenge)" },
-    { KX_CHAL,         "KX_CHAL (challenge)" },
-    { KX_REPLY,                "KX_REPLY (reply)" },
-    { KX_SWITCH,       "KX_SWITCH (switch request)" },
-    { KX_SWITCHOK,     "KX_SWITCHOK (switch response)" },
-    { 0,               0 }
-  };
-
-  static hf_register_info hfs[] = {
-    { &hf_tripe_cat, {
-      "Message category", "tripe.cat",
-      FT_UINT8, BASE_HEX, 0, MSG_CATMASK
-    } },
-    { &hf_tripe_packet_type, {
-      "Packet message type", "tripe.packet.type",
-      FT_UINT8, BASE_HEX, 0, MSG_TYPEMASK,
-      "This is the TrIPE packet type subcode."
-    } },
-    { &hf_tripe_ct, {
-      "Encrypted ciphertext", "tripe.ct",
-      FT_BYTES, BASE_NONE, 0, 0,
-      "This is an encrypted message."
-    } },
-    { &hf_tripe_ct_seq, {
-      "Ciphertext sequence number", "tripe.ct.seq",
-      FT_UINT32, BASE_DEC, 0, 0,
-      "This is the unique sequence number for the ciphertext."
-    } },
-    { &hf_tripe_ct_iv, {
-      "Ciphertext initialization vector", "tripe.ct.iv",
-      FT_BYTES, BASE_NONE, 0, 0,
-      "This is the initialization vector used for the actual encryption."
-    } },
-    { &hf_tripe_ct_ct, {
-      "Actual encrypted data", "tripe.ct.ct",
-      FT_BYTES, BASE_NONE, 0, 0,
-      "This is the encrypted message.  Reading it ought to be hard."
-    } },
-    { &hf_tripe_ct_tag, {
-      "Message authentication code", "tripe.ct.tag",
-      FT_BYTES, BASE_NONE, 0, 0,
-      "This is the message authentication code tag for the ciphertext."
-    } },
-    { &hf_tripe_misc_type, {
-      "Miscellaneous message type", "tripe.misc.type",
-      FT_UINT8, BASE_HEX, 0, MSG_TYPEMASK,
-      "This is the TrIPE miscellaneous message type subcode."
-    } },
-    { &hf_tripe_misc_payload, {
-      "Miscellaneous message payload", "tripe.misc.payload",
-      FT_BYTES, BASE_NONE, 0, 0,
-      "This is the miscellaneous message payload."
-    } },
-    { &hf_tripe_kx_type, {
-      "Key-exchange message type", "tripe.kx.type",
-      FT_UINT8, BASE_HEX, vs_kxtype, MSG_TYPEMASK,
-      "This is the TrIPE key-exchange type subcode."
-    } },
-    { &hf_tripe_kx_mychal.hf, {
-      "Sender's challenge data", "tripe.kx.mychal",
-      FT_BYTES, BASE_NONE, 0, 0,
-      "This is the sender's challenge."
-    } },
-    { &hf_tripe_kx_mychal.hf_len, {
-      "Challenge length", "tripe.kx.mychal.len",
-      FT_UINT16, BASE_DEC, 0, 0,
-      "This is the length of the sender's challenge."
-    } },
-    { &hf_tripe_kx_mychal.hf_val, {
-      "Challenge", "tripe.kx.mychal.val",
-      FT_BYTES, BASE_NONE, 0, 0,
-      "This is the value of the sender's challenge."
-    } },
-    { &hf_tripe_kx_mychal.hfx_len, {
-      "Challenge x length", "tripe.kx.mychal.x.len",
-      FT_UINT16, BASE_DEC, 0, 0,
-      "This is the length of the sender's challenge x-coordinate."
-    } },
-    { &hf_tripe_kx_mychal.hfx_val, {
-      "Challenge x value", "tripe.kx.mychal.x.val",
-      FT_BYTES, BASE_NONE, 0, 0,
-      "This is the value of the sender's challenge x-coordinate."
-    } },
-    { &hf_tripe_kx_mychal.hfy_len, {
-      "Challenge y length", "tripe.kx.mychal.y.len",
-      FT_UINT16, BASE_DEC, 0, 0,
-      "This is the length of the sender's challenge x-coordinate."
-    } },
-    { &hf_tripe_kx_mychal.hfy_val, {
-      "Challenge y value", "tripe.kx.mychal.y.val",
-      FT_BYTES, BASE_NONE, 0, 0,
-      "This is the value of the sender's challenge x-coordinate."
-    } },
-    { &hf_tripe_kx_mycookie, {
-      "Sender's hashed cookie", "tripe.kx.mycookie",
-      FT_BYTES, BASE_NONE, 0, 0,
-      "This is the hash of the sender's challenge."
-    } },
-    { &hf_tripe_kx_yourcookie, {
-      "Recipient's hashed cookie", "tripe.kx.yourcookie",
-      FT_BYTES, BASE_NONE, 0, 0,
-      "This is the hash of the recipient's challenge."
-    } },
-    { &hf_tripe_kx_check.hf, {
-      "Challenge check-value", "tripe.kx.check",
-      FT_BYTES, BASE_NONE, 0, 0,
-      "This is an encrypted check-value which proves that the sender "
-      "knows the answer to the challenge, and that it is therefore honest."
-    } },
-    { &hf_tripe_kx_check.hf_len, {
-      "Check-value length", "tripe.kx.check.len",
-      FT_UINT16, BASE_DEC, 0, 0,
-      "This is the length of the encrypted check-value."
-    } },
-    { &hf_tripe_kx_check.hf_val, {
-      "Check-value data", "tripe.kx.check.val",
-      FT_BYTES, BASE_NONE, 0, 0,
-      "This is the actual encrypted check-value."
-    } },
-    { &hf_tripe_huh, {
-      "Unknown data", "tripe.huh",
-      FT_BYTES, BASE_NONE, 0, 0,
-      "I don't know what's meant to appear here."
-    } },
-  };
-
-  static gint *tts[] = {
-    &tt_tripe,
-    &tt_tripe_ct,
-    &hf_tripe_kx_mychal.tt,
-    &hf_tripe_kx_check.tt,
-  };
-
-  proto_tripe = proto_register_protocol("TrIPE", "TrIPE", "tripe");
-  proto_register_field_array(proto_tripe, hfs, array_length(hfs));
-  proto_register_subtree_array(tts, array_length(tts));
-
-  mod = prefs_register_protocol(proto_tripe, prefcb);
-  prefs_register_uint_preference(mod, "hashsz", "Hash length",
-                                "hash function output length (in octets)",
-                                10, &hashsz);
-  prefs_register_uint_preference(mod, "tagsz", "MAC tag length",
-                                "MAC tag length (in octets)", 10, &tagsz);
-  prefs_register_uint_preference(mod, "ivsz", "IV length",
-                                "block cipher initialization vector length"
-                                " (in octets)",
-                                10, &ivsz);
-}
-
-void proto_reg_handoff_tripe(void)
-{
-  dissector_handle_t dh;
-
-  dh = create_dissector_handle(dissect_tripe, proto_tripe);
-  dissector_add_uint("udp.port", TRIPE_PORT, dh);
-}
-
-G_MODULE_EXPORT void plugin_reg_handoff(void)
-{
-  proto_reg_handoff_tripe();
-}
-
-G_MODULE_EXPORT void plugin_register(void)
-{
-  if (proto_tripe == -1)
-    proto_register_tripe();
-}
-
-/*----- That's all, folks -------------------------------------------------*/
diff --git a/wireshark/tripe.lua b/wireshark/tripe.lua
new file mode 100644 (file)
index 0000000..fcc948d
--- /dev/null
@@ -0,0 +1,582 @@
+--- -*-lua-*-
+---
+--- Wireshark protocol dissector for TrIPE
+---
+--- (c) 2017 Straylight/Edgeware
+---
+
+-------- Licensing notice ---------------------------------------------------
+---
+--- 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 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.
+
+local tripe = Proto("tripe", "TrIPE VPN")
+
+-----------------------------------------------------------------------------
+--- Configuration handling.
+
+local CONFIG = {
+  -- Information about the configuration variables.  This table, when it's
+  -- set up, maps the internal names, which are used to refer to
+  -- configuration variables in the rest of this code, to a little structure:
+  --
+  --   * `var' names the variable, and is the usual key for lookups;
+  --
+  --   * `name' is the label used in the dialogue box;
+  --
+  --   * `type' is the type of variable, currently either `enum' or `int';
+  --
+  --   * `descr' is a longer (but generally fairly useless) description for
+  --    use in a tooltip;
+  --
+  --   * `allowed' is a sequence of allowed values for an `enum' variable;
+  --    and
+  --
+  --   * `min' and `max' are the limits on permitted values for an `int'
+  --    variable (and may be omitted).
+  --
+  -- More slots are added at runtime:
+  --
+  --   * `map' is a table mapping string values to their integer indices, as
+  --     stored in Wireshark's preferences database.
+  --
+  -- Initially, though, the table is given as a sequence, so that the
+  -- preferences can be populated in a consistent (and approximately logical)
+  -- order.
+
+  { var = "bulk", name = "Bulk transform",
+    type = "enum", allowed = { "v0", "iiv", "naclbox" },
+    descr = "Bulk cryptographic transform", default = "v0" },
+  { var = "hashsz", name = "Hash length", type = "int", min = 0,
+    descr = "Hash length (bytes)", default = 20 },
+  { var = "tagsz", name = "Tag length", type = "int", min = 0,
+    descr = "Authentication tag length (bytes)", default = 10 },
+  { var = "ivsz", name = "IV length", type = "int", min = 0,
+    descr = "Initialization vector length (bytes)", default = 8 },
+  { var = "kx", name = "Key-exchange group",
+    type = "enum", allowed = { "dh", "ec", "x25519", "x448" },
+    descr = "Key-exchange group type", default = "dh" },
+  { var = "scsz", name = "Scalar length", type = "int", min = 1,
+    descr = "Scalar field-element length (bytes)", default = 32 },
+}
+
+local C = { } -- The working values of the configuration variables.
+
+local function set_config(k, v)
+  -- Set configuration variable K to the value V.
+  --
+  -- K is a string naming the variable to set.  V is the new value, which may
+  -- be a string or a number.
+  --
+  -- For `int' variables, V is converted to a number if necessary, and then
+  -- checked against the permitted bounds.
+  --
+  -- For `enum' variables, things are more complicated.  If V is a string,
+  -- it's checked against the permitted values.  If V is a number, it's
+  -- converted back into the corresponding string.
+
+  local info = CONFIG[k]
+
+  if info == nil then error("unknown config key `" .. k .. "'") end
+
+  if info.type == "enum" then
+    if type(v) == "number" then
+      local t = info.allowed[v]
+      if t == nil then
+       error(string.format("bad index %d for `%s'", n, k))
+      end
+      v = t
+    else
+      if info.map[v] == nil then
+       error(string.format("bad value `%s' for `%s'", v, k))
+      end
+    end
+
+  elseif info.type == "int" then
+    local n = tonumber(v)
+    if n == nil then error("bad number `" .. v .. "'") end
+    if n ~= math.floor(n) then
+      error("value `" .. v .. "' is not an integer")
+    end
+    if (info.min ~= nil and n < info.min) or
+       (info.max ~= nil and n > info.max)
+    then
+      error(string.format("value %d out of range for `%s'", n, k))
+    end
+    v = n
+  end
+
+  C[k] = v
+end
+
+-- Set up the configuration information.  Configure preferences objects on
+-- the dissector.  For `enum' variables, build the `map' slots.
+for i, v in ipairs(CONFIG) do
+  local k = v.var
+  CONFIG[k] = v
+  if v.type == "enum" then
+    local tab = { }
+    v.map = { }
+    for i, t in pairs(v.allowed) do
+      v.map[t] = i
+      tab[i] = { i, t, i }
+    end
+    tripe.prefs[k] = Pref.enum(v.name, v.map[v.default], v.descr, tab)
+  elseif v.type == "int" then
+    tripe.prefs[k] = Pref.uint(v.name, v.default, v.descr)
+  end
+end
+
+local function prefs_changed()
+  -- Notice that the preferences have been changed and update `C'.
+
+  for k, _ in pairs(CONFIG) do
+    if type(k) == "string" then set_config(k, tripe.prefs[k]) end
+  end
+end
+tripe.prefs_changed = prefs_changed
+
+-- Populate the configuration table from the stored preferences or their
+-- default values.
+prefs_changed()
+
+-- Now work through arguments passed in on the command line.  Annoyingly,
+-- while one can set preferences on the Wireshark command line, these are
+-- done /before/ Lua scripts are loaded, so the silly thing thinks the
+-- preference slots don't exist.  So we have to do it a different way.
+for _, arg in ipairs({...}) do
+  local k, v = arg:match("(.+)=(.+)")
+  if k == nil or v == nil then error("bad option syntax `" .. arg .. "'") end
+  se_config(k, v)
+end
+
+-----------------------------------------------------------------------------
+--- Protocol dissection primitives.
+
+local PF = { } -- The table of protocol fields, filled in later.
+
+-- The `dissect_*' functions follow a common protocol.  They parse a thing
+-- from a packet buffer BUF, of size SZ, starting from POS, and store
+-- interesting things in a given TREE; when they're done, they return the
+-- updated index where the next interesting thing might be.  As a result,
+-- it's usually a simple matter to parse a packet by invoking the appropriate
+-- primitive dissectors in the right order.
+
+local function dissect_wtf(buf, tree, pos, sz)
+  -- If POS is not at the end of the buffer, note that there's unexpected
+  -- stuff in the packet.
+
+  if pos < sz then tree:add(PF["tripe.wtf"], buf(pos, sz - pos)) end
+  return sz
+end
+
+-- Dissect a ciphertext of some particular kind.
+local dissect_ct = { }
+function dissect_ct.naclbox(buf, tree, pos, sz)
+  tree:add(PF["tripe.ciphertext.tag"], buf(pos, 16)); pos = pos + 16
+  tree:add(PF["tripe.ciphertext.seq"], buf(pos, 4)); pos = pos + 4
+  tree:add(PF["tripe.ciphertext.body"], buf(pos, sz - pos))
+end
+function dissect_ct.iiv(buf, tree, pos, sz)
+  tree:add(PF["tripe.ciphertext.tag"], buf(pos, C.tagsz)); pos = pos + C.tagsz
+  tree:add(PF["tripe.ciphertext.seq"], buf(pos, 4)); pos = pos + 4
+  tree:add(PF["tripe.ciphertext.body"], buf(pos, sz - pos))
+end
+function dissect_ct.v0(buf, tree, pos, sz)
+  tree:add(PF["tripe.ciphertext.tag"], buf(pos, C.tagsz)); pos = pos + C.tagsz
+  tree:add(PF["tripe.ciphertext.seq"], buf(pos, 4)); pos = pos + 4
+  tree:add(PF["tripe.ciphertext.iv"], buf(pos, C.ivsz)); pos = pos + C.ivsz
+  tree:add(PF["tripe.ciphertext.body"], buf(pos, sz - pos))
+end
+
+local function dissect_ciphertext(buf, tree, label, pos, sz)
+  -- Dissect a ciphertext, making the whole thing be a little subtree with
+  -- the given LABEL.
+
+  local t = tree:add(PF[label], buf(pos, sz - pos))
+  dissect_ct[C.bulk](buf, t, pos, sz)
+  return pos
+end
+
+local function dissect_packet(buf, tree, pos, sz)
+  return dissect_ciphertext(buf, tree, "tripe.packet.payload", pos, sz)
+end
+
+-- Dissect a group element of some particular kind.
+local dissect_ge = { }
+function dissect_ge.dh(buf, tree, pos, sz)
+  tree:add(PF["tripe.dh.len"], buf(pos, 2))
+  xsz = buf(pos, 2):uint(); pos = pos + 2
+  tree:add(PF["tripe.dh.x"], buf(pos, xsz)); pos = pos + xsz
+  return pos
+end
+function dissect_ge.ec(buf, tree, pos, sz)
+  tree:add(PF["tripe.ec.xlen"], buf(pos, 2))
+  xsz = buf(pos, 2):uint(); pos = pos + 2
+  tree:add(PF["tripe.ec.x"], buf(pos, xsz)); pos = pos + xsz
+  tree:add(PF["tripe.ec.ylen"], buf(pos, 2))
+  ysz = buf(pos, 2):uint(); pos = pos + 2
+  tree:add(PF["tripe.ec.y"], buf(pos, ysz)); pos = pos + ysz
+  return pos
+end
+function dissect_ge.x25519(buf, tree, pos, sz)
+  tree:add(PF["tripe.x25519.x"], buf(pos, 32))
+  return pos + 32
+end
+function dissect_ge.x448(buf, tree, pos, sz)
+  tree:add(PF["tripe.x448.x"], buf(pos, 56))
+  return pos + 56
+end
+
+local function dissect_my_challenge(buf, tree, pos, sz)
+  -- We don't know how long the group element is going to be.  We can set the
+  -- length later, but (at least in older versions) it doesn't work so well
+  -- to increase the length, so make it large to start out, and shrink it
+  -- later.
+  local t = tree:add(PF["tripe.keyexch.mychal"], buf(pos, sz - pos))
+  local q = dissect_ge[C.kx](buf, t, pos, sz)
+  t:set_len(q - pos)
+  return q
+end
+
+local function dissect_my_cookie(buf, tree, pos, sz)
+  tree:add(PF["tripe.keyexch.mycookie"], buf(pos, C.hashsz))
+  return pos + C.hashsz
+end
+
+local function dissect_your_cookie(buf, tree, pos, sz)
+  tree:add(PF["tripe.keyexch.yourcookie"], buf(pos, C.hashsz))
+  return pos + C.hashsz
+end
+
+local kx_scsz = { x25519 = 32, x448 = 56 } -- Hardwired scalar sizes.
+local function dissect_check(buf, tree, pos, sz)
+  local scsz = kx_scsz[C.kx] or C.scsz
+  tree:add(PF["tripe.keyexch.check"], buf(pos, scsz))
+  return pos + scsz
+end
+
+local function dissect_reply(buf, tree, pos, sz)
+  return dissect_ciphertext(buf, tree, "tripe.keyexch.reply", pos, sz)
+end
+
+local function dissect_switch(buf, tree, pos, sz)
+  return dissect_ciphertext(buf, tree, "tripe.keyexch.switch", pos, sz)
+end
+
+local function dissect_switchok(buf, tree, pos, sz)
+  return dissect_ciphertext(buf, tree, "tripe.keyexch.switchok", pos, sz)
+end
+
+local function dissect_misc_payload(buf, tree, pos, sz)
+  tree:add(PF["tripe.misc.payload"], buf(pos, sz - pos))
+  return sz
+end
+
+local function dissect_misc_ciphertext(buf, tree, pos, sz)
+  return dissect_ciphertext(buf, tree, "tripe.misc.ciphertext", pos, sz)
+end
+
+-----------------------------------------------------------------------------
+--- The protocol information table.
+
+local PKTINFO = {
+  -- This is the main table which describes the protocol.  The top level maps
+  -- category codes to structures:
+  --
+  --   * `label' is the category code's symbolic name;
+  --
+  --   * `subtype' is the field name for the subtype code;
+  --
+  --   * `info' is a prefix for the information column display; and
+  --
+  --   * `sub' is a table describing the individual subtypes.
+  --
+  -- The subtype table similarly maps subtype codes to structures:
+  --
+  --   * `label' is the subtype code's symbolic name;
+  --
+  --   * `info' is the suffix for the information column display; and
+  --
+  --   * `dissect' is a sequence of primitive dissectors to run in order to
+  --     parse the rest of the packet.
+
+  [0] = {
+    label = "MSG_PACKET", subtype = "tripe.packet.type",
+    info = "Packet data",
+    sub = {
+      [0] = { label = "PACKET_IP", info = "encapsulated IP datagram",
+             dissect = { dissect_packet} }
+    }
+  },
+
+  [1] = {
+    label = "MSG_KEYEXCH", subtype = "tripe.keyexch.type",
+    info = "Key exchange",
+    sub = {
+      [0] = { label = "KX_PRECHAL", info = "pre-challenge",
+             dissect = { dissect_my_challenge,
+                         dissect_wtf } },
+      [1] = { label = "KX_CHAL", info = "challenge",
+             dissect = { dissect_my_challenge,
+                         dissect_your_cookie,
+                         dissect_check,
+                         dissect_wtf } },
+      [2] = { label = "KX_REPLY", info = "reply",
+             dissect = { dissect_my_challenge,
+                         dissect_your_cookie,
+                         dissect_check,
+                         dissect_reply } },
+      [3] = { label = "KX_SWITCH", info = "switch",
+             dissect = { dissect_my_cookie,
+                         dissect_your_cookie,
+                         dissect_switch } },
+      [4] = { label = "KX_SWITCHOK", info = "switch-ok",
+             dissect = { dissect_switchok } },
+    }
+  },
+
+  [2] = {
+    label = "MSG_MISC", subtype = "tripe.misc.type",
+    info = "Miscellaneous",
+    sub = {
+      [0] = { label = "MISC_NOP", info = "no-operation (keepalive)",
+             dissect = { dissect_misc_payload } },
+      [1] = { label = "MISC_PING", info = "transport-level ping",
+             dissect = { dissect_misc_payload } },
+      [2] = { label = "MISC_PONG", info = "transport-level ping reply",
+             dissect = { dissect_misc_payload } },
+      [3] = { label = "MISC_EPING", info = "crypto-level ping",
+             dissect = { dissect_misc_ciphertext } },
+      [4] = { label = "MISC_EPONG", info = "crypto-level ping reply",
+             dissect = { dissect_misc_ciphertext } },
+      [5] = { label = "MISC_GREET", info = "greeting",
+             dissect = { dissect_misc_payload } },
+    }
+  }
+}
+
+do
+  -- Work through the master table and build `cattab' and `subtab' tables,
+  -- mapping category and subtype codes to their symbolic names for
+  -- presentation.  The `subtab' is a two-level table, needing two layers of
+  -- indexing.
+  local cattab = { }
+  local subtab = { }
+  for i, v in pairs(PKTINFO) do
+    cattab[i] = v.label
+    if v.sub ~= nil then
+      subtab[i] = { }
+      for j, w in pairs(v.sub) do
+       subtab[i][j] = w.label
+      end
+    end
+  end
+
+  local ftab = {
+    -- The protocol fields.  This table maps the field names to structures
+    -- used to build the fields, which are then stored in `PF' (declared way
+    -- above):
+    --
+    --   * `name' is the field name to show in the dissector tree view;
+    --
+    --   * `type' is the field type;
+    --
+    --   * `base' is a tweak describing how the field should be formatted;
+    --
+    --   * `mask' is used to single out a piece of a larger bitfield; and
+    --
+    --   * `tab' names a mapping table used to convert numerical values to
+    --     symbolic names.
+
+    ["tripe.type"] = {
+      name = "Message type", type = ftypes.UINT8, base = base.HEX
+    },
+    ["tripe.cat"] = {
+      name = "Message category", type = ftypes.UINT8, base = base.DEC,
+      mask = 0xf0, tab = cattab
+    },
+    ["tripe.packet.type"] = {
+      name = "Packet subcode", type = ftypes.UINT8, base = base.DEC,
+      mask = 0x0f, tab = subtab[0]
+    },
+    ["tripe.packet.payload"] = {
+      name = "Encrypted packet", type = ftypes.NONE
+    },
+    ["tripe.keyexch.type"] = {
+      name = "Key-exchange subcode", type = ftypes.UINT8, base = base.DEC,
+      mask = 0x0f, tab = subtab[1]
+    },
+    ["tripe.keyexch.mychal"] = {
+      name = "Sender's challenge R = r P", type = ftypes.NONE
+    },
+    ["tripe.keyexch.mycookie"] = {
+      name = "Hash of recipient's challenge = H(R, ...)",
+      type = ftypes.BYTES, base = base.SPACE
+    },
+    ["tripe.keyexch.yourcookie"] = {
+      name = "Hash of sender's challenge = H(R', ...)",
+      type = ftypes.BYTES, base = base.SPACE
+    },
+    ["tripe.keyexch.reply"] = {
+      name = "Encrypted reply = k R'", type = ftypes.NONE
+    },
+    ["tripe.keyexch.switch"] = {
+      name = "Encrypted reply and switch request = k R', H(...)",
+      type = ftypes.NONE
+    },
+    ["tripe.keyexch.switchok"] = {
+      name = "Encrypted switch confirmation = H(...)", type = ftypes.NONE
+    },
+    ["tripe.misc.type"] = {
+      name = "Miscellenaous subcode", type = ftypes.UINT8, base = base.DEC,
+      mask = 0x0f, tab = subtab[2]
+    },
+    ["tripe.misc.payload"] = {
+      name = "Miscellaneous payload",
+      type = ftypes.BYTES, base = base.SPACE
+    },
+    ["tripe.misc.ciphertext"] = {
+      name = "Miscellaneous encrypted payload", type = ftypes.NONE
+    },
+    ["tripe.wtf"] = {
+      name = "Unexpected trailing data",
+      type = ftypes.BYTES, base = base.SPACE
+    },
+    ["tripe.keyexch.check"] = {
+      name = "Sender's challenge check value = r XOR H(r K', ...)",
+      type = ftypes.BYTES, base = base.SPACE
+    },
+    ["tripe.ciphertext.seq"] = {
+      name = "Sequence number", type = ftypes.UINT32, base = base.DEC
+    },
+    ["tripe.ciphertext.iv"] = {
+      name = "Initialization vector", type = ftypes.BYTES, base = base.SPACE
+    },
+    ["tripe.ciphertext.tag"] = {
+      name = "Authentication tag", type = ftypes.BYTES, base = base.SPACE
+    },
+    ["tripe.ciphertext.body"] = {
+      name = "Encrypted data", type = ftypes.BYTES, base = base.SPACE
+    },
+    ["tripe.dh.len"] = {
+      name = "DH group element length",
+      type = ftypes.UINT16, base = base.DEC
+    },
+    ["tripe.dh.x"] = {
+      name = "DH group element value",
+      type = ftypes.BYTES, base = base.SPACE
+    },
+    ["tripe.ec.xlen"] = {
+      name = "Elliptic curve x-coordinate length",
+      type = ftypes.UINT16, base = base.DEC
+    },
+    ["tripe.ec.x"] = {
+      name = "Elliptic curve x-coordinate value",
+      type = ftypes.BYTES, base = base.SPACE
+    },
+    ["tripe.ec.ylen"] = {
+      name = "Elliptic curve y-coordinate length",
+      type = ftypes.UINT16, base = base.DEC
+    },
+    ["tripe.ec.y"] = {
+      name = "Elliptic curve y-coordinate value",
+      type = ftypes.BYTES, base = base.SPACE
+    },
+    ["tripe.x25519.x"] = {
+      name = "X25519 x-coordinate",
+      type = ftypes.BYTES, base = base.SPACE
+    },
+    ["tripe.x448.x"] = {
+      name = "X448 x-coordinate",
+      type = ftypes.BYTES, base = base.SPACE
+    },
+  }
+
+  -- Convert this table into the protocol fields, and populate `PF'.
+  local ff = { }
+  local i = 1
+
+  -- Figure out whether we can use `none' fields (see below).
+  -- probe for this easily
+  local use_none_p = rawget(ProtoField, 'none') ~= nil
+  for abbr, args in pairs(ftab) do
+
+    -- An annoying hack.  Older versions of Wireshark don't allow setting
+    -- fields with type `none', which is a shame because they're ideal as
+    -- internal tree nodes.
+    ty = args.type
+    b = args.base
+    if ty == ftypes.NONE and not use_none_p then
+      ty = ftypes.BYTES
+      b = base.SPACE
+    end
+
+    -- Go make the field.
+    local f = ProtoField.new(args.name, abbr, ty,
+                            args.tab, b, args.mask, args.descr)
+    PF[abbr] = f
+    ff[i] = f; i = i + 1
+  end
+  tripe.fields = PF
+end
+
+-----------------------------------------------------------------------------
+--- The main dissector.
+
+function tripe.dissector(buf, pinfo, tree)
+
+  -- Fill in the obvious stuff.
+  pinfo.cols.protocol = "TrIPE"
+
+  local sz = buf:reported_length_remaining()
+  local sub = tree:add(tripe, buf(0, sz), "TrIPE packet")
+  local p = 1
+
+  -- Decode the packet type octet.
+  local tycode = buf(0, 1):uint()
+  local ty = sub:add(PF["tripe.type"], buf(0, 1))
+  ty:add(PF["tripe.cat"], buf(0, 1))
+  local cat = bit.rshift(bit.band(tycode, 0xf0), 4)
+  local subty = bit.band(tycode, 0x0f)
+  local info = PKTINFO[cat]
+
+  -- Dispatch using the master protocol table.
+  if info == nil then
+    pinfo.cols.info = string.format("Unknown category code %u, " ..
+                                     "unknown type code %u",
+                                   cat, subty)
+  else
+    ty:add(PF[info.subtype], buf(0, 1))
+    local subinfo = info.sub[subty]
+    if subinfo == nil then
+      pinfo.cols.info = string.format("%s, unknown type code %u",
+                                     info.info, subty)
+    else
+      pinfo.cols.info = string.format("%s, %s", info.info, subinfo.info)
+      p = 1
+      for _, d in ipairs(subinfo.dissect) do p = d(buf, sub, p, sz) end
+    end
+  end
+
+  -- Return the final position we reached.
+  return p
+end
+
+-- We're done.  Register the dissector.
+DissectorTable.get("udp.port"):add(4070, tripe)
+
+-------- That's all, folks --------------------------------------------------