From f4be99ecd0326493d4d23e0666627ee7a5e8caac Mon Sep 17 00:00:00 2001
From: zzz <zzz@i2pmail.org>
Date: Thu, 24 Feb 2022 06:13:28 -0500
Subject: [PATCH] SSU: Add SSU2 class extensions and packet builder

Pass XDH key builder to UDPTransport
Add SSU2 static keygen when enabled
WIP, not hooked in
---
 .../router/transport/TransportManager.java    |    7 +-
 .../transport/udp/InboundEstablishState2.java |  398 ++++++
 .../udp/OutboundEstablishState2.java          |  288 ++++
 .../router/transport/udp/PacketBuilder2.java  | 1173 +++++++++++++++++
 .../i2p/router/transport/udp/PeerState2.java  |   68 +
 .../router/transport/udp/UDPTransport.java    |  131 +-
 6 files changed, 2063 insertions(+), 2 deletions(-)
 create mode 100644 router/java/src/net/i2p/router/transport/udp/InboundEstablishState2.java
 create mode 100644 router/java/src/net/i2p/router/transport/udp/OutboundEstablishState2.java
 create mode 100644 router/java/src/net/i2p/router/transport/udp/PacketBuilder2.java
 create mode 100644 router/java/src/net/i2p/router/transport/udp/PeerState2.java

