diff --git a/router/java/src/net/i2p/router/transport/udp/SSU2Payload.java b/router/java/src/net/i2p/router/transport/udp/SSU2Payload.java
new file mode 100644
index 0000000000000000000000000000000000000000..0439a5b3ad3be1155e398acb59231d1b386063c6
--- /dev/null
+++ b/router/java/src/net/i2p/router/transport/udp/SSU2Payload.java
@@ -0,0 +1,669 @@
+package net.i2p.router.transport.udp;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+
+import net.i2p.I2PAppContext;
+import net.i2p.data.DataFormatException;
+import net.i2p.data.DataHelper;
+import net.i2p.data.i2np.I2NPMessage;
+import net.i2p.data.i2np.I2NPMessageException;
+import net.i2p.data.i2np.I2NPMessageImpl;
+import net.i2p.data.router.RouterInfo;
+
+/**
+ *
+ *  SSU2 Payload generation and parsing
+ *
+ *  @since 0.9.54
+ */
+class SSU2Payload {
+
+    public static final int BLOCK_HEADER_SIZE = 3;
+
+    private static final int BLOCK_DATETIME = 0;
+    private static final int BLOCK_OPTIONS = 1;
+    private static final int BLOCK_ROUTERINFO = 2;
+    private static final int BLOCK_I2NP = 3;
+    private static final int BLOCK_FIRSTFRAG = 4;
+    private static final int BLOCK_FOLLOWONFRAG = 5;
+    private static final int BLOCK_TERMINATION = 6;
+    private static final int BLOCK_RELAYREQ = 7;
+    private static final int BLOCK_RELAYRESP = 8;
+    private static final int BLOCK_RELAYINTRO = 9;
+    private static final int BLOCK_PEERTEST = 10;
+    private static final int BLOCK_NEXTNONCE = 11;
+    private static final int BLOCK_ACK = 12;
+    private static final int BLOCK_ADDRESS = 13;
+    private static final int BLOCK_INTROKEY= 14;
+    private static final int BLOCK_RELAYTAGREQ = 15;
+    private static final int BLOCK_RELAYTAG = 16;
+    private static final int BLOCK_NEWTOKEN = 17;
+    private static final int BLOCK_PATHCHALLENGE = 18;
+    private static final int BLOCK_PATHRESP = 19;
+    private static final int BLOCK_PADDING = 254;
+
+    /**
+     *  For all callbacks, recommend throwing exceptions only from the handshake.
+     *  Exceptions will get thrown out of processPayload() and prevent
+     *  processing of succeeding blocks.
+     */
+    public interface PayloadCallback {
+        public void gotDateTime(long time) throws DataFormatException;
+
+        public void gotI2NP(I2NPMessage msg) throws I2NPMessageException;
+
+        /**
+         *  @param expires 0 for frag greater than 1
+         *  @param type 0 for frag greater than 1
+         */
+        public void gotFragment(byte[] data, long messageID, int type, long expires, int frag, boolean isLast) throws DataFormatException;
+
+        /**
+         *  @param ranges null if none
+         */
+        public void gotACK(long ackThru, int acks, byte[] ranges);
+
+        /**
+         *  @param isHandshake true only for message 3 part 2
+         */
+        public void gotOptions(byte[] options, boolean isHandshake) throws DataFormatException;
+
+        /**
+         *  @param ri will already be validated
+         *  @param isHandshake true only for message 3 part 2
+         */
+        public void gotRI(RouterInfo ri, boolean isHandshake, boolean flood) throws DataFormatException;
+
+        /**
+         *  @param data is first gzipped and then fragmented
+         *  @param isHandshake true only for message 3 part 2
+         */
+        public void gotRIFragment(byte[] data, boolean isHandshake, boolean flood, boolean isGzipped, int frag, int totalFrags);
+
+        public void gotAddress(byte[] ip, int port);
+
+        public void gotIntroKey(byte[] key);
+
+        public void gotRelayTagRequest();
+
+        public void gotRelayTag(long tag);
+
+        public void gotToken(long token, long expires);
+
+        /**
+         *  @param lastReceived in theory could wrap around to negative, but very unlikely
+         */
+        public void gotTermination(int reason, long lastReceived);
+
+        /**
+         *  For stats.
+         *  @param paddingLength the number of padding bytes, not including the 3-byte block header
+         *  @param frameLength the total size of the frame, including all blocks and block headers
+         */
+        public void gotPadding(int paddingLength, int frameLength);
+
+        public void gotUnknown(int type, int len);
+    }
+
+    /**
+     *  Incoming payload. Calls the callback for each received block.
+     *
+     *  @return number of blocks processed
+     *  @throws IOException on major errors
+     *  @throws DataFormatException on parsing of individual blocks
+     *  @throws I2NPMessageException on parsing of I2NP block
+     */
+    public static int processPayload(I2PAppContext ctx, PayloadCallback cb,
+                                     byte[] payload, int off, int length, boolean isHandshake)
+                                     throws IOException, DataFormatException, I2NPMessageException {
+        int blocks = 0;
+        boolean gotPadding = false;
+        boolean gotTermination = false;
+        int i = off;
+        final int end = off + length;
+        while (i < end) {
+            int type = payload[i++] & 0xff;
+            if (gotPadding)
+                throw new IOException("Illegal block after padding: " + type);
+            if (gotTermination && type != BLOCK_PADDING)
+                throw new IOException("Illegal block after termination: " + type);
+            if (isHandshake && blocks == 0 && type != BLOCK_DATETIME)
+                throw new IOException("Illegal first block in handshake: " + type);
+            int len = (int) DataHelper.fromLong(payload, i, 2);
+            i += 2;
+            if (i + len > end) {
+                throw new IOException("Block " + blocks + " type " + type + " length " + len +
+                                      " at offset " + (i - 3 - off) + " runs over frame of size " + length +
+                                      '\n' + net.i2p.util.HexDump.dump(payload, off, length));
+            }
+            switch (type) {
+                // don't modify i inside switch
+
+                case BLOCK_DATETIME:
+                    if (len != 4)
+                        throw new IOException("Bad length for DATETIME: " + len);
+                    long time = DataHelper.fromLong(payload, i, 4) * 1000;
+                    cb.gotDateTime(time);
+                    break;
+
+                case BLOCK_OPTIONS:
+                    byte[] options = new byte[len];
+                    System.arraycopy(payload, i, options, 0, len);
+                    cb.gotOptions(options, isHandshake);
+                    break;
+
+                case BLOCK_ROUTERINFO: {
+                    int flag = payload[i] & 0xff;
+                    boolean flood = (flag & 0x01) != 0;
+                    boolean gz = (flag & 0x02) != 0;
+                    int frag = payload[i + 1] & 0xff;
+                    int fnum = frag >> 4;
+                    int ftot = frag & 0x0f;
+                    if (ftot == 0)
+                        throw new IOException("Bad fragment count for ROUTERINFO: " + ftot);
+                    if (fnum == 0 && ftot == 1) {
+                        RouterInfo alice = new RouterInfo();
+                        ByteArrayInputStream bais;
+                        if (gz) {
+                            byte decompressed[] = DataHelper.decompress(payload, i + 2, len - 2);
+                            bais = new ByteArrayInputStream(decompressed);
+                        } else {
+                            bais = new ByteArrayInputStream(payload, i + 2, len - 2);
+                        }
+                        alice.readBytes(bais, true);
+                        cb.gotRI(alice, isHandshake, flood);
+                    } else {
+                        byte[] data = new byte[len - 2];
+                        System.arraycopy(payload, i + 2, data, 0, len - 2);
+                        cb.gotRIFragment(data, isHandshake, flood, gz, fnum, ftot);
+                    }
+                    break;
+                }
+
+                case BLOCK_I2NP:
+                    if (isHandshake)
+                        throw new IOException("Illegal block in handshake: " + type);
+                    I2NPMessage msg = I2NPMessageImpl.fromRawByteArrayNTCP2(ctx, payload, i, len, null);
+                    cb.gotI2NP(msg);
+                    break;
+
+                case BLOCK_FIRSTFRAG: {
+                    if (isHandshake)
+                        throw new IOException("Illegal block in handshake: " + type);
+                    if (len < 9)
+                        throw new IOException("Bad length for FIRSTFRAG: " + len);
+                    int mtype = payload[i] & 0xff;
+                    long id = DataHelper.fromLong(payload, i + 1, 4);
+                    long exp = DataHelper.fromLong(payload, i + 5, 4) * 1000;
+                    byte[] data = new byte[len - 9];
+                    System.arraycopy(payload, i + 9, data, 0, len - 9);
+                    cb.gotFragment(data, id, mtype, exp, 0, false);
+                    break;
+                }
+
+                case BLOCK_FOLLOWONFRAG: {
+                    if (isHandshake)
+                        throw new IOException("Illegal block in handshake: " + type);
+                    if (len < 5)
+                        throw new IOException("Bad length for FOLLOWON: " + len);
+                    int frag = (payload[i] & 0xff) >> 1;
+                    boolean isLast = (payload[i] & 0x01) != 0;
+                    long id = DataHelper.fromLong(payload, i + 1, 4);
+                    byte[] data = new byte[len - 5];
+                    System.arraycopy(payload, i + 5, data, 0, len - 5);
+                    cb.gotFragment(data, id, 0, 0, frag, isLast);
+                    break;
+                }
+
+                case BLOCK_ACK: {
+                    if (isHandshake)
+                        throw new IOException("Illegal block in handshake: " + type);
+                    if (len < 5 || (len % 2) != 1)
+                        throw new IOException("Bad length for ACK: " + len);
+                    long ack = DataHelper.fromLong(payload, i, 4);
+                    int acnt = payload[i + 4] & 0xff;
+                    int rcnt = len - 5;
+                    byte[] ranges;
+                    if (rcnt > 0) {
+                        ranges = new byte[rcnt];
+                        System.arraycopy(payload, i + 5, ranges, 0, rcnt);
+                    } else {
+                        ranges = null;
+                    }
+                    cb.gotACK(ack, acnt, ranges);
+                    break;
+                }
+
+                case BLOCK_ADDRESS:
+                    if (len != 6 && len != 18)
+                        throw new IOException("Bad length for Address: " + len);
+                    int port = (int) DataHelper.fromLong(payload, i, 2);
+                    byte[] ip = new byte[len - 2];
+                    System.arraycopy(payload, i + 2, ip, 0, len - 2);
+                    cb.gotAddress(ip, port);
+                    break;
+
+                case BLOCK_RELAYTAGREQ:
+                    cb.gotRelayTagRequest();
+                    break;
+
+                case BLOCK_RELAYTAG:
+                    if (len < 4)
+                        throw new IOException("Bad length for RELAYTAG: " + len);
+                    long tag = DataHelper.fromLong(payload, i, 4);
+                    cb.gotRelayTag(tag);
+                    break;
+
+                case BLOCK_NEWTOKEN:
+                    if (len < 12)
+                        throw new IOException("Bad length for NEWTOKEN: " + len);
+                    long exp = DataHelper.fromLong(payload, i, 4) * 1000;
+                    long token = DataHelper.fromLong8(payload, i + 4);
+                    cb.gotToken(token, exp);
+                    break;
+
+                case BLOCK_TERMINATION:
+                    if (isHandshake)
+                        throw new IOException("Illegal block in handshake: " + type);
+                    if (len < 9)
+                        throw new IOException("Bad length for TERMINATION: " + len);
+                    long last = DataHelper.fromLong8(payload, i);
+                    int rsn = payload[i + 8] & 0xff;
+                    cb.gotTermination(rsn, last);
+                    gotTermination = true;
+                    break;
+
+                case BLOCK_PADDING:
+                    gotPadding = true;
+                    cb.gotPadding(len, length);
+                    break;
+
+                default:
+                    if (isHandshake)
+                        throw new IOException("Illegal block in handshake: " + type);
+                    cb.gotUnknown(type, len);
+                    break;
+
+            }
+            i += len;
+            blocks++;
+        }
+        if (isHandshake && blocks == 0)
+            throw new IOException("No blocks in handshake");
+        return blocks;
+    }
+
+    /**
+     *  @param payload writes to it starting at off
+     *  @return the new offset
+     */
+    public static int writePayload(byte[] payload, int off, List<Block> blocks) {
+        for (Block block : blocks) {
+            off = block.write(payload, off);
+        }
+        return off;
+    }
+
+    /**
+     *  Base class for blocks to be transmitted.
+     *  Not used for receive; we use callbacks instead.
+     */
+    public static abstract class Block {
+        private final int type;
+
+        public Block(int ttype) {
+            type = ttype;
+        }
+
+        /** @return new offset */
+        public int write(byte[] tgt, int off) {
+            tgt[off++] = (byte) type;
+            // we do it this way so we don't call getDataLength(),
+            // which may be inefficient
+            // off is where the length goes
+            int rv = writeData(tgt, off + 2);
+            DataHelper.toLong(tgt, off, 2, rv - (off + 2));
+            return rv;
+        }
+
+        /**
+         *  @return the size of the block, including the 3 byte header (type and size)
+         */
+        public int getTotalLength() {
+            return BLOCK_HEADER_SIZE + getDataLength();
+        }
+
+        /**
+         *  @return the size of the block, NOT including the 3 byte header (type and size)
+         */
+        public abstract int getDataLength();
+
+        /** @return new offset */
+        public abstract int writeData(byte[] tgt, int off);
+
+        @Override
+        public String toString() {
+            return "Payload block type " + type + " length " + getDataLength();
+        }
+    }
+
+    public static class RIBlock extends Block {
+        private final byte[] data;
+        private final int doff, dlen;
+        private final boolean f, gz;
+        private final int fr, frt;
+
+        /**
+         *  Whole thing
+         */
+        public RIBlock(byte[] ridata, boolean flood, boolean gzipped) {
+            this(ridata, 0, ridata.length, flood, gzipped, 0, 1);
+        }
+
+        /**
+         *  Fragment
+         */
+        public RIBlock(byte[] ridata, int off, int len, boolean flood, boolean gzipped, int frag, int total) {
+            super(BLOCK_ROUTERINFO);
+            data = ridata;
+            doff = off;
+            dlen = len;
+            f = flood;
+            gz = gzipped;
+            fr = frag;
+            frt = total;
+        }
+
+        public int getDataLength() {
+            return 2 + data.length;
+        }
+
+        public int writeData(byte[] tgt, int off) {
+            byte b = (byte) (f ? 1 : 0);
+            if (gz)
+                b |= 0x02;
+            tgt[off++] = b;    // flag
+            b = (byte) ((fr << 4) | frt);
+            tgt[off++] = b;    // frag
+            System.arraycopy(data, doff, tgt, off, dlen);
+            return off + dlen;
+        }
+    }
+
+/*
+    public static class I2NPBlock extends Block {
+        private final OutboundMessageState2 m;
+
+        public I2NPBlock(OutboundMessageState2 msg) {
+            super(BLOCK_I2NP);
+            m = msg;
+        }
+
+        public int getDataLength() {
+            // 9 byte header vs. 16
+            return m.getMessageSize();
+        }
+
+        public int writeData(byte[] tgt, int off) {
+            // fixme NTCP2 flavor
+            return off + m.writeFragment(tgt, off, 0);
+        }
+    }
+*/
+
+    /**
+     *  Same format as I2NPBlock
+     */
+/*
+    public static class FirstFragBlock extends Block {
+        private final OutboundMessageState2 m;
+
+        public FirstFragBlock(OutboundMessageState2 msg) {
+            super(BLOCK_FIRSTFRAG);
+            m = msg;
+        }
+
+        public int getDataLength() {
+            // 9 byte header vs. 5
+            return m.fragmentSize(0); // + 4;
+        }
+
+        public int writeData(byte[] tgt, int off) {
+            // fixme NTCP2 flavor
+            return off + m.writeFragment(tgt, off, 0);
+        }
+    }
+*/
+
+    /**
+     *
+     */
+/*
+    public static class FollowFragBlock extends Block {
+        private final OutboundMessageState2 m;
+        private final int f;
+
+        public FollowFragBlock(OutboundMessageState2 msg, int frag) {
+            super(BLOCK_FOLLOWONFRAG);
+            if (frag <= 0)
+                throw new IllegalArgumentException();
+            m = msg;
+            f = frag;
+        }
+
+        public int getDataLength() {
+            return m.fragmentSize(f) + 5;
+        }
+
+        public int writeData(byte[] tgt, int off) {
+            byte b = (byte) (f << 1);
+            if (f == m.getFragmentCount() - 1)
+                b |= (byte) 0x01;
+            tgt[off++] = b;
+            DataHelper.toLong(tgt, off, 4, m.getMessageId());
+            off += 4;
+            return off + m.writeFragment(tgt, off, 0);
+        }
+    }
+*/
+
+    public static class PaddingBlock extends Block {
+        private final int sz;
+        private final I2PAppContext ctx;
+
+        /** with zero-filled data */
+        public PaddingBlock(int size) {
+            this(null, size);
+        }
+
+        /** with random data */
+        public PaddingBlock(I2PAppContext context, int size) {
+            super(BLOCK_PADDING);
+            sz = size;
+            ctx = context;
+        }
+
+        public int getDataLength() {
+            return sz;
+        }
+
+        public int writeData(byte[] tgt, int off) {
+            if (ctx != null)
+                ctx.random().nextBytes(tgt, off, sz);
+            else
+                Arrays.fill(tgt, off, off + sz, (byte) 0);
+            return off + sz;
+        }
+    }
+
+    public static class DateTimeBlock extends Block {
+        private final long now;
+
+        public DateTimeBlock(I2PAppContext ctx) {
+            super(BLOCK_DATETIME);
+            now = ctx.clock().now();
+        }
+
+        public int getDataLength() {
+            return 4;
+        }
+
+        public int writeData(byte[] tgt, int off) {
+            DataHelper.toLong(tgt, off, 4, now / 1000);
+            return off + 4;
+        }
+    }
+
+    public static class OptionsBlock extends Block {
+        private final byte[] opts;
+
+        public OptionsBlock(byte[] options) {
+            super(BLOCK_OPTIONS);
+            opts = options;
+        }
+
+        public int getDataLength() {
+            return opts.length;
+        }
+
+        public int writeData(byte[] tgt, int off) {
+            System.arraycopy(opts, 0, tgt, off, opts.length);
+            return off + opts.length;
+        }
+    }
+
+    public static class TerminationBlock extends Block {
+        private final byte rsn;
+        private final long rcvd;
+
+        public TerminationBlock(int reason, long lastReceived) {
+            super(BLOCK_TERMINATION);
+            rsn = (byte) reason;
+            rcvd = lastReceived;
+        }
+
+        public int getDataLength() {
+            return 9;
+        }
+
+        public int writeData(byte[] tgt, int off) {
+            DataHelper.toLong8(tgt, off, rcvd);
+            tgt[off + 8] = rsn;
+            return off + 9;
+        }
+    }
+
+    public static class AckBlock extends Block {
+        private final long t;
+        private final int a;
+        private final byte[] r;
+        private final int rc;
+
+        /*
+         * @param ranges nack/ack/nack/ack
+         * @param rangeCount ranges length / 2
+         */
+        public AckBlock(long thru, int acnt, byte[] ranges, int rangeCount) {
+            super(BLOCK_ACK);
+            if (rangeCount > 255)
+                throw new IllegalArgumentException();
+            if (acnt > 255)
+                throw new IllegalArgumentException();
+            t = thru;
+            a = acnt;
+            r = ranges;
+            rc = rangeCount;
+        }
+
+        public int getDataLength() {
+            return 5 + (rc * 2);
+        }
+
+        public int writeData(byte[] tgt, int off) {
+            DataHelper.toLong(tgt, off, 4, t);
+            off += 4;
+            tgt[off++] = (byte) a;
+            System.arraycopy(r, 0, tgt, off, rc * 2);
+            return off + (rc * 2);
+        }
+    }
+
+    public static class AddressBlock extends Block {
+        private final byte[] i;
+        private final int p;
+
+        public AddressBlock(byte[] ip, int port) {
+            super(BLOCK_ADDRESS);
+            i = ip;
+            p = port;
+        }
+
+        public int getDataLength() {
+            return 2 + i.length;
+        }
+
+        public int writeData(byte[] tgt, int off) {
+            DataHelper.toLong(tgt, off, 2, p);
+            off += 2;
+            System.arraycopy(i, 0, tgt, off, i.length);
+            return off + i.length;
+        }
+    }
+
+    public static class RelayTagRequestBlock extends Block {
+
+        public RelayTagRequestBlock() {
+            super(BLOCK_RELAYTAGREQ);
+        }
+
+        public int getDataLength() {
+            return 0;
+        }
+
+        public int writeData(byte[] tgt, int off) {
+            return off;
+        }
+    }
+
+    public static class RelayTagBlock extends Block {
+        private final long t;
+
+        public RelayTagBlock(long tag) {
+            super(BLOCK_RELAYTAG);
+            t = tag;
+        }
+
+        public int getDataLength() {
+            return 4;
+        }
+
+        public int writeData(byte[] tgt, int off) {
+            DataHelper.toLong(tgt, off, 4, t);
+            return off + 4;
+        }
+    }
+
+    public static class NewTokenBlock extends Block {
+        private final long t, e;
+
+        public NewTokenBlock(long token, long expires) {
+            super(BLOCK_NEWTOKEN);
+            t = token;
+            e = expires / 1000;
+        }
+
+        public int getDataLength() {
+            return 12;
+        }
+
+        public int writeData(byte[] tgt, int off) {
+            DataHelper.toLong(tgt, off, 4, e);
+            off += 4;
+            DataHelper.toLong8(tgt, off, t);
+            return off + 8;
+        }
+    }
+}
diff --git a/router/java/src/net/i2p/router/transport/udp/SSU2Util.java b/router/java/src/net/i2p/router/transport/udp/SSU2Util.java
index 4d8295b70f8b5e058de267d52e4f4b9deb113bc6..aa4370815196845d1e90c60ce71cde1b00106365 100644
--- a/router/java/src/net/i2p/router/transport/udp/SSU2Util.java
+++ b/router/java/src/net/i2p/router/transport/udp/SSU2Util.java
@@ -56,16 +56,16 @@ final class SSU2Util {
 
 
     /** 3 byte block header + 9 byte I2NP header = 12 */
- //   public static final int FULL_I2NP_HEADER_SIZE = SSU2Payload.BLOCK_HEADER_SIZE + 9;
+    public static final int FULL_I2NP_HEADER_SIZE = SSU2Payload.BLOCK_HEADER_SIZE + 9;
 
     /** 3 byte block header + 9 byte I2NP header = 12 */
- //   public static final int FIRST_FRAGMENT_HEADER_SIZE = SSU2Payload.BLOCK_HEADER_SIZE + 9;
+    public static final int FIRST_FRAGMENT_HEADER_SIZE = SSU2Payload.BLOCK_HEADER_SIZE + 9;
 
     /** 3 byte block header + 4 byte msg ID + 1 byte fragment info = 8 */
- //   public static final int FOLLOWON_FRAGMENT_HEADER_SIZE = SSU2Payload.BLOCK_HEADER_SIZE + 5;
+    public static final int FOLLOWON_FRAGMENT_HEADER_SIZE = SSU2Payload.BLOCK_HEADER_SIZE + 5;
 
     /** 16 byte block header + 2 + 12 = 30 */
- //   public static final int DATA_HEADER_SIZE = SHORT_HEADER_SIZE + 2 + FULL_I2NP_HEADER_SIZE;
+    public static final int DATA_HEADER_SIZE = SHORT_HEADER_SIZE + 2 + FULL_I2NP_HEADER_SIZE;
 
     /**
      *  The message types, 0-10, as bytes