diff --git a/router/java/src/net/i2p/router/transport/TransportManager.java b/router/java/src/net/i2p/router/transport/TransportManager.java
index 4c63f1ea5f..6e887c4eb5 100644
--- a/router/java/src/net/i2p/router/transport/TransportManager.java
+++ b/router/java/src/net/i2p/router/transport/TransportManager.java
@@ -83,6 +83,10 @@ public class TransportManager implements TransportEventListener {
 
     /** default true */
     public final static String PROP_ENABLE_UDP = "i2np.udp.enable";
+    /**
+     * @since 0.9.54
+     */
+    public final static String PROP_ENABLE_SSU2 = "i2np.ssu2.enable";
     /** default true */
     public final static String PROP_ENABLE_NTCP = "i2np.ntcp.enable";
     /** default true */
@@ -255,7 +259,8 @@ public class TransportManager implements TransportEventListener {
     private void configTransports() {
         Transport udp = null;
         if (_enableUDP) {
-            udp = new UDPTransport(_context, _dhThread);
+            X25519KeyFactory xdh = _context.getBooleanProperty(PROP_ENABLE_SSU2) ? _xdhThread : null;
+            udp = new UDPTransport(_context, _dhThread, _xdhThread);
             addTransport(udp);
             initializeAddress(udp);
         }
diff --git a/router/java/src/net/i2p/router/transport/udp/InboundEstablishState2.java b/router/java/src/net/i2p/router/transport/udp/InboundEstablishState2.java
new file mode 100644
index 0000000000..06f015fcd1
--- /dev/null
+++ b/router/java/src/net/i2p/router/transport/udp/InboundEstablishState2.java
@@ -0,0 +1,398 @@
+package net.i2p.router.transport.udp;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.security.GeneralSecurityException;
+import java.util.List;
+
+import com.southernstorm.noise.protocol.CipherState;
+import com.southernstorm.noise.protocol.CipherStatePair;
+import com.southernstorm.noise.protocol.HandshakeState;
+
+import net.i2p.data.Base64;
+import net.i2p.data.DataFormatException;
+import net.i2p.data.DataHelper;
+import net.i2p.data.SessionKey;
+import net.i2p.data.i2np.I2NPMessage;
+import net.i2p.data.router.RouterAddress;
+import net.i2p.data.router.RouterInfo;
+import net.i2p.router.RouterContext;
+import net.i2p.util.Addresses;
+import net.i2p.util.Log;
+
+/**
+ * Data for a new connection being established, where the remote peer has
+ * initiated the connection with us.  In other words, they are Alice and
+ * we are Bob.
+ *
+ * SSU2 only.
+ *
+ * @since 0.9.54
+ */
+class InboundEstablishState2 extends InboundEstablishState implements SSU2Payload.PayloadCallback {
+    private final InetSocketAddress _aliceSocketAddress;
+    private final long _rcvConnID;
+    private final long _sendConnID;
+    private final long _token;
+    private final long _nextToken;
+    private final HandshakeState _handshakeState;
+    private byte[] _sendHeaderEncryptKey1;
+    private final byte[] _rcvHeaderEncryptKey1;
+    private byte[] _sendHeaderEncryptKey2;
+    private byte[] _rcvHeaderEncryptKey2;
+    
+    /**
+     *  @param localPort Must be our external port, otherwise the signature of the
+     *                   SessionCreated message will be bad if the external port != the internal port.
+     *  @param packet with all header encryption removed
+     */
+    public InboundEstablishState2(RouterContext ctx, UDPTransport transport,
+                                  UDPPacket packet) throws GeneralSecurityException {
+        super(ctx, (InetSocketAddress) packet.getPacket().getSocketAddress());
+        DatagramPacket pkt = packet.getPacket();
+        _aliceSocketAddress = (InetSocketAddress) pkt.getSocketAddress();
+        _handshakeState = new HandshakeState(HandshakeState.PATTERN_ID_XK_SSU2, HandshakeState.RESPONDER, transport.getXDHFactory());
+        _handshakeState.getLocalKeyPair().setKeys(transport.getSSU2StaticPrivKey(), 0,
+                                                  transport.getSSU2StaticPubKey(), 0);
+        byte[] introKey = transport.getSSU2StaticIntroKey();
+        _sendHeaderEncryptKey1 = introKey;
+        _rcvHeaderEncryptKey1 = introKey;
+        //_sendHeaderEncryptKey2 set below
+        //_rcvHeaderEncryptKey2 set below
+        _introductionRequested = false; // todo
+        //_bobIP = TODO
+        //if (_log.shouldLog(Log.DEBUG))
+        //    _log.debug("Receive sessionRequest, BobIP = " + Addresses.toString(_bobIP));
+        int off = pkt.getOffset();
+        int len = pkt.getLength();
+        byte data[] = pkt.getData();
+        // fast MSB check for key < 2^255
+        if ((data[off + 32 + 32 - 1] & 0x80) != 0)
+            throw new GeneralSecurityException("Bad PK msg 1");
+        _rcvConnID = DataHelper.fromLong8(data, off);
+        _sendConnID = DataHelper.fromLong8(data, off + 16);
+        if (_rcvConnID == _sendConnID)
+            throw new GeneralSecurityException("Identical Conn IDs");
+        int type = data[off + 12] & 0xff;
+        long token = DataHelper.fromLong8(data, off + 24);
+        if (type == 10) {
+            _currentState = InboundState.IB_STATE_TOKEN_REQUEST_RECEIVED;
+            // TODO decrypt chacha?
+            _sendHeaderEncryptKey2 = introKey;
+            do {
+                token = ctx.random().nextLong();
+            } while (token == 0);
+            _token = token;
+        } else if (type == 0 && token == 0) { // || token not valid
+            _currentState = InboundState.IB_STATE_REQUEST_BAD_TOKEN_RECEIVED;
+            _sendHeaderEncryptKey2 = introKey;
+            do {
+                token = ctx.random().nextLong();
+            } while (token == 0);
+            _token = token;
+        } else {
+            // probably don't need again
+            _token = token;
+            _handshakeState.start();
+            if (_log.shouldDebug())
+                _log.debug("State after start: " + _handshakeState);
+            _handshakeState.mixHash(data, off, 32);
+            if (_log.shouldDebug())
+                _log.debug("State after mixHash 1: " + _handshakeState);
+
+            byte[] payload = new byte[len - 80]; // 32 hdr, 32 eph. key, 16 MAC
+            try {
+                _handshakeState.readMessage(data, off + 32, len - 32, payload, 0);
+            } catch (GeneralSecurityException gse) {
+                if (_log.shouldDebug())
+                    _log.debug("Session request error, State at failure: " + _handshakeState + '\n' + net.i2p.util.HexDump.dump(data, off, len), gse);
+                throw gse;
+            }
+            if (_log.shouldDebug())
+                _log.debug("State after sess req: " + _handshakeState);
+            processPayload(payload, payload.length, true);
+            _sendHeaderEncryptKey2 = SSU2Util.hkdf(_context, _handshakeState.getChainingKey(), "SessCreateHeader");
+            _currentState = InboundState.IB_STATE_REQUEST_RECEIVED;
+        }
+        _nextToken = ctx.random().nextLong();
+        packetReceived();
+    }
+
+    @Override
+    public int getVersion() { return 2; }
+    
+    private void processPayload(byte[] payload, int length, boolean isHandshake) throws GeneralSecurityException {
+        try {
+            int blocks = SSU2Payload.processPayload(_context, this, payload, 0, length, isHandshake);
+            System.out.println("Processed " + blocks + " blocks");
+        } catch (Exception e) {
+            _log.error("IES2 payload error\n" + net.i2p.util.HexDump.dump(payload, 0, length));
+            throw new GeneralSecurityException("IES2 payload error", e);
+        }
+    }
+
+    /////////////////////////////////////////////////////////
+    // begin payload callbacks
+    /////////////////////////////////////////////////////////
+
+    public void gotDateTime(long time) {
+        System.out.println("Got DATE block: " + DataHelper.formatTime(time));
+    }
+
+    public void gotOptions(byte[] options, boolean isHandshake) {
+        System.out.println("Got OPTIONS block");
+    }
+
+    public void gotRI(RouterInfo ri, boolean isHandshake, boolean flood) throws DataFormatException {
+        System.out.println("Got RI block: " + ri);
+        if (isHandshake)
+            throw new DataFormatException("RI in Sess Req");
+        List<RouterAddress> addrs = ri.getTargetAddresses("SSU", "SSU2");
+        RouterAddress ra = null;
+        for (RouterAddress addr : addrs) {
+            // skip NTCP w/o "s"
+            if (addrs.size() > 1 && addr.getTransportStyle().equals("SSU") && addr.getOption("s") == null)
+                continue;
+            ra = addr;
+            break;
+        }
+        if (ra == null)
+            throw new DataFormatException("no SSU2 addr");
+        String siv = ra.getOption("i");
+        if (siv == null)
+            throw new DataFormatException("no SSU2 IKey");
+        byte[] ik = Base64.decode(siv);
+        if (ik == null)
+            throw new DataFormatException("bad SSU2 IKey");
+        if (ik.length != 32)
+            throw new DataFormatException("bad SSU2 IKey len");
+        String ss = ra.getOption("s");
+        if (ss == null)
+            throw new DataFormatException("no SSU2 S");
+        byte[] s = Base64.decode(ss);
+        if (s == null)
+            throw new DataFormatException("bad SSU2 S");
+        if (s.length != 32)
+            throw new DataFormatException("bad SSU2 S len");
+        if (!"2".equals(ra.getOption("v")))
+            throw new DataFormatException("bad SSU2 v");
+
+        _sendHeaderEncryptKey1 = ik;
+        //_sendHeaderEncryptKey2 calculated below
+
+    }
+
+    public void gotRIFragment(byte[] data, boolean isHandshake, boolean flood, boolean isGzipped, int frag, int totalFrags) {
+            System.out.println("Got RI fragment " + frag + " of " + totalFrags);
+        if (isHandshake)
+            throw new IllegalStateException("RI in Sess Req");
+    }
+
+    public void gotAddress(byte[] ip, int port) {
+        System.out.println("Got ADDRESS block: " + Addresses.toString(ip, port));
+        throw new IllegalStateException("Address in Handshake");
+    }
+
+    public void gotIntroKey(byte[] key) {
+        System.out.println("Got Intro key: " + Base64.encode(key));
+    }
+
+    public void gotRelayTagRequest() {
+        System.out.println("Got relay tag request");
+    }
+
+    public void gotRelayTag(long tag) {
+        System.out.println("Got relay tag " + tag);
+        throw new IllegalStateException("Relay tag in Handshake");
+    }
+
+    public void gotToken(long token, long expires) {
+        System.out.println("Got NEW TOKEN block " + token + " expires " + DataHelper.formatTime(expires));
+    }
+
+    public void gotI2NP(I2NPMessage msg) {
+        System.out.println("Got I2NP block: " + msg);
+        if (getState() != InboundState.IB_STATE_CREATED_SENT)
+            throw new IllegalStateException("I2NP in Sess Req");
+    }
+
+    public void gotFragment(byte[] data, long messageID, int type, long expires, int frag, boolean isLast) throws DataFormatException {
+        System.out.println("Got FRAGMENT block: " + messageID);
+        if (getState() != InboundState.IB_STATE_CREATED_SENT)
+            throw new IllegalStateException("I2NP in Sess Req");
+    }
+
+    public void gotACK(long ackThru, int acks, byte[] ranges) {
+        System.out.println("Got ACK block: " + ackThru);
+        throw new IllegalStateException("ACK in Handshake");
+    }
+
+    public void gotTermination(int reason, long count) {
+        System.out.println("Got TERMINATION block, reason: " + reason + " count: " + count);
+        throw new IllegalStateException("Termination in Handshake");
+    }
+
+    public void gotUnknown(int type, int len) {
+        System.out.println("Got UNKNOWN block, type: " + type + " len: " + len);
+    }
+
+    public void gotPadding(int paddingLength, int frameLength) {
+        System.out.println("Got PADDING block, len: " + paddingLength + " in frame len: " + frameLength);
+    }
+
+    /////////////////////////////////////////////////////////
+    // end payload callbacks
+    /////////////////////////////////////////////////////////
+    
+    public long getSendConnID() { return _sendConnID; }
+    public long getRcvConnID() { return _rcvConnID; }
+    public long getToken() { return _token; }
+    public long getNextToken() { return _nextToken; }
+    public HandshakeState getHandshakeState() { return _handshakeState; }
+    public byte[] getSendHeaderEncryptKey1() { return _sendHeaderEncryptKey1; }
+    public byte[] getRcvHeaderEncryptKey1() { return _rcvHeaderEncryptKey1; }
+    public byte[] getSendHeaderEncryptKey2() { return _sendHeaderEncryptKey2; }
+    public byte[] getRcvHeaderEncryptKey2() { return _rcvHeaderEncryptKey2; }
+    public InetSocketAddress getSentAddress() { return _aliceSocketAddress; }
+
+    @Override
+    public synchronized void createdPacketSent() {
+        /// todo state check
+        if (_rcvHeaderEncryptKey2 == null)
+            _rcvHeaderEncryptKey2 = SSU2Util.hkdf(_context, _handshakeState.getChainingKey(), "SessionConfirmed");
+        _lastSend = _context.clock().now();
+        long delay;
+        if (_createdSentCount == 0) {
+            delay = RETRANSMIT_DELAY;
+        } else {
+            delay = Math.min(RETRANSMIT_DELAY << _createdSentCount, MAX_DELAY);
+        }
+        _createdSentCount++;
+        _nextSend = _lastSend + delay;
+        if ( (_currentState == InboundState.IB_STATE_UNKNOWN) || (_currentState == InboundState.IB_STATE_REQUEST_RECEIVED) )
+            _currentState = InboundState.IB_STATE_CREATED_SENT;
+    }
+
+    
+    /** note that we just sent a Retry packet */
+    public synchronized void retryPacketSent() {
+        if (_currentState != InboundState.IB_STATE_REQUEST_BAD_TOKEN_RECEIVED &&
+            _currentState != InboundState.IB_STATE_TOKEN_REQUEST_RECEIVED)
+            throw new IllegalStateException("Bad state for Retry Sent: " + _currentState);
+        _currentState = InboundState.IB_STATE_RETRY_SENT;
+    }
+
+    /**
+     *
+     */
+    public synchronized void receiveSessionRequestAfterRetry(UDPPacket packet) throws GeneralSecurityException {
+        if (_currentState != InboundState.IB_STATE_RETRY_SENT)
+            throw new GeneralSecurityException("Bad state for Session Request after Retry: " + _currentState);
+        DatagramPacket pkt = packet.getPacket();
+        SocketAddress from = pkt.getSocketAddress();
+        if (!from.equals(_aliceSocketAddress))
+            throw new GeneralSecurityException("Address mismatch: req: " + _aliceSocketAddress + " conf: " + from);
+        int off = pkt.getOffset();
+        int len = pkt.getLength();
+        byte data[] = pkt.getData();
+        long rid = DataHelper.fromLong8(data, off);
+        if (rid != _rcvConnID)
+            throw new GeneralSecurityException("Conn ID mismatch: 1: " + _rcvConnID + " 2: " + rid);
+        long sid = DataHelper.fromLong8(data, off + 16);
+        if (sid != _sendConnID)
+            throw new GeneralSecurityException("Conn ID mismatch: 1: " + _sendConnID + " 2: " + sid);
+        long token = DataHelper.fromLong8(data, off + 24);
+        if (token != _token)
+            throw new GeneralSecurityException("Token mismatch: 1: " + _token + " 2: " + token);
+        _handshakeState.start();
+        _handshakeState.mixHash(data, off, 32);
+        if (_log.shouldDebug())
+            _log.debug("State after mixHash 1: " + _handshakeState);
+
+        byte[] payload = new byte[len - 80]; // 16 hdr, 32 static key, 16 MAC, 16 MAC
+        try {
+            _handshakeState.readMessage(data, off + 32, len - 32, payload, 0);
+        } catch (GeneralSecurityException gse) {
+            if (_log.shouldDebug())
+                _log.debug("Session Request error, State at failure: " + _handshakeState + '\n' + net.i2p.util.HexDump.dump(data, off, len), gse);
+            throw gse;
+        }
+        if (_log.shouldDebug())
+            _log.debug("State after sess req: " + _handshakeState);
+        processPayload(payload, payload.length, true);
+        _sendHeaderEncryptKey2 = SSU2Util.hkdf(_context, _handshakeState.getChainingKey(), "SessCreateHeader");
+        _currentState = InboundState.IB_STATE_REQUEST_RECEIVED;
+        
+        if (_createdSentCount == 1) {
+            _rtt = (int) ( _context.clock().now() - _lastSend );
+        }	
+
+        packetReceived();
+    }
+
+    /**
+     *
+     *
+     *
+     */
+    public synchronized void receiveSessionConfirmed(UDPPacket packet) throws GeneralSecurityException {
+        if (_currentState != InboundState.IB_STATE_CREATED_SENT)
+            throw new GeneralSecurityException("Bad state for Session Confirmed: " + _currentState);
+        DatagramPacket pkt = packet.getPacket();
+        SocketAddress from = pkt.getSocketAddress();
+        if (!from.equals(_aliceSocketAddress))
+            throw new GeneralSecurityException("Address mismatch: req: " + _aliceSocketAddress + " conf: " + from);
+        int off = pkt.getOffset();
+        int len = pkt.getLength();
+        byte data[] = pkt.getData();
+        long rid = DataHelper.fromLong8(data, off);
+        if (rid != _rcvConnID)
+            throw new GeneralSecurityException("Conn ID mismatch: req: " + _rcvConnID + " conf: " + rid);
+        _handshakeState.mixHash(data, off, 16);
+        if (_log.shouldDebug())
+            _log.debug("State after mixHash 3: " + _handshakeState);
+
+        byte[] payload = new byte[len - 80]; // 16 hdr, 32 static key, 16 MAC, 16 MAC
+        try {
+            _handshakeState.readMessage(data, off + 16, len - 16, payload, 0);
+        } catch (GeneralSecurityException gse) {
+            if (_log.shouldDebug())
+                _log.debug("Session Confirmed error, State at failure: " + _handshakeState + '\n' + net.i2p.util.HexDump.dump(data, off, len), gse);
+            throw gse;
+        }
+        if (_log.shouldDebug())
+            _log.debug("State after sess conf: " + _handshakeState);
+        processPayload(payload, payload.length, false);
+
+        // TODO split, calculate keys
+
+        
+        // TODO fix state
+        if ( (_currentState == InboundState.IB_STATE_UNKNOWN) || 
+             (_currentState == InboundState.IB_STATE_REQUEST_RECEIVED) ||
+             (_currentState == InboundState.IB_STATE_CREATED_SENT) ) {
+            if (confirmedFullyReceived())
+                _currentState = InboundState.IB_STATE_CONFIRMED_COMPLETELY;
+            else
+                _currentState = InboundState.IB_STATE_CONFIRMED_PARTIALLY;
+        }
+        
+        if (_createdSentCount == 1) {
+            _rtt = (int) ( _context.clock().now() - _lastSend );
+        }	
+
+        packetReceived();
+    }
+    
+    @Override
+    public String toString() {            
+        StringBuilder buf = new StringBuilder(128);
+        buf.append("IES2 ");
+        buf.append(Addresses.toString(_aliceIP, _alicePort));
+        buf.append(" RelayTag: ").append(_sentRelayTag);
+        buf.append(' ').append(_currentState);
+        return buf.toString();
+    }
+}
diff --git a/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState2.java b/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState2.java
new file mode 100644
index 0000000000..86975bec28
--- /dev/null
+++ b/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState2.java
@@ -0,0 +1,288 @@
+package net.i2p.router.transport.udp;
+
+import java.net.DatagramPacket;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.net.UnknownHostException;
+import java.security.GeneralSecurityException;
+
+import com.southernstorm.noise.protocol.CipherState;
+import com.southernstorm.noise.protocol.CipherStatePair;
+import com.southernstorm.noise.protocol.HandshakeState;
+
+import net.i2p.data.Base64;
+import net.i2p.data.DataFormatException;
+import net.i2p.data.DataHelper;
+import net.i2p.data.SessionKey;
+import net.i2p.data.i2np.I2NPMessage;
+import net.i2p.data.router.RouterIdentity;
+import net.i2p.data.router.RouterInfo;
+import net.i2p.router.RouterContext;
+import net.i2p.util.Addresses;
+import net.i2p.util.Log;
+
+/**
+ * Data for a new connection being established, where we initiated the 
+ * connection with a remote peer.  In other words, we are Alice and
+ * they are Bob.
+ *
+ * SSU2 only.
+ *
+ * @since 0.9.54
+ */
+class OutboundEstablishState2 extends OutboundEstablishState implements SSU2Payload.PayloadCallback {
+    private InetSocketAddress _bobSocketAddress;
+    private final UDPTransport _transport;
+    private final long _sendConnID;
+    private final long _rcvConnID;
+    private long _token;
+    private final long _nextToken;
+    private HandshakeState _handshakeState;
+    private final byte[] _sendHeaderEncryptKey1;
+    private final byte[] _rcvHeaderEncryptKey1;
+    private byte[] _sendHeaderEncryptKey2;
+    private byte[] _rcvHeaderEncryptKey2;
+    private final byte[] _rcvRetryHeaderEncryptKey2;
+    private int _mtu;
+    private static final boolean SET_TOKEN = false;
+
+    /**
+     *  @param claimedAddress an IP/port based RemoteHostId, or null if unknown
+     *  @param remoteHostId non-null, == claimedAddress if direct, or a hash-based one if indirect
+     *  @param remotePeer must have supported sig type
+     *  @param needIntroduction should we ask Bob to be an introducer for us?
+               ignored unless allowExtendedOptions is true
+     *  @param introKey Bob's introduction key, as published in the netdb
+     *  @param addr non-null
+     */
+    public OutboundEstablishState2(RouterContext ctx, UDPTransport transport, RemoteHostId claimedAddress,
+                                  RemoteHostId remoteHostId, int mtu,
+                                  RouterIdentity remotePeer, byte[] publicKey,
+                                  boolean needIntroduction,
+                                  SessionKey introKey, UDPAddress addr) {
+        super(ctx, claimedAddress, remoteHostId, remotePeer, needIntroduction, introKey, addr);
+        _transport = transport;
+        if (claimedAddress != null) {
+            try {
+                _bobSocketAddress = new InetSocketAddress(InetAddress.getByAddress(_bobIP), _bobPort);
+            } catch (UnknownHostException uhe) {
+                throw new IllegalArgumentException("bad IP", uhe);
+            }
+            _mtu = mtu;
+        } else {
+            _mtu = PeerState.MIN_IPV6_MTU;
+        }
+        if (addr.getIntroducerCount() > 0) {
+            if (_log.shouldLog(Log.DEBUG))
+                _log.debug("new outbound establish to " + remotePeer.calculateHash() + ", with address: " + addr);
+            _currentState = OutboundState.OB_STATE_PENDING_INTRO;
+        } else {
+            _currentState = OutboundState.OB_STATE_UNKNOWN;
+        }
+
+        // SSU2
+        createNewState(publicKey);
+
+        _sendConnID = ctx.random().nextLong();
+        // rcid == scid is not allowed
+        long rcid;
+        do {
+            rcid = ctx.random().nextLong();
+        } while (_sendConnID == rcid);
+        if (SET_TOKEN) {
+            do {
+                _token = ctx.random().nextLong();
+            } while (_token == 0);
+        }
+        _rcvConnID = rcid;
+        _nextToken = ctx.random().nextLong();
+        byte[] ik = introKey.getData();
+        _sendHeaderEncryptKey1 = ik;
+        _rcvHeaderEncryptKey1 = ik;
+        _sendHeaderEncryptKey2 = ik;
+        //_rcvHeaderEncryptKey2 will be set after the Session Request message is created
+        _rcvRetryHeaderEncryptKey2 = ik;
+    }
+
+    private void createNewState(byte[] publicKey) {
+        try {
+            _handshakeState = new HandshakeState(HandshakeState.PATTERN_ID_XK_SSU2, HandshakeState.INITIATOR, _transport.getXDHFactory());
+        } catch (GeneralSecurityException gse) {
+            throw new IllegalStateException("bad proto", gse);
+        }
+        _handshakeState.getRemotePublicKey().setPublicKey(publicKey, 0);
+        _handshakeState.getLocalKeyPair().setKeys(_transport.getSSU2StaticPrivKey(), 0,
+                                                  _transport.getSSU2StaticPubKey(), 0);
+    }
+    
+    public synchronized void restart(long token) {
+        _token = token;
+        HandshakeState old = _handshakeState;
+        byte[] pub = new byte[32];
+        old.getRemotePublicKey().getPublicKey(pub, 0);
+        createNewState(pub);
+        old.destroy();
+        //_rcvHeaderEncryptKey2 will be set after the Session Request message is created
+        _rcvHeaderEncryptKey2 = null;
+    }
+
+    private void processPayload(byte[] payload, int length, boolean isHandshake) throws GeneralSecurityException {
+        try {
+            int blocks = SSU2Payload.processPayload(_context, this, payload, 0, length, isHandshake);
+            System.out.println("Processed " + blocks + " blocks");
+        } catch (Exception e) {
+            throw new GeneralSecurityException("Session Created payload error", e);
+        }
+    }
+
+    /////////////////////////////////////////////////////////
+    // begin payload callbacks
+    /////////////////////////////////////////////////////////
+
+    public void gotDateTime(long time) {
+        System.out.println("Got DATE block: " + DataHelper.formatTime(time));
+    }
+
+    public void gotOptions(byte[] options, boolean isHandshake) {
+        System.out.println("Got OPTIONS block");
+    }
+
+    public void gotRI(RouterInfo ri, boolean isHandshake, boolean flood) throws DataFormatException {
+        System.out.println("Got RI block: " + ri);
+        throw new DataFormatException("RI in Sess Created");
+    }
+
+    public void gotRIFragment(byte[] data, boolean isHandshake, boolean flood, boolean isGzipped, int frag, int totalFrags) {
+        System.out.println("Got RI fragment " + frag + " of " + totalFrags);
+        throw new IllegalStateException("RI in Sess Created");
+    }
+
+    public void gotAddress(byte[] ip, int port) {
+        System.out.println("Got ADDRESS block: " + Addresses.toString(ip, port));
+    }
+
+    public void gotIntroKey(byte[] key) {
+        System.out.println("Got Intro key: " + Base64.encode(key));
+    }
+
+    public void gotRelayTagRequest() {
+        System.out.println("Got relay tag request");
+        throw new IllegalStateException("Relay tag req in Sess Created");
+    }
+
+    public void gotRelayTag(long tag) {
+        System.out.println("Got relay tag " + tag);
+    }
+
+    public void gotToken(long token, long expires) {
+        System.out.println("Got NEW TOKEN block " + token + " expires " + DataHelper.formatTime(expires));
+    }
+
+    public void gotI2NP(I2NPMessage msg) {
+        System.out.println("Got I2NP block: " + msg);
+        throw new IllegalStateException("I2NP in Sess Created");
+    }
+
+    public void gotFragment(byte[] data, long messageID, int type, long expires, int frag, boolean isLast) throws DataFormatException {
+        System.out.println("Got FRAGMENT block: " + messageID);
+        throw new IllegalStateException("I2NP in Sess Created");
+    }
+
+    public void gotACK(long ackThru, int acks, byte[] ranges) {
+        System.out.println("Got ACK block: " + ackThru);
+        throw new IllegalStateException("ACK in Sess Created");
+    }
+
+    public void gotTermination(int reason, long count) {
+        System.out.println("Got TERMINATION block, reason: " + reason + " count: " + count);
+        throw new IllegalStateException("Termination in Sess Created");
+    }
+
+    public void gotUnknown(int type, int len) {
+        System.out.println("Got UNKNOWN block, type: " + type + " len: " + len);
+    }
+
+    public void gotPadding(int paddingLength, int frameLength) {
+        System.out.println("Got PADDING block, len: " + paddingLength + " in frame len: " + frameLength);
+    }
+
+    /////////////////////////////////////////////////////////
+    // end payload callbacks
+    /////////////////////////////////////////////////////////
+    
+    public long getSendConnID() { return _sendConnID; }
+    public long getRcvConnID() { return _rcvConnID; }
+    public long getToken() { return _token; }
+    public long getNextToken() { return _nextToken; }
+    public HandshakeState getHandshakeState() { return _handshakeState; }
+    public byte[] getSendHeaderEncryptKey1() { return _sendHeaderEncryptKey1; }
+    public byte[] getRcvHeaderEncryptKey1() { return _rcvHeaderEncryptKey1; }
+    public byte[] getSendHeaderEncryptKey2() { return _sendHeaderEncryptKey2; }
+    public byte[] getRcvHeaderEncryptKey2() { return _rcvHeaderEncryptKey2; }
+    public byte[] getRcvRetryHeaderEncryptKey2() { return _rcvRetryHeaderEncryptKey2; }
+    public InetSocketAddress getSentAddress() { return _bobSocketAddress; }
+
+    /** what is the largest packet we can send to the peer? */
+    public int getMTU() { return _mtu; }
+
+    public synchronized void receiveSessionCreated(UDPPacket packet) throws GeneralSecurityException {
+        ////// todo fix state check
+        if (_currentState == OutboundState.OB_STATE_VALIDATION_FAILED) {
+            if (_log.shouldLog(Log.WARN))
+                _log.warn("Session created already failed");
+            return;
+        }
+
+        DatagramPacket pkt = packet.getPacket();
+        SocketAddress from = pkt.getSocketAddress();
+        if (!from.equals(_bobSocketAddress))
+            throw new GeneralSecurityException("Address mismatch: req: " + _bobSocketAddress + " created: " + from);
+        int off = pkt.getOffset();
+        int len = pkt.getLength();
+        byte data[] = pkt.getData();
+        _handshakeState.mixHash(data, off, 32);
+        if (_log.shouldDebug())
+            _log.debug("State after mixHash 2: " + _handshakeState);
+
+        byte[] payload = new byte[len - 80]; // 32 hdr, 32 eph. key, 16 MAC
+        try {
+            _handshakeState.readMessage(data, off + 32, len - 32, payload, 0);
+        } catch (GeneralSecurityException gse) {
+            if (_log.shouldDebug())
+                _log.debug("Session create error, State at failure: " + _handshakeState + '\n' + net.i2p.util.HexDump.dump(data, off, len), gse);
+            throw gse;
+        }
+        if (_log.shouldDebug())
+            _log.debug("State after sess cr: " + _handshakeState);
+        processPayload(payload, payload.length, true);
+        _sendHeaderEncryptKey2 = SSU2Util.hkdf(_context, _handshakeState.getChainingKey(), "SessionConfirmed");
+
+        if (_currentState == OutboundState.OB_STATE_UNKNOWN ||
+            _currentState == OutboundState.OB_STATE_REQUEST_SENT ||
+            _currentState == OutboundState.OB_STATE_INTRODUCED ||
+            _currentState == OutboundState.OB_STATE_PENDING_INTRO)
+            _currentState = OutboundState.OB_STATE_CREATED_RECEIVED;
+
+        if (_requestSentCount == 1) {
+            _rtt = (int) (_context.clock().now() - _requestSentTime);
+        }
+        packetReceived();
+    }
+
+    /**
+     * note that we just sent the SessionRequest packet
+     */
+    @Override
+    public synchronized void requestSent() {
+        /// TODO store pkt for retx
+        if (_rcvHeaderEncryptKey2 == null)
+            _rcvHeaderEncryptKey2 = SSU2Util.hkdf(_context, _handshakeState.getChainingKey(), "SessCreateHeader");
+        super.requestSent();
+    }
+
+    @Override
+    public String toString() {
+        return "OES2 " + _remoteHostId + ' ' + _currentState;
+    }
+}
diff --git a/router/java/src/net/i2p/router/transport/udp/PacketBuilder2.java b/router/java/src/net/i2p/router/transport/udp/PacketBuilder2.java
new file mode 100644
index 0000000000..7c67268b5c
--- /dev/null
+++ b/router/java/src/net/i2p/router/transport/udp/PacketBuilder2.java
@@ -0,0 +1,1173 @@
+package net.i2p.router.transport.udp;
+
+import java.net.DatagramPacket;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.security.GeneralSecurityException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+import com.southernstorm.noise.protocol.ChaChaPolyCipherState;
+import com.southernstorm.noise.protocol.HandshakeState;
+
+import net.i2p.crypto.ChaCha20;
+import net.i2p.data.Base64;
+import net.i2p.data.DataHelper;
+import net.i2p.data.Hash;
+import net.i2p.data.router.RouterInfo;
+import net.i2p.data.SessionKey;
+import net.i2p.data.router.RouterAddress;
+import net.i2p.router.OutNetMessage;
+import net.i2p.router.RouterContext;
+import net.i2p.router.transport.TransportUtil;
+import net.i2p.router.transport.udp.PacketBuilder.Fragment;
+import net.i2p.router.transport.udp.SSU2Payload.Block;
+import static net.i2p.router.transport.udp.SSU2Util.*;
+import net.i2p.util.Addresses;
+import net.i2p.util.Log;
+
+/**
+ *  SSU2 only
+ *
+ *  @since 0.9.54
+ */
+class PacketBuilder2 {
+    private final RouterContext _context;
+    private final Log _log;
+    private final UDPTransport _transport;
+    
+    /**
+     *  For debugging and stats only - does not go out on the wire.
+     *  These are chosen to be higher than the highest I2NP message type,
+     *  as a data packet is set to the underlying I2NP message type.
+     */
+    static final int TYPE_FIRST = 62;
+    static final int TYPE_ACK = TYPE_FIRST;
+    static final int TYPE_PUNCH = 63;
+    static final int TYPE_TCB = 67;
+    static final int TYPE_TBC = 68;
+    static final int TYPE_TTA = 69;
+    static final int TYPE_TFA = 70;
+    static final int TYPE_CONF = 71;
+    static final int TYPE_SREQ = 72;
+    static final int TYPE_CREAT = 73;
+
+    /** IPv4 only */
+    public static final int IP_HEADER_SIZE = 20;
+    /** Same for IPv4 and IPv6 */
+    public static final int UDP_HEADER_SIZE = 8;
+
+    /** 74 */
+    public static final int MIN_DATA_PACKET_OVERHEAD = IP_HEADER_SIZE + UDP_HEADER_SIZE + DATA_HEADER_SIZE + MAC_LEN;
+
+    public static final int IPV6_HEADER_SIZE = 40;
+    /** 94 */
+    public static final int MIN_IPV6_DATA_PACKET_OVERHEAD = IPV6_HEADER_SIZE + UDP_HEADER_SIZE + DATA_HEADER_SIZE + MAC_LEN;
+
+/// FIXME
+    private static final int MAX_IDENTITY_FRAGMENT_SIZE = 1280 - (MIN_DATA_PACKET_OVERHEAD + KEY_LEN + MAC_LEN);
+
+    /** one byte field */
+    public static final int ABSOLUTE_MAX_ACKS = 255;
+
+    /* Higher than all other OutNetMessage priorities, but still droppable,
+     * and will be shown in the codel.UDP-Sender.drop.500 stat.
+     */
+    static final int PRIORITY_HIGH = 550;
+    private static final int PRIORITY_LOW = OutNetMessage.PRIORITY_LOWEST;
+    
+    /**
+     *  No state, all methods are thread-safe.
+     *
+     *  @param transport may be null for unit testing only
+     */
+    public PacketBuilder2(RouterContext ctx, UDPTransport transport) {
+        _context = ctx;
+        _transport = transport;
+        _log = ctx.logManager().getLog(PacketBuilder2.class);
+        // all createRateStat in UDPTransport
+    }
+
+    /**
+     *  Will a packet to 'peer' that already has 'numFragments' fragments
+     *  totalling 'curDataSize' bytes fit another fragment of size 'newFragSize' ??
+     *
+     *  This doesn't leave anything for acks.
+     *
+     *  @param numFragments &gt;= 1
+     */
+    public static int getMaxAdditionalFragmentSize(PeerState2 peer, int numFragments, int curDataSize) {
+        int available = peer.getMTU() - curDataSize;
+        if (peer.isIPv6())
+            available -= MIN_IPV6_DATA_PACKET_OVERHEAD;
+        else
+            available -= MIN_DATA_PACKET_OVERHEAD;
+        // OVERHEAD above includes 1 * FRAGMENT+HEADER_SIZE;
+        // this adds for the others, plus the new one.
+        available -= numFragments * FIRST_FRAGMENT_HEADER_SIZE;
+        return available;
+    }
+
+    /**
+     * This builds a data packet (PAYLOAD_TYPE_DATA).
+     * See the methods below for the other message types.
+     *
+     * Note that while the UDP message spec allows for more than one fragment in a message,
+     * this method writes exactly one fragment.
+     * For no fragments use buildAck().
+     * Multiple fragments in a single packet is not supported.
+     * Rekeying and extended options are not supported.
+     *
+     * So ignoring the ack bitfields, and assuming we have explicit acks,
+     * it's (47 + 4*explict acks + padding) added to the
+     * fragment length.
+     *
+     * @param ackIdsRemaining list of messageIds (Long) that should be acked by this packet.  
+     *                        The list itself is passed by reference, and if a messageId is
+     *                        transmitted it will be removed from the list.
+     *                        Not all message IDs will necessarily be sent, there may not be room.
+     *                        non-null.
+     *
+     * @param newAckCount the number of ackIdsRemaining entries that are new. These must be the first
+     *                    ones in the list
+     *
+     * @param partialACKsRemaining list of messageIds (ACKBitfield) that should be acked by this packet.  
+     *                        The list itself is passed by reference, and if a messageId is
+     *                        included, it should be removed from the list.
+     *                        Full acks in this list are skipped, they are NOT transmitted.
+     *                        non-null.
+     *                        Not all acks will necessarily be sent, there may not be room.
+     *
+     * @return null on error
+     */
+    public UDPPacket buildPacket(OutboundMessageState state, int fragment, PeerState2 peer,
+                                 Collection<Long> ackIdsRemaining, int newAckCount,
+                                 List<ACKBitfield> partialACKsRemaining) {
+        List<Fragment> frags = Collections.singletonList(new Fragment(state, fragment));
+        return buildPacket(frags, peer, ackIdsRemaining, newAckCount, partialACKsRemaining);
+    }
+
+    /*
+     *  Multiple fragments
+     *
+     */
+    public UDPPacket buildPacket(List<Fragment> fragments, PeerState2 peer,
+                                 Collection<Long> ackIdsRemaining, int newAckCount,
+                                 List<ACKBitfield> partialACKsRemaining) {
+        // calculate data size
+        int numFragments = fragments.size();
+        int dataSize = 0;
+        int priority = 0;
+        for (int i = 0; i < numFragments; i++) {
+            Fragment frag = fragments.get(i);
+            OutboundMessageState state = frag.state;
+            int pri = state.getPriority();
+            if (pri > priority)
+                priority = pri;
+            int fragment = frag.num;
+            int sz = state.fragmentSize(fragment);
+            dataSize += sz;
+        }
+        
+        if (dataSize < 0)
+            return null;
+
+        // calculate size available for acks
+        int currentMTU = peer.getMTU();
+        int availableForAcks = currentMTU - dataSize;
+        int ipHeaderSize;
+        if (peer.isIPv6()) {
+            availableForAcks -= MIN_IPV6_DATA_PACKET_OVERHEAD;
+            ipHeaderSize = IPV6_HEADER_SIZE;
+        } else {
+            availableForAcks -= MIN_DATA_PACKET_OVERHEAD;
+            ipHeaderSize = IP_HEADER_SIZE;
+        }
+        if (numFragments > 1)
+            availableForAcks -= (numFragments - 1) * FIRST_FRAGMENT_HEADER_SIZE;
+        int availableForExplicitAcks = availableForAcks;
+
+        // make the packet
+        long pktNum = peer.getNextPacketNumber();
+        UDPPacket packet = buildShortPacketHeader(peer.getSendConnID(), pktNum, DATA_FLAG_BYTE);
+        DatagramPacket pkt = packet.getPacket();
+        byte data[] = pkt.getData();
+        int off = SHORT_HEADER_SIZE;
+
+        // ok, now for the body...
+        // +2 for acks and padding
+        List<Block> blocks = new ArrayList<Block>(fragments.size() + 2);
+        
+/*
+        // partial acks have priority but they are after explicit acks in the packet
+        // so we have to compute the space in advance
+        int partialAcksToSend = 0;
+        if (availableForExplicitAcks >= 6 && !partialACKsRemaining.isEmpty()) {
+            for (ACKBitfield bf : partialACKsRemaining) {
+                if (partialAcksToSend >= ABSOLUTE_MAX_ACKS)
+                    break;  // ack count
+                // only send what we have to
+                //int acksz = 4 + (bf.fragmentCount() / 7) + 1;
+                int bits = bf.highestReceived() + 1;
+                if (bits <= 0)
+                    continue;
+                int acksz = bits / 7;
+                if (bits % 7 > 0)
+                    acksz++;
+                acksz += 4;
+                if (partialAcksToSend == 0)
+                    acksz++;  // ack count
+                if (availableForExplicitAcks >= acksz) {
+                    availableForExplicitAcks -= acksz;
+                    partialAcksToSend++;
+                } else {
+                    break;
+                }
+            }
+        }
+
+
+        // always send all the new acks if we have room
+        int explicitToSend = Math.min(ABSOLUTE_MAX_ACKS,
+                                      Math.min(newAckCount + (currentMTU > PeerState.MIN_MTU ? MAX_RESEND_ACKS_LARGE : MAX_RESEND_ACKS_SMALL),
+                                               Math.min((availableForExplicitAcks - 1) / 4, ackIdsRemaining.size())));
+        if (explicitToSend > 0) {
+            data[off++] = (byte) explicitToSend;
+            Iterator<Long> iter = ackIdsRemaining.iterator();
+            for (int i = 0; i < explicitToSend && iter.hasNext(); i++) {
+                Long ackId = iter.next();
+                iter.remove();
+            }
+            //acksIncluded = true;
+        }
+*/
+
+/*
+        if (partialAcksToSend > 0) {
+            if (msg != null)
+                msg.append(partialAcksToSend).append(" partial acks included:");
+            int origNumRemaining = partialACKsRemaining.size();
+            int numPartialOffset = off;
+            // leave it blank for now, since we could skip some
+            off++;
+            Iterator<ACKBitfield> iter = partialACKsRemaining.iterator();
+            for (int i = 0; i < partialAcksToSend && iter.hasNext(); i++) {
+                ACKBitfield bitfield = iter.next();
+                if (bitfield.receivedComplete()) continue;
+                // only send what we have to
+                //int bits = bitfield.fragmentCount();
+                int bits = bitfield.highestReceived() + 1;
+                if (bits <= 0)
+                    continue;
+                int size = bits / 7;
+                if (bits % 7 > 0)
+                    size++;
+                DataHelper.toLong(data, off, 4, bitfield.getMessageId());
+                off += 4;
+                for (int curByte = 0; curByte < size; curByte++) {
+                    if (curByte + 1 < size)
+                        data[off] = (byte)(1 << 7);
+                    else
+                        data[off] = 0;
+                    
+                    for (int curBit = 0; curBit < 7; curBit++) {
+                        if (bitfield.received(curBit + 7*curByte))
+                            data[off] |= (byte)(1 << curBit);
+                    }
+                    off++;
+                }
+                iter.remove();
+            }
+            DataHelper.toLong(data, numPartialOffset, 1, origNumRemaining - partialACKsRemaining.size());
+        }
+*/
+        
+        // now write each fragment
+        int sizeWritten = 0;
+        for (int i = 0; i < numFragments; i++) {
+            Fragment frag = fragments.get(i);
+            OutboundMessageState state = frag.state;
+            int fragment = frag.num;
+            int count = state.getFragmentCount();
+            Block block;
+            if (fragment == 0) {
+                if (count == 1)
+                    block = new SSU2Payload.I2NPBlock(state);
+                else
+                    block = new SSU2Payload.FirstFragBlock(state);
+            } else {
+                block = new SSU2Payload.FollowFragBlock(state, fragment);
+            }
+            blocks.add(block);
+            int sz = block.getTotalLength();
+            off += sz;
+            sizeWritten += sz;
+        }
+        Block block = getPadding(sizeWritten, peer.getMTU());
+        if (block != null) {
+            blocks.add(block);
+            int sz = block.getTotalLength();
+            off += sz;
+            sizeWritten += sz;
+        }
+        SSU2Payload.writePayload(data, SHORT_HEADER_SIZE, blocks);
+        pkt.setLength(off);
+
+        encryptDataPacket(packet, peer.getSendEncryptKey(), pktNum, peer.getSendHeaderEncryptKey1(), peer.getSendHeaderEncryptKey2());
+        setTo(packet, peer.getRemoteIPAddress(), peer.getRemotePort());
+        
+        // FIXME ticket #2675
+        // the packet could have been built before the current mtu got lowered, so
+        // compare to LARGE_MTU
+        // Also happens on switch between IPv4 and IPv6
+        if (_log.shouldWarn()) {
+            int maxMTU = peer.isIPv6() ? PeerState.MAX_IPV6_MTU : PeerState.LARGE_MTU;
+            if (off + (ipHeaderSize + UDP_HEADER_SIZE) > maxMTU) {
+                _log.warn("Size is " + off + " for " + packet +
+                       " data size " + dataSize +
+                       " pkt size " + (off + (ipHeaderSize + UDP_HEADER_SIZE)) +
+                       " MTU " + currentMTU +
+/*
+                       ' ' + availableForAcks + " for all acks, " +
+                       availableForExplicitAcks + " for full acks, " + 
+                       explicitToSend + " full acks included, " +
+                       partialAcksToSend + " partial acks included, " +
+*/
+                       " Fragments: " + DataHelper.toString(fragments), new Exception());
+            }
+        }
+        
+        packet.setPriority(priority);
+        return packet;
+    }
+    
+    /**
+     * An ACK packet with no acks.
+     * We use this for keepalive purposes.
+     * It doesn't generate a reply, but that's ok.
+     */
+    public UDPPacket buildPing(PeerState2 peer) {
+        return buildACK(peer, Collections.<ACKBitfield> emptyList());
+    }
+
+    /**
+     *  Build the ack packet. The list need not be sorted into full and partial;
+     *  this method will put all fulls before the partials in the outgoing packet.
+     *  An ack packet is just a data packet with no data.
+     *  See buildPacket() for format.
+     *
+     *  TODO MTU not enforced.
+     *  TODO handle huge number of acks better
+     *
+     * @param ackBitfields list of ACKBitfield instances to either fully or partially ACK
+     */
+    public UDPPacket buildACK(PeerState2 peer, List<ACKBitfield> ackBitfields) {
+        long pktNum = peer.getNextPacketNumber();
+        UDPPacket packet = buildShortPacketHeader(peer.getSendConnID(), pktNum, DATA_FLAG_BYTE);
+        DatagramPacket pkt = packet.getPacket();
+        byte data[] = pkt.getData();
+        int off = SHORT_HEADER_SIZE;
+
+        int fullACKCount = 0;
+        int partialACKCount = 0;
+/*
+        for (int i = 0; i < ackBitfields.size(); i++) {
+            if (ackBitfields.get(i).receivedComplete())
+                fullACKCount++;
+            else
+                partialACKCount++;
+        }
+
+// sort, create ranges
+
+        // FIXME do better than this, we could still exceed MTU
+        if (fullACKCount > ABSOLUTE_MAX_ACKS ||
+            partialACKCount > ABSOLUTE_MAX_ACKS)
+            throw new IllegalArgumentException("Too many acks full/partial " + fullACKCount +
+                                               '/' + partialACKCount);
+
+        // ok, now for the body...
+        if (fullACKCount > 0)
+            data[off] |= UDPPacket.DATA_FLAG_EXPLICIT_ACK;
+        if (partialACKCount > 0)
+            data[off] |= UDPPacket.DATA_FLAG_ACK_BITFIELDS;
+        // add ECN if (peer.getSomethingOrOther())
+        off++;
+        
+        if (fullACKCount > 0) {
+            data[off++] = (byte) fullACKCount;
+            for (int i = 0; i < ackBitfields.size(); i++) {
+                ACKBitfield bf = ackBitfields.get(i);
+                if (bf.receivedComplete()) {
+                    DataHelper.toLong(data, off, 4, bf.getMessageId());
+                    off += 4;
+                }
+            }
+        }
+        
+        if (partialACKCount > 0) {
+            data[off++] = (byte) partialACKCount;
+            for (int i = 0; i < ackBitfields.size(); i++) {
+                ACKBitfield bitfield = ackBitfields.get(i);
+                if (bitfield.receivedComplete()) continue;
+                DataHelper.toLong(data, off, 4, bitfield.getMessageId());
+                off += 4;
+                // only send what we have to
+                //int bits = bitfield.fragmentCount();
+                int bits = bitfield.highestReceived() + 1;
+                int size = bits / 7;
+                if (bits == 0 || bits % 7 > 0)
+                    size++;
+                for (int curByte = 0; curByte < size; curByte++) {
+                    if (curByte + 1 < size)
+                        data[off] = (byte)(1 << 7);
+                    else
+                        data[off] = 0;
+                    
+                    for (int curBit = 0; curBit < 7; curBit++) {
+                        if (bitfield.received(curBit + 7*curByte))
+                            data[off] |= (byte)(1 << curBit);
+                    }
+                    off++;
+                }
+            }
+        }
+*/
+        
+        pkt.setLength(off);
+        encryptDataPacket(packet, peer.getSendEncryptKey(), pktNum, peer.getSendHeaderEncryptKey1(), peer.getSendHeaderEncryptKey2());
+        setTo(packet, peer.getRemoteIPAddress(), peer.getRemotePort());
+        packet.setMessageType(TYPE_ACK);
+        packet.setPriority((fullACKCount > 0 || partialACKCount > 0) ? PRIORITY_HIGH : PRIORITY_LOW);
+        return packet;
+    }
+    
+    /**
+     * Build a new SessionRequest packet for the given peer, encrypting it 
+     * as necessary.
+     * 
+     * @return ready to send packet, or null if there was a problem
+     */
+    public UDPPacket buildTokenRequestPacket(OutboundEstablishState2 state) {
+        long n = _context.random().signedNextInt() & 0xFFFFFFFFL;
+        UDPPacket packet = buildLongPacketHeader(state.getSendConnID(), n, SESSION_REQUEST_FLAG_BYTE,
+                                                 state.getRcvConnID(), 0);
+        DatagramPacket pkt = packet.getPacket();
+
+        byte toIP[] = state.getSentIP();
+        if (!_transport.isValid(toIP)) {
+            packet.release();
+            return null;
+        }
+        InetAddress to = null;
+        try {
+            to = InetAddress.getByAddress(toIP);
+        } catch (UnknownHostException uhe) {
+            if (_log.shouldLog(Log.ERROR))
+                _log.error("How did we think this was a valid IP?  " + state.getRemoteHostId());
+            packet.release();
+            return null;
+        }
+        
+        pkt.setLength(LONG_HEADER_SIZE);
+        byte[] introKey = state.getSendHeaderEncryptKey1();
+        encryptTokenRequest(packet, introKey, n, introKey, introKey);
+        state.requestSent();
+        setTo(packet, to, state.getSentPort());
+        packet.setMessageType(TYPE_SREQ);
+        packet.setPriority(PRIORITY_HIGH);
+        return packet;
+    }
+    
+    /**
+     * Build a new SessionRequest packet for the given peer, encrypting it 
+     * as necessary.
+     * 
+     * @return ready to send packet, or null if there was a problem
+     */
+    public UDPPacket buildSessionRequestPacket(OutboundEstablishState2 state) {
+        UDPPacket packet = buildLongPacketHeader(state.getSendConnID(), 0, SESSION_REQUEST_FLAG_BYTE,
+                                                 state.getRcvConnID(), state.getToken());
+        DatagramPacket pkt = packet.getPacket();
+
+        byte toIP[] = state.getSentIP();
+        if (!_transport.isValid(toIP)) {
+            packet.release();
+            return null;
+        }
+        InetAddress to = null;
+        try {
+            to = InetAddress.getByAddress(toIP);
+        } catch (UnknownHostException uhe) {
+            if (_log.shouldLog(Log.ERROR))
+                _log.error("How did we think this was a valid IP?  " + state.getRemoteHostId());
+            packet.release();
+            return null;
+        }
+        
+        pkt.setLength(LONG_HEADER_SIZE);
+        byte[] introKey = state.getSendHeaderEncryptKey1();
+        encryptSessionRequest(packet, state.getHandshakeState(), introKey, introKey, state.needIntroduction());
+        state.requestSent();
+        setTo(packet, to, state.getSentPort());
+        packet.setMessageType(TYPE_SREQ);
+        packet.setPriority(PRIORITY_HIGH);
+        return packet;
+    }
+    
+    /**
+     * Build a new SessionCreated packet for the given peer, encrypting it 
+     * as necessary.
+     * 
+     * @return ready to send packet, or null if there was a problem
+     */
+    public UDPPacket buildSessionCreatedPacket(InboundEstablishState2 state) {
+        UDPPacket packet = buildLongPacketHeader(state.getSendConnID(), 0, SESSION_CREATED_FLAG_BYTE,
+                                                 state.getRcvConnID(), state.getToken());
+        DatagramPacket pkt = packet.getPacket();
+        
+        byte sentIP[] = state.getSentIP();
+        pkt.setLength(LONG_HEADER_SIZE);
+        int port = state.getSentPort();
+        encryptSessionCreated(packet, state.getHandshakeState(), state.getSendHeaderEncryptKey1(),
+                              state.getSendHeaderEncryptKey2(), state.getSentRelayTag(), state.getNextToken(),
+                              sentIP, port);
+        state.createdPacketSent();
+        pkt.setSocketAddress(state.getSentAddress());
+        packet.setMessageType(TYPE_CREAT);
+        packet.setPriority(PRIORITY_HIGH);
+        return packet;
+    }
+    
+    /**
+     * Build a new Retry packet for the given peer, encrypting it 
+     * as necessary.
+     * 
+     * @return ready to send packet, or null if there was a problem
+     */
+    public UDPPacket buildRetryPacket(InboundEstablishState2 state) {
+        long n = _context.random().signedNextInt() & 0xFFFFFFFFL;
+        UDPPacket packet = buildLongPacketHeader(state.getSendConnID(), n, RETRY_FLAG_BYTE,
+                                                 state.getRcvConnID(), state.getToken());
+        DatagramPacket pkt = packet.getPacket();
+        
+        byte sentIP[] = state.getSentIP();
+        pkt.setLength(LONG_HEADER_SIZE);
+        int port = state.getSentPort();
+        encryptRetry(packet, state.getSendHeaderEncryptKey1(), n, state.getSendHeaderEncryptKey1(),
+                     state.getSendHeaderEncryptKey2(),
+                     sentIP, port);
+        state.retryPacketSent();
+        pkt.setSocketAddress(state.getSentAddress());
+        packet.setMessageType(TYPE_CREAT);
+        packet.setPriority(PRIORITY_HIGH);
+        return packet;
+    }
+    
+    /**
+     * Build a new series of SessionConfirmed packets for the given peer, 
+     * encrypting it as necessary.
+     *
+     * Note that while a SessionConfirmed could in theory be fragmented,
+     * in practice a RouterIdentity is 387 bytes and a single fragment is 512 bytes max,
+     * so it will never be fragmented.
+     * 
+     * @return ready to send packets, or null if there was a problem
+     * 
+     * TODO: doesn't really return null, and caller doesn't handle null return
+     * (null SigningPrivateKey should cause this?)
+     * Should probably return null if buildSessionConfirmedPacket() returns null for any fragment
+     */
+    public UDPPacket[] buildSessionConfirmedPackets(OutboundEstablishState2 state, RouterInfo ourInfo) {
+        boolean gzip = false;
+        byte info[] = ourInfo.toByteArray();
+        int mtu = state.getMTU();
+        byte toIP[] = state.getSentIP();
+        // 20 + 8 + 16 + 32 + 16 + 16 + 3 + 2 = 113
+        // 40 + 8 + 16 + 32 + 16 + 16 + 3 + 2 = 133
+        int overhead = (toIP.length == 16 ? IPV6_HEADER_SIZE : IP_HEADER_SIZE) +
+                       UDP_HEADER_SIZE + SHORT_HEADER_SIZE + KEY_LEN + MAC_LEN + MAC_LEN +
+                       SSU2Payload.BLOCK_HEADER_SIZE + 2;  // RIBlock flags
+        int max = mtu - overhead;
+
+        int numFragments = info.length / max;
+        if (numFragments * max != info.length)
+            numFragments++;
+
+        if (numFragments > 1) {
+            byte[] gzipped = DataHelper.compress(info, 0, info.length, DataHelper.MAX_COMPRESSION);
+            if (gzipped.length < info.length) {
+                if (_log.shouldWarn())
+                    _log.warn("Gzipping RI, max is " + max + " size was " + info.length + " size now " + gzipped.length);
+                gzip = true;
+                info = gzipped;
+                numFragments = info.length / max;
+                if (numFragments * max != info.length)
+                    numFragments++;
+            }
+        }
+
+        int len;
+        if (numFragments > 1) {
+            if (_log.shouldWarn())
+                _log.warn("RI size " + info.length + " requires " + numFragments + " packets");
+            len = max;
+        } else {
+            len = info.length;
+        }
+
+
+        UDPPacket packets[] = new UDPPacket[numFragments];
+        packets[0] = buildSessionConfirmedPacket(state, numFragments, info, len, gzip);
+        if (numFragments > 1) {
+            // get PeerState from OES
+            for (int i = 1; i < numFragments; i++) {
+                //packets[i] = buildSessionConfirmedPacket(state, i, numFragments, info, gzip);
+            }
+            // TODO numFragments > 1 requires shift to data phase
+            throw new IllegalArgumentException("TODO");
+        }
+        state.confirmedPacketsSent();
+        return packets;
+    }
+
+    /**
+     * Build a new SessionConfirmed packet for the given peer
+     * 
+     * @return ready to send packet, or null if there was a problem
+     */
+    private UDPPacket buildSessionConfirmedPacket(OutboundEstablishState2 state, int numFragments, byte ourInfo[], int len, boolean gzip) {
+        UDPPacket packet = buildShortPacketHeader(state.getSendConnID(), 1, SESSION_CONFIRMED_FLAG_BYTE);
+        DatagramPacket pkt = packet.getPacket();
+
+        InetAddress to = null;
+        try {
+            to = InetAddress.getByAddress(state.getSentIP());
+        } catch (UnknownHostException uhe) {
+            if (_log.shouldLog(Log.ERROR))
+                _log.error("How did we think this was a valid IP?  " + state.getRemoteHostId());
+            packet.release();
+            return null;
+        }
+        
+        pkt.setLength(SHORT_HEADER_SIZE);
+        SSU2Payload.RIBlock block = new SSU2Payload.RIBlock(ourInfo,  0, len,
+                                                            false, gzip, 0, numFragments);
+        encryptSessionConfirmed(packet, state.getHandshakeState(), state.getMTU(),
+                                state.getSendHeaderEncryptKey1(), state.getSendHeaderEncryptKey2(), block, state.getNextToken());
+        setTo(packet, to, state.getSentPort());
+        packet.setMessageType(TYPE_CONF);
+        packet.setPriority(PRIORITY_HIGH);
+        return packet;
+    }
+
+    /**
+     * Build a packet as if we are Alice and we either want Bob to begin a 
+     * peer test or Charlie to finish a peer test.
+     * 
+     * @return ready to send packet, or null if there was a problem
+     */
+/*
+    public UDPPacket buildPeerTestFromAlice(InetAddress toIP, int toPort, SessionKey toIntroKey, long nonce, SessionKey aliceIntroKey) {
+        return buildPeerTestFromAlice(toIP, toPort, toIntroKey, toIntroKey, nonce, aliceIntroKey);
+    }
+*/
+
+    /**
+     * Build a packet as if we are Alice and we either want Bob to begin a 
+     * peer test or Charlie to finish a peer test.
+     * 
+     * @return ready to send packet, or null if there was a problem
+     */
+/*
+    public UDPPacket buildPeerTestFromAlice(InetAddress toIP, int toPort, SessionKey toCipherKey, SessionKey toMACKey,
+                                            long nonce, SessionKey aliceIntroKey) {
+        UDPPacket packet = buildShortPacketHeader(PEER_TEST_FLAG_BYTE);
+        DatagramPacket pkt = packet.getPacket();
+        byte data[] = pkt.getData();
+        int off = SHORT_HEADER_SIZE;
+        if (_log.shouldLog(Log.DEBUG))
+            _log.debug("Sending peer test " + nonce + " to Bob");
+        
+        // now for the body
+        DataHelper.toLong(data, off, 4, nonce);
+        off += 4;
+        data[off++] = 0; // neither Bob nor Charlie need Alice's IP from her
+        DataHelper.toLong(data, off, 2, 0); // neither Bob nor Charlie need Alice's port from her
+        off += 2;
+        System.arraycopy(aliceIntroKey.getData(), 0, data, off, SessionKey.KEYSIZE_BYTES);
+        off += SessionKey.KEYSIZE_BYTES;
+        
+        pkt.setLength(off);
+        authenticate(packet, toCipherKey, toMACKey);
+        setTo(packet, toIP, toPort);
+        packet.setMessageType(TYPE_TFA);
+        packet.setPriority(PRIORITY_LOW);
+        return packet;
+    }
+*/
+
+    /**
+     * Build a packet as if we are either Bob or Charlie and we are helping test Alice.
+     * Not for use as Bob, as of 0.9.52; use in-session cipher/mac keys instead.
+     *
+     * @return ready to send packet, or null if there was a problem
+     */
+/*
+    public UDPPacket buildPeerTestToAlice(InetAddress aliceIP, int alicePort,
+                                          SessionKey aliceIntroKey, SessionKey charlieIntroKey, long nonce) {
+        return buildPeerTestToAlice(aliceIP, alicePort, aliceIntroKey, aliceIntroKey, charlieIntroKey, nonce);
+    }
+*/
+
+    /**
+     * Build a packet as if we are either Bob or Charlie and we are helping test Alice.
+     * 
+     * @param aliceCipherKey the intro key if we are Charlie
+     * @param aliceMACKey the intro key if we are Charlie
+     * @return ready to send packet, or null if there was a problem
+     */
+/*
+    public UDPPacket buildPeerTestToAlice(InetAddress aliceIP, int alicePort,
+                                          SessionKey aliceCipherKey, SessionKey aliceMACKey,
+                                          SessionKey charlieIntroKey, long nonce) {
+        UDPPacket packet = buildShortPacketHeader(PEER_TEST_FLAG_BYTE);
+        DatagramPacket pkt = packet.getPacket();
+        byte data[] = pkt.getData();
+        int off = SHORT_HEADER_SIZE;
+        if (_log.shouldLog(Log.DEBUG))
+            _log.debug("Sending peer test " + nonce + " to Alice");
+        
+        // now for the body
+        DataHelper.toLong(data, off, 4, nonce);
+        off += 4;
+        byte ip[] = aliceIP.getAddress();
+        data[off++] = (byte) ip.length;
+        System.arraycopy(ip, 0, data, off, ip.length);
+        off += ip.length;
+        DataHelper.toLong(data, off, 2, alicePort);
+        off += 2;
+        System.arraycopy(charlieIntroKey.getData(), 0, data, off, SessionKey.KEYSIZE_BYTES);
+        off += SessionKey.KEYSIZE_BYTES;
+        
+        pkt.setLength(off);
+        authenticate(packet, aliceCipherKey, aliceMACKey);
+        setTo(packet, aliceIP, alicePort);
+        packet.setMessageType(TYPE_TTA);
+        packet.setPriority(PRIORITY_LOW);
+        return packet;
+    }
+*/
+
+    /**
+     * Build a packet as if we are Bob sending Charlie a packet to help test Alice.
+     * 
+     * @return ready to send packet, or null if there was a problem
+     */
+/*
+    public UDPPacket buildPeerTestToCharlie(InetAddress aliceIP, int alicePort, SessionKey aliceIntroKey, long nonce, 
+                                            InetAddress charlieIP, int charliePort, 
+                                            SessionKey charlieCipherKey, SessionKey charlieMACKey) {
+        UDPPacket packet = buildShortPacketHeader(PEER_TEST_FLAG_BYTE);
+        DatagramPacket pkt = packet.getPacket();
+        byte data[] = pkt.getData();
+        int off = SHORT_HEADER_SIZE;
+        if (_log.shouldLog(Log.DEBUG))
+            _log.debug("Sending peer test " + nonce + " to Charlie");
+        
+        // now for the body
+        DataHelper.toLong(data, off, 4, nonce);
+        off += 4;
+        byte ip[] = aliceIP.getAddress();
+        data[off++] = (byte) ip.length;
+        System.arraycopy(ip, 0, data, off, ip.length);
+        off += ip.length;
+        DataHelper.toLong(data, off, 2, alicePort);
+        off += 2;
+        System.arraycopy(aliceIntroKey.getData(), 0, data, off, SessionKey.KEYSIZE_BYTES);
+        off += SessionKey.KEYSIZE_BYTES;
+        
+        pkt.setLength(off);
+        authenticate(packet, charlieCipherKey, charlieMACKey);
+        setTo(packet, charlieIP, charliePort);
+        packet.setMessageType(TYPE_TBC);
+        packet.setPriority(PRIORITY_LOW);
+        return packet;
+    }
+*/
+    
+    /**
+     * Build a packet as if we are Charlie sending Bob a packet verifying that we will help test Alice.
+     * 
+     * @return ready to send packet, or null if there was a problem
+     */
+/*
+    public UDPPacket buildPeerTestToBob(InetAddress bobIP, int bobPort, InetAddress aliceIP, int alicePort,
+                                        SessionKey aliceIntroKey, long nonce,
+                                        SessionKey bobCipherKey, SessionKey bobMACKey) {
+        UDPPacket packet = buildShortPacketHeader(PEER_TEST_FLAG_BYTE);
+        DatagramPacket pkt = packet.getPacket();
+        byte data[] = pkt.getData();
+        int off = SHORT_HEADER_SIZE;
+        if (_log.shouldLog(Log.DEBUG))
+            _log.debug("Sending peer test " + nonce + " to Bob");
+        
+        // now for the body
+        DataHelper.toLong(data, off, 4, nonce);
+        off += 4;
+        byte ip[] = aliceIP.getAddress();
+        data[off++] = (byte) ip.length;
+        System.arraycopy(ip, 0, data, off, ip.length);
+        off += ip.length;
+        DataHelper.toLong(data, off, 2, alicePort);
+        off += 2;
+        System.arraycopy(aliceIntroKey.getData(), 0, data, off, SessionKey.KEYSIZE_BYTES);
+        off += SessionKey.KEYSIZE_BYTES;
+        
+        pkt.setLength(off);
+        authenticate(packet, bobCipherKey, bobMACKey);
+        setTo(packet, bobIP, bobPort);
+        packet.setMessageType(TYPE_TCB);
+        packet.setPriority(PRIORITY_LOW);
+        return packet;
+    }
+*/
+
+    /**
+     *  Creates an empty unauthenticated packet for hole punching.
+     *  Parameters must be validated previously.
+     */
+    public UDPPacket buildHolePunch(InetAddress to, int port) {
+        UDPPacket packet = UDPPacket.acquire(_context, false);
+        if (_log.shouldLog(Log.INFO))
+            _log.info("Sending relay hole punch to " + to + ":" + port);
+
+        // the packet is empty and does not need to be authenticated, since
+        // its just for hole punching
+        packet.getPacket().setLength(0);
+        setTo(packet, to, port);
+        
+        packet.setMessageType(TYPE_PUNCH);
+        packet.setPriority(PRIORITY_HIGH);
+        return packet;
+    }
+    
+    /**
+     *  @param pktNum 0 - 0xFFFFFFFF
+     *  @return a packet with the first 32 bytes filled in
+     */
+    private UDPPacket buildLongPacketHeader(long destID, long pktNum, byte type, long srcID, long token) {
+        if (_log.shouldDebug())
+            _log.debug("Building long header destID " + destID + " pkt num " + pktNum + " type " + type + " srcID " + srcID + " token " + token);
+        UDPPacket packet = buildShortPacketHeader(destID, pktNum, type);
+        byte data[] = packet.getPacket().getData();
+        data[13] = PROTOCOL_VERSION;
+        data[14] = (byte) _context.router().getNetworkID();
+        DataHelper.toLong8(data, 16, srcID);
+        DataHelper.toLong8(data, 24, token);
+        return packet;
+    }
+    
+    /**
+     *  @param pktNum 0 - 0xFFFFFFFF
+     *  @return a packet with the first 16 bytes filled in
+     */
+    private UDPPacket buildShortPacketHeader(long destID, long pktNum, byte type) {
+        UDPPacket packet = UDPPacket.acquire(_context, false);
+        byte data[] = packet.getPacket().getData();
+        Arrays.fill(data, 0, data.length, (byte) 0);
+        DataHelper.toLong8(data, 0, destID);
+        DataHelper.toLong(data, 8, 4, pktNum);
+        data[12] = type;
+        return packet;
+    }
+
+    private static void setTo(UDPPacket packet, InetAddress ip, int port) {
+        DatagramPacket pkt = packet.getPacket();
+        pkt.setAddress(ip);
+        pkt.setPort(port);
+    }
+
+    /**
+     *  @param packet containing only 32 byte header
+     */
+    private void encryptSessionRequest(UDPPacket packet, HandshakeState state,
+                                       byte[] hdrKey1, byte[] hdrKey2, boolean needIntro) {
+        DatagramPacket pkt = packet.getPacket();
+        byte data[] = pkt.getData();
+        int off = pkt.getOffset();
+        try {
+            if (_log.shouldDebug())
+                _log.debug("After start: " + state);
+            List<Block> blocks = new ArrayList<Block>(3);
+            Block block = new SSU2Payload.DateTimeBlock(_context);
+            int len = block.getTotalLength();
+            blocks.add(block);
+            if (needIntro) {
+                block = new SSU2Payload.RelayTagRequestBlock();
+                len += block.getTotalLength();
+                blocks.add(block);
+            }
+            // plenty of room
+            block = getPadding(len, 1280);
+            len += block.getTotalLength();
+            blocks.add(block);
+
+            // If we skip past where the ephemeral key will be, we can
+            // use the packet for the plaintext and Noise will symmetric encrypt in-place
+            SSU2Payload.writePayload(data, off + LONG_HEADER_SIZE + KEY_LEN, blocks);
+            state.start();
+            if (_log.shouldDebug())
+                _log.debug("State after start: " + state);
+            state.mixHash(data, off, LONG_HEADER_SIZE);
+            if (_log.shouldDebug())
+                _log.debug("State after mixHash 1: " + state);
+            state.writeMessage(data, off + LONG_HEADER_SIZE, data, off + LONG_HEADER_SIZE + KEY_LEN, len);
+            pkt.setLength(pkt.getLength() + KEY_LEN + len + MAC_LEN);
+        } catch (RuntimeException re) {
+            if (!_log.shouldWarn())
+                _log.error("Bad msg 1 out", re);
+            throw re;
+        } catch (GeneralSecurityException gse) {
+            if (!_log.shouldWarn())
+                _log.error("Bad msg 1 out", gse);
+            throw new RuntimeException("Bad msg 1 out", gse);
+        }
+        if (_log.shouldDebug())
+            _log.debug("After msg 1: " + state + '\n' + net.i2p.util.HexDump.dump(data, off, pkt.getLength()));
+        SSU2Header.encryptHandshakeHeader(packet, hdrKey1, hdrKey2);
+        if (_log.shouldDebug())
+            _log.debug("Hdr key 1: " + Base64.encode(hdrKey1) + " Hdr key 2: " + Base64.encode(hdrKey2));
+    }
+
+    /**
+     *  @param packet containing only 32 byte header
+     */
+    private void encryptSessionCreated(UDPPacket packet, HandshakeState state,
+                                       byte[] hdrKey1, byte[] hdrKey2, long relayTag, long token, byte[] ip, int port) {
+        DatagramPacket pkt = packet.getPacket();
+        byte data[] = pkt.getData();
+        int off = pkt.getOffset();
+        try {
+            List<Block> blocks = new ArrayList<Block>(4);
+            Block block = new SSU2Payload.DateTimeBlock(_context);
+            int len = block.getTotalLength();
+            blocks.add(block);
+            block = new SSU2Payload.AddressBlock(ip, port);
+            len += block.getTotalLength();
+            blocks.add(block);
+            if (relayTag > 0) {
+                block = new SSU2Payload.RelayTagBlock(relayTag);
+                len += block.getTotalLength();
+                blocks.add(block);
+            }
+            if (token > 0) {
+                block = new SSU2Payload.NewTokenBlock(token, _context.clock().now() + 6*24*60*60*1000L);
+                len += block.getTotalLength();
+                blocks.add(block);
+            }
+            // plenty of room
+            block = getPadding(len, 1280);
+            len += block.getTotalLength();
+            blocks.add(block);
+
+            // If we skip past where the ephemeral key will be, we can
+            // use the packet for the plaintext and Noise will symmetric encrypt in-place
+            SSU2Payload.writePayload(data, off + LONG_HEADER_SIZE + KEY_LEN, blocks);
+
+            state.mixHash(data, off, LONG_HEADER_SIZE);
+            if (_log.shouldDebug())
+                _log.debug("State after mixHash 2: " + state);
+            state.writeMessage(data, off + LONG_HEADER_SIZE, data, off + LONG_HEADER_SIZE + KEY_LEN, len);
+            pkt.setLength(pkt.getLength() + KEY_LEN + len + MAC_LEN);
+        } catch (RuntimeException re) {
+            if (!_log.shouldWarn())
+                _log.error("Bad msg 2 out", re);
+            throw re;
+        } catch (GeneralSecurityException gse) {
+            if (!_log.shouldWarn())
+                _log.error("Bad msg 2 out", gse);
+            throw new RuntimeException("Bad msg 2 out", gse);
+        }
+        if (_log.shouldDebug())
+            _log.debug("After msg 2: " + state);
+        SSU2Header.encryptHandshakeHeader(packet, hdrKey1, hdrKey2);
+    }
+
+    /**
+     *  @param packet containing only 32 byte header
+     */
+    private void encryptRetry(UDPPacket packet, byte[] chachaKey, long n,
+                              byte[] hdrKey1, byte[] hdrKey2, byte[] ip, int port) {
+        DatagramPacket pkt = packet.getPacket();
+        byte data[] = pkt.getData();
+        int off = pkt.getOffset();
+        try {
+            List<Block> blocks = new ArrayList<Block>(4);
+            Block block = new SSU2Payload.DateTimeBlock(_context);
+            int len = block.getTotalLength();
+            blocks.add(block);
+            block = new SSU2Payload.AddressBlock(ip, port);
+            len += block.getTotalLength();
+            blocks.add(block);
+            // plenty of room
+            block = getPadding(len, 1280);
+            len += block.getTotalLength();
+            blocks.add(block);
+            byte[] payload = new byte[len];
+            SSU2Payload.writePayload(payload, 0, blocks);
+
+            ChaChaPolyCipherState chacha = new ChaChaPolyCipherState();
+            chacha.initializeKey(chachaKey, 0);
+            chacha.setNonce(n);
+            chacha.encryptWithAd(data, off, LONG_HEADER_SIZE,
+                                 data, off + LONG_HEADER_SIZE, data, off + LONG_HEADER_SIZE, len);
+
+            pkt.setLength(pkt.getLength() + len + MAC_LEN);
+        } catch (RuntimeException re) {
+            if (!_log.shouldWarn())
+                _log.error("Bad retry msg out", re);
+            throw re;
+        } catch (GeneralSecurityException gse) {
+            if (!_log.shouldWarn())
+                _log.error("Bad retry msg out", gse);
+            throw new RuntimeException("Bad retry msg out", gse);
+        }
+        SSU2Header.encryptLongHeader(packet, hdrKey1, hdrKey2);
+    }
+
+    /**
+     *  @param packet containing only 32 byte header
+     */
+    private void encryptTokenRequest(UDPPacket packet, byte[] chachaKey, long n,
+                                     byte[] hdrKey1, byte[] hdrKey2) {
+        DatagramPacket pkt = packet.getPacket();
+        byte data[] = pkt.getData();
+        int off = pkt.getOffset();
+        try {
+            List<Block> blocks = new ArrayList<Block>(4);
+            Block block = new SSU2Payload.DateTimeBlock(_context);
+            int len = block.getTotalLength();
+            blocks.add(block);
+            // plenty of room
+            block = getPadding(len, 1280);
+            len += block.getTotalLength();
+            blocks.add(block);
+            byte[] payload = new byte[len];
+            SSU2Payload.writePayload(payload, 0, blocks);
+
+            ChaChaPolyCipherState chacha = new ChaChaPolyCipherState();
+            chacha.initializeKey(chachaKey, 0);
+            chacha.setNonce(n);
+            chacha.encryptWithAd(data, off, LONG_HEADER_SIZE,
+                                 data, off + LONG_HEADER_SIZE, data, off + LONG_HEADER_SIZE, len - LONG_HEADER_SIZE);
+
+            pkt.setLength(pkt.getLength() + len + MAC_LEN);
+        } catch (RuntimeException re) {
+            if (!_log.shouldWarn())
+                _log.error("Bad token req msg out", re);
+            throw re;
+        } catch (GeneralSecurityException gse) {
+            if (!_log.shouldWarn())
+                _log.error("Bad token req msg out", gse);
+            throw new RuntimeException("Bad token req msg out", gse);
+        }
+        SSU2Header.encryptHandshakeHeader(packet, hdrKey1, hdrKey2);
+    }
+
+    /**
+     *  @param packet containing only 16 byte header
+     */
+    private void encryptSessionConfirmed(UDPPacket packet, HandshakeState state, int mtu,
+                                         byte[] hdrKey1, byte[] hdrKey2,
+                                         SSU2Payload.RIBlock riblock, long token) {
+        DatagramPacket pkt = packet.getPacket();
+        byte data[] = pkt.getData();
+        int off = pkt.getOffset();
+        try {
+            List<Block> blocks = new ArrayList<Block>(3);
+            int len = riblock.getTotalLength();
+            blocks.add(riblock);
+            if (token > 0) {
+                // TODO only if room
+                Block block = new SSU2Payload.NewTokenBlock(token, _context.clock().now() + 6*24*60*60*1000L);
+                len += block.getTotalLength();
+                blocks.add(block);
+            }
+            Block block = getPadding(len, mtu - 80);
+            if (block != null) {
+                len += block.getTotalLength();
+                blocks.add(block);
+            }
+
+            // If we skip past where the static key and 1st MAC will be, we can
+            // use the packet for the plaintext and Noise will symmetric encrypt in-place
+            SSU2Payload.writePayload(data, off + SHORT_HEADER_SIZE + KEY_LEN + MAC_LEN, blocks);
+            state.mixHash(data, off, SHORT_HEADER_SIZE);
+            if (_log.shouldDebug())
+                _log.debug("State after mixHash 3: " + state);
+            state.writeMessage(data, off + SHORT_HEADER_SIZE, data, off + SHORT_HEADER_SIZE + KEY_LEN + MAC_LEN, len);
+            pkt.setLength(pkt.getLength() + KEY_LEN + MAC_LEN + len + MAC_LEN);
+        } catch (RuntimeException re) {
+            if (!_log.shouldWarn())
+                _log.error("Bad msg 3 out", re);
+            throw re;
+        } catch (GeneralSecurityException gse) {
+            if (!_log.shouldWarn())
+                _log.error("Bad msg 3 out", gse);
+            throw new RuntimeException("Bad msg 1 out", gse);
+        }
+        if (_log.shouldDebug())
+            _log.debug("After msg 3: " + state);
+        SSU2Header.encryptShortHeader(packet, hdrKey1, hdrKey2);
+    }
+
+    /**
+     *  @param packet containing 16 byte header and all data with
+     *                length set to the end of the data.
+     *                This will extend the length by 16 for the MAC.
+     */
+    private void encryptDataPacket(UDPPacket packet, byte[] chachaKey, long n,
+                                    byte[] hdrKey1, byte[] hdrKey2) {
+        DatagramPacket pkt = packet.getPacket();
+        byte data[] = pkt.getData();
+        int off = pkt.getOffset();
+        int len = pkt.getLength();
+        ChaChaPolyCipherState chacha = new ChaChaPolyCipherState();
+        chacha.initializeKey(chachaKey, 0);
+        chacha.setNonce(n);
+        try {
+            chacha.encryptWithAd(data, off, SHORT_HEADER_SIZE,
+                                 data, off + SHORT_HEADER_SIZE, data, off + SHORT_HEADER_SIZE, len - SHORT_HEADER_SIZE);
+        } catch (GeneralSecurityException e) {
+            throw new IllegalArgumentException("Bad data msg", e);
+        }
+        pkt.setLength(len + MAC_LEN);
+        SSU2Header.encryptShortHeader(packet, hdrKey1, hdrKey2);
+    }
+
+    /**
+     *  @param len current length of the packet
+     *  @param max max length of the packet
+     *  @return null if no room
+     */
+    private Block getPadding(int len, int max) {
+        int maxpadlen = Math.min(max - len, PADDING_MAX) - SSU2Payload.BLOCK_HEADER_SIZE;
+        if (maxpadlen < 0)
+            return null;
+        int padlen;
+        if (maxpadlen == 0)
+            padlen = 0;
+        else
+            padlen = _context.random().nextInt(maxpadlen + 1);
+        return new SSU2Payload.PaddingBlock(padlen);
+    }
+
+    private void writePayload(List<Block> blocks, byte[] data, int off) {
+        SSU2Payload.writePayload(data, off, blocks);
+    }
+}
diff --git a/router/java/src/net/i2p/router/transport/udp/PeerState2.java b/router/java/src/net/i2p/router/transport/udp/PeerState2.java
new file mode 100644
index 0000000000..df31c12f8c
--- /dev/null
+++ b/router/java/src/net/i2p/router/transport/udp/PeerState2.java
@@ -0,0 +1,68 @@
+package net.i2p.router.transport.udp;
+
+import java.net.InetSocketAddress;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import net.i2p.data.DataHelper;
+import net.i2p.data.Hash;
+import net.i2p.data.SessionKey;
+import net.i2p.router.RouterContext;
+import net.i2p.util.Log;
+import net.i2p.util.SimpleTimer2;
+
+/**
+ * Contain all of the state about a UDP connection to a peer.
+ * This is instantiated only after a connection is fully established.
+ *
+ * Public only for UI peers page. Not a public API, not for external use.
+ *
+ * SSU2 only.
+ *
+ * @since 0.9.54
+ */
+public class PeerState2 extends PeerState {
+    private final long _sendConnID;
+    private final long _rcvConnID;
+    private final AtomicInteger _packetNumber = new AtomicInteger();
+    private final byte[] _sendEncryptKey;
+    private final byte[] _rcvEncryptKey;
+    private final byte[] _sendHeaderEncryptKey1;
+    private final byte[] _rcvHeaderEncryptKey1;
+    private final byte[] _sendHeaderEncryptKey2;
+    private final byte[] _rcvHeaderEncryptKey2;
+    private final SSU2Bitfield _receivedMessages;
+
+    public static final int MIN_MTU = 1280;
+
+    /**
+     *  @param rtt from the EstablishState, or 0 if not available
+     */
+    public PeerState2(RouterContext ctx, UDPTransport transport,
+                     InetSocketAddress remoteAddress, Hash remotePeer, boolean isInbound, int rtt,
+                     byte[] sendKey, byte[] rcvKey, long sendID, long rcvID,
+                     byte[] sendHdrKey1, byte[] sendHdrKey2, byte[] rcvHdrKey2) {
+        super(ctx, transport, remoteAddress, remotePeer, isInbound, rtt);
+        _sendConnID = sendID;
+        _rcvConnID = rcvID;
+        _sendEncryptKey = sendKey;
+        _rcvEncryptKey = rcvKey;
+        _sendHeaderEncryptKey1 = sendHdrKey1;
+        _rcvHeaderEncryptKey1 = transport.getSSU2StaticIntroKey();
+        _sendHeaderEncryptKey2 = sendHdrKey2;
+        _rcvHeaderEncryptKey2 = rcvHdrKey2;
+        _receivedMessages = new SSU2Bitfield(256, 0);
+    }
+
+    // SSU2
+    long getNextPacketNumber() { return _packetNumber.incrementAndGet(); }
+    public long getSendConnID() { return _sendConnID; }
+    public long getRcvConnID() { return _rcvConnID; }
+    public byte[] getSendEncryptKey() { return _sendEncryptKey; }
+    public byte[] getRcvEncryptKey() { return _rcvEncryptKey; }
+    public byte[] getSendHeaderEncryptKey1() { return _sendHeaderEncryptKey1; }
+    public byte[] getRcvHeaderEncryptKey1() { return _rcvHeaderEncryptKey1; }
+    public byte[] getSendHeaderEncryptKey2() { return _sendHeaderEncryptKey2; }
+    public byte[] getRcvHeaderEncryptKey2() { return _rcvHeaderEncryptKey2; }
+    public SSU2Bitfield getReceivedMessages() { return _receivedMessages; }
+
+}
diff --git a/router/java/src/net/i2p/router/transport/udp/UDPTransport.java b/router/java/src/net/i2p/router/transport/udp/UDPTransport.java
index c6a6c6da3b..53ea224b33 100644
--- a/router/java/src/net/i2p/router/transport/udp/UDPTransport.java
+++ b/router/java/src/net/i2p/router/transport/udp/UDPTransport.java
@@ -21,7 +21,9 @@ import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.CopyOnWriteArrayList;
 
 import net.i2p.CoreVersion;
+import net.i2p.crypto.EncType;
 import net.i2p.crypto.HMACGenerator;
+import net.i2p.crypto.KeyPair;
 import net.i2p.crypto.SigType;
 import net.i2p.data.Base64;
 import net.i2p.data.DatabaseEntry;
@@ -30,6 +32,7 @@ import net.i2p.data.Hash;
 import net.i2p.data.router.RouterAddress;
 import net.i2p.data.router.RouterIdentity;
 import net.i2p.data.router.RouterInfo;
+import net.i2p.data.PrivateKey;
 import net.i2p.data.SessionKey;
 import net.i2p.data.i2np.DatabaseStoreMessage;
 import net.i2p.data.i2np.I2NPMessage;
@@ -46,6 +49,7 @@ import net.i2p.router.transport.TransportImpl;
 import net.i2p.router.transport.TransportUtil;
 import static net.i2p.router.transport.TransportUtil.IPv6Config.*;
 import net.i2p.router.transport.crypto.DHSessionKeyBuilder;
+import net.i2p.router.transport.crypto.X25519KeyFactory;
 import static net.i2p.router.transport.udp.PeerTestState.Role.*;
 import net.i2p.router.util.EventLog;
 import net.i2p.router.util.RandomIterator;
@@ -132,6 +136,22 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
     private RouterAddress _currentOurV4Address;
     private RouterAddress _currentOurV6Address;
 
+    // SSU2
+    private final boolean _enableSSU1;
+    private final boolean _enableSSU2;
+    private final PacketBuilder2 _packetBuilder2;
+    private final X25519KeyFactory _xdhFactory;
+    private final byte[] _ssu2StaticPubKey;
+    private final byte[] _ssu2StaticPrivKey;
+    private final byte[] _ssu2StaticIntroKey;
+    private final String _ssu2B64StaticPubKey;
+    private final String _ssu2B64StaticIntroKey;
+    /** b64 static private key */
+    public static final String PROP_SSU2_SP = "i2np.ssu2.sp";
+    /** b64 static IV */
+    public static final String PROP_SSU2_IKEY = "i2np.ssu2.ikey";
+    private static final long MIN_DOWNTIME_TO_REKEY_HIDDEN = 24*60*60*1000L;
+
     private static final int DROPLIST_PERIOD = 10*60*1000;
     public static final String STYLE = "SSU";
     public static final String PROP_INTERNAL_PORT = "i2np.udp.internalPort";
@@ -295,10 +315,14 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
                                                                     Status.IPV4_DISABLED_IPV6_OK);
 
 
-    public UDPTransport(RouterContext ctx, DHSessionKeyBuilder.Factory dh) {
+    /**
+     *  @param xdh non-null to enable SSU2
+     */
+    public UDPTransport(RouterContext ctx, DHSessionKeyBuilder.Factory dh, X25519KeyFactory xdh) {
         super(ctx);
         _networkID = ctx.router().getNetworkID();
         _dhFactory = dh;
+        _xdhFactory = xdh;
         _log = ctx.logManager().getLog(UDPTransport.class);
         _peersByIdent = new ConcurrentHashMap<Hash, PeerState>(128);
         _peersByRemoteHost = new ConcurrentHashMap<RemoteHostId, PeerState>(128);
@@ -322,6 +346,7 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
         }
 
         _packetBuilder = new PacketBuilder(_context, this);
+        _packetBuilder2 = (xdh != null) ? new PacketBuilder2(_context, this) : null;
         _fragments = new OutboundMessageFragments(_context, this, _activeThrottle);
         _inboundFragments = new InboundMessageFragments(_context, _fragments, this);
         //if (SHOULD_FLOOD_PEERS)
@@ -363,6 +388,63 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
         //_context.statManager().createRateStat("udp.packetAuthTimeSlow", "How long it takes to encrypt and MAC a packet for sending (when its slow)", "udp", RATES);
 
         _context.simpleTimer2().addPeriodicEvent(new PingIntroducers(), MIN_EXPIRE_TIMEOUT * 3 / 4);
+
+        // SSU2 key and IV generation if required
+        _enableSSU1 = dh != null;
+        _enableSSU2 = xdh != null;
+        byte[] ikey = null;
+        String b64Ikey = null;
+        if (_enableSSU2) {
+            byte[] priv = null;
+            boolean shouldSave = false;
+            String s = null;
+            // try to determine if we've been down for 30 days or more
+            long minDowntime = _context.router().isHidden() ? MIN_DOWNTIME_TO_REKEY_HIDDEN : MIN_DOWNTIME_TO_REKEY;
+            boolean shouldRekey = _context.getEstimatedDowntime() >= minDowntime;
+            if (!shouldRekey) {
+                s = ctx.getProperty(PROP_SSU2_SP);
+                if (s != null) {
+                    priv = Base64.decode(s);
+                }
+            }
+            if (priv == null || priv.length != SSU2Util.KEY_LEN) {
+                KeyPair keys = xdh.getKeys();
+                _ssu2StaticPrivKey = keys.getPrivate().getData();
+                _ssu2StaticPubKey = keys.getPublic().getData();
+                shouldSave = true;
+            } else {
+                _ssu2StaticPrivKey = priv;
+                _ssu2StaticPubKey = (new PrivateKey(EncType.ECIES_X25519, priv)).toPublic().getData();
+            }
+            if (!shouldSave) {
+                s = ctx.getProperty(PROP_SSU2_IKEY);
+                if (s != null) {
+                    ikey = Base64.decode(s);
+                    b64Ikey = s;
+                }
+            }
+            if (ikey == null || ikey.length != SSU2Util.INTRO_KEY_LEN) {
+                ikey = new byte[SSU2Util.INTRO_KEY_LEN];
+                do {
+                    ctx.random().nextBytes(ikey);
+                } while (DataHelper.eq(ikey, 0, SSU2Util.ZEROKEY, 0, SSU2Util.INTRO_KEY_LEN));
+                shouldSave = true;
+            }
+            if (shouldSave) {
+                Map<String, String> changes = new HashMap<String, String>(2);
+                String b64Priv = Base64.encode(_ssu2StaticPrivKey);
+                b64Ikey = Base64.encode(ikey);
+                changes.put(PROP_SSU2_SP, b64Priv);
+                changes.put(PROP_SSU2_IKEY, b64Ikey);
+                ctx.router().saveConfig(changes, null);
+            }
+        } else {
+            _ssu2StaticPrivKey = null;
+            _ssu2StaticPubKey = null;
+        }
+        _ssu2StaticIntroKey = ikey;
+        _ssu2B64StaticIntroKey = b64Ikey;
+        _ssu2B64StaticPubKey = (_ssu2StaticPubKey != null) ? Base64.encode(_ssu2StaticPubKey) : null;
     }
 
     /**
@@ -372,6 +454,7 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
     OutboundMessageFragments getOMF() {
         return _fragments;
     }
+
     
     /**
      *  Pick a port if not previously configured, so that TransportManager may
@@ -779,6 +862,36 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
      */
     SessionKey getIntroKey() { return _introKey; }
 
+    /**
+     * The static Intro key
+     *
+     * @return null if not configured for SSU2
+     * @since 0.9.54
+     */
+    byte[] getSSU2StaticIntroKey() {
+        return _ssu2StaticIntroKey;
+    }
+
+    /**
+     * The static pub key
+     *
+     * @return null if not configured for SSU2
+     * @since 0.9.54
+     */
+    byte[] getSSU2StaticPubKey() {
+        return _ssu2StaticPubKey;
+    }
+
+    /**
+     * The static priv key
+     *
+     * @return null if not configured for SSU2
+     * @since 0.9.54
+     */
+    byte[] getSSU2StaticPrivKey() {
+        return _ssu2StaticPrivKey;
+    }
+
     /**
      *  Published or requested port
      */
@@ -3205,6 +3318,14 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
     DHSessionKeyBuilder.Factory getDHFactory() {
         return _dhFactory;
     }
+
+    /**
+     *  @return null if not configured for SSU2
+     *  @since 0.9.54
+     */
+    X25519KeyFactory getXDHFactory() {
+        return _xdhFactory;
+    }
     
     /**
      *  @return the SSU HMAC
@@ -3222,6 +3343,14 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
         return _packetBuilder;
     }
 
+    /**
+     *  @return null if not configured for SSU2
+     *  @since 0.9.54
+     */
+    PacketBuilder2 getBuilder2() {
+        return _packetBuilder2;
+    }
+
     /**
      * Does nothing
      * @deprecated as of 0.9.31
-- 
GitLab