diff --git a/history.txt b/history.txt
index 3f86ddf9313d23a4f4a78d6062f8cc40f8674025..a80a560810f63178f6e72529baf685d72dd2eaaf 100644
--- a/history.txt
+++ b/history.txt
@@ -1,3 +1,10 @@
+2018-06-02 zzz
+ * Console: Sort tunnels within pools by expiration (ticket #2232)
+ * NTCP: Refactor EstablishState in prep for NTCP2
+
+2018-06-01 zzz
+ * SusiDNS: Fix deleting notes (ticket #1433)
+
 2018-05-31 zzz
  * Console:
    - Fix CSS preventing ordered lists (ticket #2075)
diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java
index e1f87e077ded8c1235896b49923811522eadbbfe..8ab24107c98197555eefc67083d312bbe5d8ab93 100644
--- a/router/java/src/net/i2p/router/RouterVersion.java
+++ b/router/java/src/net/i2p/router/RouterVersion.java
@@ -18,7 +18,7 @@ public class RouterVersion {
     /** deprecated */
     public final static String ID = "Monotone";
     public final static String VERSION = CoreVersion.VERSION;
-    public final static long BUILD = 16;
+    public final static long BUILD = 17;
 
     /** for example "-test" */
     public final static String EXTRA = "";
diff --git a/router/java/src/net/i2p/router/transport/ntcp/EstablishBase.java b/router/java/src/net/i2p/router/transport/ntcp/EstablishBase.java
new file mode 100644
index 0000000000000000000000000000000000000000..492385e72d227899a31b0767a4a4979c2cc27b85
--- /dev/null
+++ b/router/java/src/net/i2p/router/transport/ntcp/EstablishBase.java
@@ -0,0 +1,376 @@
+package net.i2p.router.transport.ntcp;
+
+import java.nio.ByteBuffer;
+
+import net.i2p.router.Router;
+import net.i2p.router.RouterContext;
+import net.i2p.router.transport.crypto.DHSessionKeyBuilder;
+import net.i2p.util.Log;
+import net.i2p.util.SimpleByteCache;
+
+/**
+ * Handle the 4-phase establishment, which is as follows:
+ *
+ * <pre>
+ *
+ * Alice                   contacts                      Bob
+ * =========================================================
+ *
+ * Message 1 (Session Request):
+ *  X+(H(X) xor Bob.identHash)-----------------------------&gt;
+ *
+ * Message 2 (Session Created):
+ *  &lt;----------------------------------------Y+E(H(X+Y)+tsB, sk, Y[239:255])
+ *
+ * Message 3 (Session Confirm A):
+ *  E(sz+Alice.identity+tsA+padding+S(X+Y+Bob.identHash+tsA+tsB), sk, hX_xor_Bob.identHash[16:31])---&gt;
+ *
+ * Message 4 (Session Confirm B):
+ *  &lt;----------------------E(S(X+Y+Alice.identHash+tsA+tsB)+padding, sk, prev)
+ *
+ *  Key:
+ *
+ *    X, Y: 256 byte DH keys
+ *    H(): 32 byte SHA256 Hash
+ *    E(data, session key, IV): AES256 Encrypt
+ *    S(): 40 byte DSA Signature
+ *    tsA, tsB: timestamps (4 bytes, seconds since epoch)
+ *    sk: 32 byte Session key
+ *    sz: 2 byte size of Alice identity to follow
+ *
+ * </pre>
+ *
+ *
+ * Alternately, when Bob receives a connection, it could be a
+ * check connection (perhaps prompted by Bob asking for someone
+ * to verify his listener).  check connections are formatted per
+ * isCheckInfo()
+ * NOTE: Check info is unused.
+ *
+ */
+abstract class EstablishBase implements EstablishState {
+    
+    public static final VerifiedEstablishState VERIFIED = new VerifiedEstablishState();
+    public static final FailedEstablishState FAILED = new FailedEstablishState();
+    
+    protected final RouterContext _context;
+    protected final Log _log;
+
+    // bob receives (and alice sends)
+    protected final byte _X[];
+    protected final byte _hX_xor_bobIdentHash[];
+    // alice receives (and bob sends)
+    protected final byte _Y[];
+    protected final byte _e_hXY_tsB[];
+    /** Bob's timestamp in seconds, this is in message #2, *before* _tsA */
+    protected transient long _tsB;
+    /** Alice's timestamp in seconds, this is in message #3, *after* _tsB
+     *  Only saved for outbound. For inbound, see verifyInbound().
+     */
+    protected transient long _tsA;
+    /**
+     *  OUR clock minus HIS clock, in seconds
+     *
+     *  Inbound: tsB - tsA - rtt/2
+     *  Outbound: tsA - tsB - rtt/2
+     */
+    protected transient long _peerSkew;
+    protected transient byte _e_bobSig[];
+
+    /** previously received encrypted block (or the IV) */
+    protected byte _prevEncrypted[];
+    /** decryption buffer */
+    protected final byte _curDecrypted[];
+
+    /** bytes received so far */
+    protected int _received;
+    private byte _extra[];
+
+    protected final DHSessionKeyBuilder _dh;
+
+    protected final NTCPTransport _transport;
+    protected final NTCPConnection _con;
+    /** error causing the corruption */
+    private String _err;
+    /** exception causing the error */
+    private Exception _e;
+    private boolean _failedBySkew;
+    
+    protected static final int MIN_RI_SIZE = 387;
+    protected static final int MAX_RI_SIZE = 3072;
+
+    protected static final int AES_SIZE = 16;
+    protected static final int XY_SIZE = 256;
+    protected static final int HXY_SIZE = 32;  //Hash.HASH_LENGTH;
+    protected static final int HXY_TSB_PAD_SIZE = HXY_SIZE + 4 + 12;  // 48
+
+    protected static final Object _stateLock = new Object();
+    protected State _state;
+
+    protected enum State {
+        OB_INIT,
+        /** sent 1 */
+        OB_SENT_X,
+        /** sent 1, got 2 partial */
+        OB_GOT_Y,
+        /** sent 1, got 2 */
+        OB_GOT_HXY,
+        /** sent 1, got 2, sent 3 */
+        OB_SENT_RI,
+        /** sent 1, got 2, sent 3, got 4 */
+        OB_GOT_SIG,
+
+        IB_INIT,
+        /** got 1 partial */
+        IB_GOT_X,
+        /** got 1 */
+        IB_GOT_HX,
+        /** got 1, sent 2 */
+        IB_SENT_Y,
+        /** got 1, sent 2, got partial 3 */
+        IB_GOT_RI_SIZE,
+        /** got 1, sent 2, got 3 */
+        IB_GOT_RI,
+
+        /** OB: got and verified 4; IB: got and verified 3 and sent 4 */
+        VERIFIED,
+        CORRUPT
+    }
+
+    private EstablishBase() {
+        _context = null;
+        _log = null;
+        _X = null;
+        _Y = null;
+        _hX_xor_bobIdentHash = null;
+        _curDecrypted = null;
+        _dh = null;
+        _transport = null;
+        _con = null;
+        _e_hXY_tsB = null;
+    }
+
+    protected EstablishBase(RouterContext ctx, NTCPTransport transport, NTCPConnection con) {
+        _context = ctx;
+        _log = ctx.logManager().getLog(getClass());
+        _transport = transport;
+        _con = con;
+        _dh = _transport.getDHBuilder();
+        _hX_xor_bobIdentHash = SimpleByteCache.acquire(HXY_SIZE);
+        if (_con.isInbound()) {
+            _X = SimpleByteCache.acquire(XY_SIZE);
+            _Y = _dh.getMyPublicValueBytes();
+        } else {
+            _X = _dh.getMyPublicValueBytes();
+            _Y = SimpleByteCache.acquire(XY_SIZE);
+        }
+
+        _e_hXY_tsB = new byte[HXY_TSB_PAD_SIZE];
+        _curDecrypted = SimpleByteCache.acquire(AES_SIZE);
+    }
+
+    /** @since 0.9.16 */
+    protected void changeState(State state) {
+        synchronized (_stateLock) {
+            _state = state;
+        }
+    }
+
+    /**
+     * parse the contents of the buffer as part of the handshake.  if the
+     * handshake is completed and there is more data remaining, the data are
+     * copieed out so that the next read will be the (still encrypted) remaining
+     * data (available from getExtraBytes)
+     *
+     * All data must be copied out of the buffer as Reader.processRead()
+     * will return it to the pool.
+     */
+    public synchronized void receive(ByteBuffer src) {
+        synchronized(_stateLock) {    
+            if (_state == State.VERIFIED || _state == State.CORRUPT)
+                throw new IllegalStateException(prefix() + "received unexpected data on " + _con);
+        }
+        if (_log.shouldLog(Log.DEBUG))
+            _log.debug(prefix() + "Receiving: " + src.remaining() + " Received: " + _received);
+    }
+
+    /**
+     * Does nothing. Outbound (Alice) must override.
+     * We are establishing an outbound connection, so prepare ourselves by
+     * queueing up the write of the first part of the handshake
+     */
+    public void prepareOutbound() {}
+
+    /**
+     *  Was this connection failed because of clock skew?
+     */
+    public synchronized boolean getFailedBySkew() { return _failedBySkew; }
+
+    /** did the handshake fail for some reason? */
+    public boolean isCorrupt() {
+        synchronized(_stateLock) {
+            return _state == State.CORRUPT;
+        }
+    }
+
+    /**
+     *  If synchronized on this, fails with
+     *  deadlocks from all over via CSFI.isEstablished().
+     *  Also CSFI.getFramedAveragePeerClockSkew().
+     *
+     *  @return is the handshake complete and valid?
+     */
+    public boolean isComplete() {
+        synchronized(_stateLock) {
+            return _state == State.VERIFIED;
+        }
+    }
+
+    /** Anything left over in the byte buffer after verification is extra
+     *
+     *  All data must be copied out of the buffer as Reader.processRead()
+     *  will return it to the pool.
+     *
+     *  State must be VERIFIED.
+     *  Caller must synch.
+     */
+    protected void prepareExtra(ByteBuffer buf) {
+        int remaining = buf.remaining();
+        if (remaining > 0) {
+            _extra = new byte[remaining];
+            buf.get(_extra);
+            _received += remaining;
+        }
+        if (_log.shouldLog(Log.DEBUG))
+            _log.debug(prefix() + "prepare extra " + remaining + " (total received: " + _received + ")");
+    }
+
+    /**
+     * if complete, this will contain any bytes received as part of the
+     * handshake that were after the actual handshake.  This may return null.
+     */
+    public synchronized byte[] getExtraBytes() { return _extra; }
+
+    /**
+     *  Release resources on timeout.
+     *  @param e may be null
+     *  @since 0.9.16
+     */
+    public synchronized void close(String reason, Exception e) {
+        fail(reason, e);
+    }
+
+    /** Caller must synch. */
+    protected void fail(String reason) { fail(reason, null); }
+
+    /** Caller must synch. */
+    protected void fail(String reason, Exception e) { fail(reason, e, false); }
+
+    /** Caller must synch. */
+    protected void fail(String reason, Exception e, boolean bySkew) {
+        synchronized(_stateLock) {    
+            if (_state == State.CORRUPT || _state == State.VERIFIED)
+                return;
+            changeState(State.CORRUPT);
+        }
+        _failedBySkew = bySkew;
+        _err = reason;
+        _e = e;
+        if (_log.shouldLog(Log.WARN))
+            _log.warn(prefix()+"Failed to establish: " + _err, e);
+        releaseBufs(false);
+    }
+
+    /**
+     *  Only call once. Caller must synch.
+     *  @since 0.9.16
+     */
+    protected void releaseBufs(boolean isVerified) {
+        // null or longer for OB
+        if (_prevEncrypted != null && _prevEncrypted.length == AES_SIZE)
+            SimpleByteCache.release(_prevEncrypted);
+        SimpleByteCache.release(_curDecrypted);
+        SimpleByteCache.release(_hX_xor_bobIdentHash);
+        if (_dh.getPeerPublicValue() == null)
+            _transport.returnUnused(_dh);
+    }
+
+    public synchronized String getError() { return _err; }
+
+    public synchronized Exception getException() { return _e; }
+    
+    /**
+     *  XOR a into b. Modifies b. a is unmodified.
+     *  @param a 32 bytes
+     *  @param b 32 bytes
+     *  @since 0.9.12
+     */
+    protected static void xor32(byte[] a, byte[] b) {
+        for (int i = 0; i < 32; i++) {
+            b[i] ^= a[i];
+        }
+    }
+
+    protected String prefix() { return toString(); }
+
+    @Override
+    public String toString() {
+        StringBuilder buf = new StringBuilder(64);
+        if (_con.isInbound())
+            buf.append("IBES ");
+        else
+            buf.append("OBES ");
+        buf.append(System.identityHashCode(this));
+        buf.append(' ').append(_state);
+        if (_con.isEstablished()) buf.append(" established");
+        buf.append(": ");
+        return buf.toString();
+    }
+
+    /**
+     *  @since 0.9.8
+     */
+    private static class VerifiedEstablishState extends EstablishBase {
+
+        public VerifiedEstablishState() {
+            super();
+            _state = State.VERIFIED;
+        }
+
+        @Override public void prepareOutbound() {
+            Log log =RouterContext.getCurrentContext().logManager().getLog(VerifiedEstablishState.class);
+            log.warn("prepareOutbound() on verified state, doing nothing!");
+        }
+
+        @Override public String toString() { return "VerifiedEstablishState: ";}
+    }
+
+    /**
+     *  @since 0.9.16
+     */
+    private static class FailedEstablishState extends EstablishBase {
+
+        public FailedEstablishState() {
+            super();
+            _state = State.CORRUPT;
+        }
+
+        @Override public void prepareOutbound() {
+            Log log =RouterContext.getCurrentContext().logManager().getLog(VerifiedEstablishState.class);
+            log.warn("prepareOutbound() on verified state, doing nothing!");
+        }
+
+        @Override public String toString() { return "FailedEstablishState: ";}
+    }
+
+    /**
+     *  Mark a string for extraction by xgettext and translation.
+     *  Use this only in static initializers.
+     *  It does not translate!
+     *  @return s
+     */
+    protected static final String _x(String s) {
+        return s;
+    }
+
+}
diff --git a/router/java/src/net/i2p/router/transport/ntcp/EstablishState.java b/router/java/src/net/i2p/router/transport/ntcp/EstablishState.java
index ff43d294769f54cb2f68850be8ce4129c6635971..2a40f69c1780a204592f58c48a08b7a656219a87 100644
--- a/router/java/src/net/i2p/router/transport/ntcp/EstablishState.java
+++ b/router/java/src/net/i2p/router/transport/ntcp/EstablishState.java
@@ -1,215 +1,13 @@
 package net.i2p.router.transport.ntcp;
 
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.net.InetAddress;
-import java.net.UnknownHostException;
 import java.nio.ByteBuffer;
 
-import net.i2p.I2PAppContext;
-import net.i2p.crypto.SigType;
-import net.i2p.data.Base64;
-import net.i2p.data.DataFormatException;
-import net.i2p.data.DataHelper;
-import net.i2p.data.Hash;
-import net.i2p.data.router.RouterIdentity;
-import net.i2p.data.Signature;
-import net.i2p.router.Router;
-import net.i2p.router.RouterContext;
-import net.i2p.router.transport.crypto.DHSessionKeyBuilder;
-import net.i2p.util.Log;
-import net.i2p.util.SimpleByteCache;
-
 /**
- * Handle the 4-phase establishment, which is as follows:
- *
- * <pre>
- *
- * Alice                   contacts                      Bob
- * =========================================================
- *
- * Message 1 (Session Request):
- *  X+(H(X) xor Bob.identHash)-----------------------------&gt;
- *
- * Message 2 (Session Created):
- *  &lt;----------------------------------------Y+E(H(X+Y)+tsB, sk, Y[239:255])
- *
- * Message 3 (Session Confirm A):
- *  E(sz+Alice.identity+tsA+padding+S(X+Y+Bob.identHash+tsA+tsB), sk, hX_xor_Bob.identHash[16:31])---&gt;
- *
- * Message 4 (Session Confirm B):
- *  &lt;----------------------E(S(X+Y+Alice.identHash+tsA+tsB)+padding, sk, prev)
- *
- *  Key:
- *
- *    X, Y: 256 byte DH keys
- *    H(): 32 byte SHA256 Hash
- *    E(data, session key, IV): AES256 Encrypt
- *    S(): 40 byte DSA Signature
- *    tsA, tsB: timestamps (4 bytes, seconds since epoch)
- *    sk: 32 byte Session key
- *    sz: 2 byte size of Alice identity to follow
- *
- * </pre>
- *
- *
- * Alternately, when Bob receives a connection, it could be a
- * check connection (perhaps prompted by Bob asking for someone
- * to verify his listener).  check connections are formatted per
- * isCheckInfo()
- * NOTE: Check info is unused.
+ * Handle the establishment
  *
  */
-class EstablishState {
-    
-    public static final VerifiedEstablishState VERIFIED = new VerifiedEstablishState();
-    public static final FailedEstablishState FAILED = new FailedEstablishState();
-    
-    private final RouterContext _context;
-    private final Log _log;
-
-    // bob receives (and alice sends)
-    private final byte _X[];
-    private final byte _hX_xor_bobIdentHash[];
-    private int _aliceIdentSize;
-    private RouterIdentity _aliceIdent;
-    /** contains the decrypted aliceIndexSize + aliceIdent + tsA + padding + aliceSig */
-    private ByteArrayOutputStream _sz_aliceIdent_tsA_padding_aliceSig;
-    /** how long we expect _sz_aliceIdent_tsA_padding_aliceSig to be when its full */
-    private int _sz_aliceIdent_tsA_padding_aliceSigSize;
-    // alice receives (and bob sends)
-    private final byte _Y[];
-    private final byte _e_hXY_tsB[];
-    /** Bob's timestamp in seconds, this is in message #2, *before* _tsA */
-    private transient long _tsB;
-    /** Alice's timestamp in seconds, this is in message #3, *after* _tsB
-     *  Only saved for outbound. For inbound, see verifyInbound().
-     */
-    private transient long _tsA;
-    /**
-     *  OUR clock minus HIS clock, in seconds
-     *
-     *  Inbound: tsB - tsA - rtt/2
-     *  Outbound: tsA - tsB - rtt/2
-     */
-    private transient long _peerSkew;
-    private transient byte _e_bobSig[];
-
-    /** previously received encrypted block (or the IV) */
-    private byte _prevEncrypted[];
-    /** current encrypted block we are reading (IB only) or an IV buf used at the end for OB */
-    private byte _curEncrypted[];
-    /**
-     * next index in _curEncrypted to write to (equals _curEncrypted length if the block is
-     * ready to decrypt)
-     */
-    private int _curEncryptedOffset;
-    /** decryption buffer */
-    private final byte _curDecrypted[];
-
-    /** bytes received so far */
-    private int _received;
-    private byte _extra[];
-
-    private final DHSessionKeyBuilder _dh;
-
-    private final NTCPTransport _transport;
-    private final NTCPConnection _con;
-    /** error causing the corruption */
-    private String _err;
-    /** exception causing the error */
-    private Exception _e;
-    private boolean _failedBySkew;
+interface EstablishState {
     
-    private static final int MIN_RI_SIZE = 387;
-    private static final int MAX_RI_SIZE = 3072;
-
-    private static final int AES_SIZE = 16;
-    private static final int XY_SIZE = 256;
-    private static final int HXY_SIZE = 32;  //Hash.HASH_LENGTH;
-    private static final int HXY_TSB_PAD_SIZE = HXY_SIZE + 4 + 12;  // 48
-
-    private static final Object _stateLock = new Object();
-    protected State _state;
-
-    private enum State {
-        OB_INIT,
-        /** sent 1 */
-        OB_SENT_X,
-        /** sent 1, got 2 partial */
-        OB_GOT_Y,
-        /** sent 1, got 2 */
-        OB_GOT_HXY,
-        /** sent 1, got 2, sent 3 */
-        OB_SENT_RI,
-        /** sent 1, got 2, sent 3, got 4 */
-        OB_GOT_SIG,
-
-        IB_INIT,
-        /** got 1 partial */
-        IB_GOT_X,
-        /** got 1 */
-        IB_GOT_HX,
-        /** got 1, sent 2 */
-        IB_SENT_Y,
-        /** got 1, sent 2, got partial 3 */
-        IB_GOT_RI_SIZE,
-        /** got 1, sent 2, got 3 */
-        IB_GOT_RI,
-
-        /** OB: got and verified 4; IB: got and verified 3 and sent 4 */
-        VERIFIED,
-        CORRUPT
-    }
-
-    private EstablishState() {
-        _context = null;
-        _log = null;
-        _X = null;
-        _Y = null;
-        _hX_xor_bobIdentHash = null;
-        _curDecrypted = null;
-        _dh = null;
-        _transport = null;
-        _con = null;
-        _e_hXY_tsB = null;
-    }
-
-    public EstablishState(RouterContext ctx, NTCPTransport transport, NTCPConnection con) {
-        _context = ctx;
-        _log = ctx.logManager().getLog(getClass());
-        _transport = transport;
-        _con = con;
-        _dh = _transport.getDHBuilder();
-        _hX_xor_bobIdentHash = SimpleByteCache.acquire(HXY_SIZE);
-        if (_con.isInbound()) {
-            _X = SimpleByteCache.acquire(XY_SIZE);
-            _Y = _dh.getMyPublicValueBytes();
-            _sz_aliceIdent_tsA_padding_aliceSig = new ByteArrayOutputStream(512);
-            _prevEncrypted = SimpleByteCache.acquire(AES_SIZE);
-            _state = State.IB_INIT;
-        } else {
-            _X = _dh.getMyPublicValueBytes();
-            _Y = SimpleByteCache.acquire(XY_SIZE);
-            ctx.sha().calculateHash(_X, 0, XY_SIZE, _hX_xor_bobIdentHash, 0);
-            xor32(con.getRemotePeer().calculateHash().getData(), _hX_xor_bobIdentHash);
-            // _prevEncrypted will be created later
-            _state = State.OB_INIT;
-        }
-
-        _e_hXY_tsB = new byte[HXY_TSB_PAD_SIZE];
-        _curEncrypted = SimpleByteCache.acquire(AES_SIZE);
-        _curDecrypted = SimpleByteCache.acquire(AES_SIZE);
-    }
-
-    /** @since 0.9.16 */
-    private void changeState(State state) {
-        synchronized (_stateLock) {
-            _state = state;
-        }
-    }
-
     /**
      * parse the contents of the buffer as part of the handshake.  if the
      * handshake is completed and there is more data remaining, the data are
@@ -219,442 +17,22 @@ class EstablishState {
      * All data must be copied out of the buffer as Reader.processRead()
      * will return it to the pool.
      */
-    public synchronized void receive(ByteBuffer src) {
-        synchronized(_stateLock) {    
-            if (_state == State.VERIFIED || _state == State.CORRUPT)
-                throw new IllegalStateException(prefix() + "received unexpected data on " + _con);
-        }
-        if (!src.hasRemaining())
-            return; // nothing to receive
-
-        if (_log.shouldLog(Log.DEBUG))
-            _log.debug(prefix() + "Receiving: " + src.remaining() + " Received: " + _received);
-        if (_con.isInbound())
-            receiveInbound(src);
-        else
-            receiveOutbound(src);
-    }
-
-    /**
-     *  Was this connection failed because of clock skew?
-     */
-    public synchronized boolean getFailedBySkew() { return _failedBySkew; }
+    public void receive(ByteBuffer src);
 
     /**
-     *  we are Bob, so receive these bytes as part of an inbound connection
-     *  This method receives messages 1 and 3, and sends messages 2 and 4.
-     *
-     *  All data must be copied out of the buffer as Reader.processRead()
-     *  will return it to the pool.
-     *
-     *  Caller must synch.
-     *
-     *  FIXME none of the _state comparisons use _stateLock, but whole thing
-     *  is synchronized, should be OK. See isComplete()
+     * Does nothing. Outbound (Alice) must override.
+     * We are establishing an outbound connection, so prepare ourselves by
+     * queueing up the write of the first part of the handshake
      */
-    private void receiveInbound(ByteBuffer src) {
-        while (_state == State.IB_INIT && src.hasRemaining()) {
-            byte c = src.get();
-            _X[_received++] = c;
-            if (_received >= XY_SIZE)
-                changeState(State.IB_GOT_X);
-        }
-        while (_state == State.IB_GOT_X && src.hasRemaining()) {
-            int i = _received - XY_SIZE;
-            _received++;
-            byte c = src.get();
-            _hX_xor_bobIdentHash[i] = c;
-            if (i >= HXY_SIZE - 1)
-                changeState(State.IB_GOT_HX);
-        }
-
-        if (_state == State.IB_GOT_HX) {
-
-                if (_log.shouldLog(Log.DEBUG))
-                    _log.debug(prefix()+"Enough data for a DH received");
-
-                // first verify that Alice knows who she is trying to talk with and that the X
-                // isn't corrupt
-                byte[] realXor = SimpleByteCache.acquire(HXY_SIZE);
-                _context.sha().calculateHash(_X, 0, XY_SIZE, realXor, 0);
-                xor32(_context.routerHash().getData(), realXor);
-                if (!DataHelper.eq(realXor, _hX_xor_bobIdentHash)) {
-                    SimpleByteCache.release(realXor);
-                    _context.statManager().addRateData("ntcp.invalidHXxorBIH", 1);
-                    fail("Invalid hX_xor");
-                    return;
-                }
-                SimpleByteCache.release(realXor);
-                if (!_transport.isHXHIValid(_hX_xor_bobIdentHash)) {
-                    // blocklist source? but spoofed IPs could DoS us
-                    _context.statManager().addRateData("ntcp.replayHXxorBIH", 1);
-                    fail("Replay hX_xor");
-                    return;
-                }
-
-                try {
-                    // ok, they're actually trying to talk to us, and we got their (unauthenticated) X
-                    _dh.setPeerPublicValue(_X);
-                    _dh.getSessionKey(); // force the calc
-                    System.arraycopy(_hX_xor_bobIdentHash, AES_SIZE, _prevEncrypted, 0, AES_SIZE);
-                    if (_log.shouldLog(Log.DEBUG))
-                        _log.debug(prefix()+"DH session key calculated (" + _dh.getSessionKey().toBase64() + ")");
-
-                    // now prepare our response: Y+E(H(X+Y)+tsB+padding, sk, Y[239:255])
-                    byte xy[] = new byte[XY_SIZE + XY_SIZE];
-                    System.arraycopy(_X, 0, xy, 0, XY_SIZE);
-                    System.arraycopy(_Y, 0, xy, XY_SIZE, XY_SIZE);
-                    byte[] hxy = SimpleByteCache.acquire(HXY_SIZE);
-                    _context.sha().calculateHash(xy, 0, XY_SIZE + XY_SIZE, hxy, 0);
-                    // our (Bob's) timestamp in seconds
-                    _tsB = (_context.clock().now() + 500) / 1000l;
-                    byte toEncrypt[] = new byte[HXY_TSB_PAD_SIZE];  // 48
-                    System.arraycopy(hxy, 0, toEncrypt, 0, HXY_SIZE);
-                    byte tsB[] = DataHelper.toLong(4, _tsB);
-                    System.arraycopy(tsB, 0, toEncrypt, HXY_SIZE, tsB.length);
-                    _context.random().nextBytes(toEncrypt, HXY_SIZE + 4, 12);
-                    if (_log.shouldLog(Log.DEBUG)) {
-                        _log.debug(prefix()+"h(x+y)="+Base64.encode(hxy));
-                        _log.debug(prefix() + "tsb = " + _tsB);
-                        _log.debug(prefix()+"unencrypted H(X+Y)+tsB+padding: " + Base64.encode(toEncrypt));
-                        _log.debug(prefix()+"encryption iv= " + Base64.encode(_Y, XY_SIZE-AES_SIZE, AES_SIZE));
-                        _log.debug(prefix()+"encryption key= " + _dh.getSessionKey().toBase64());
-                    }
-                    SimpleByteCache.release(hxy);
-                    _context.aes().encrypt(toEncrypt, 0, _e_hXY_tsB, 0, _dh.getSessionKey(),
-                                           _Y, XY_SIZE-AES_SIZE, HXY_TSB_PAD_SIZE);
-                    if (_log.shouldLog(Log.DEBUG))
-                        _log.debug(prefix()+"encrypted H(X+Y)+tsB+padding: " + Base64.encode(_e_hXY_tsB));
-                    byte write[] = new byte[XY_SIZE + HXY_TSB_PAD_SIZE];
-                    System.arraycopy(_Y, 0, write, 0, XY_SIZE);
-                    System.arraycopy(_e_hXY_tsB, 0, write, XY_SIZE, HXY_TSB_PAD_SIZE);
-
-                    // ok, now that is prepared, we want to actually send it, so make sure we are up for writing
-                    changeState(State.IB_SENT_Y);
-                    _transport.getPumper().wantsWrite(_con, write);
-                    if (!src.hasRemaining()) return;
-                } catch (DHSessionKeyBuilder.InvalidPublicParameterException e) {
-                    _context.statManager().addRateData("ntcp.invalidDH", 1);
-                    fail("Invalid X", e);
-                    return;
-                }
-
-        }
-
-        // ok, we are onto the encrypted area, i.e. Message #3
-        while ((_state == State.IB_SENT_Y ||
-                _state == State.IB_GOT_RI_SIZE ||
-                _state == State.IB_GOT_RI) && src.hasRemaining()) {
-
-                // Collect a 16-byte block
-                while (_curEncryptedOffset < AES_SIZE && src.hasRemaining()) {
-                    _curEncrypted[_curEncryptedOffset++] = src.get();
-                    _received++;
-                }
-                // Decrypt the 16-byte block
-                if (_curEncryptedOffset >= AES_SIZE) {
-                    _context.aes().decrypt(_curEncrypted, 0, _curDecrypted, 0, _dh.getSessionKey(),
-                                           _prevEncrypted, 0, AES_SIZE);
-
-                    byte swap[] = _prevEncrypted;
-                    _prevEncrypted = _curEncrypted;
-                    _curEncrypted = swap;
-                    _curEncryptedOffset = 0;
-
-                    if (_state == State.IB_SENT_Y) { // we are on the first decrypted block
-                        int sz = (int)DataHelper.fromLong(_curDecrypted, 0, 2);
-                        if (sz < MIN_RI_SIZE || sz > MAX_RI_SIZE) {
-                            _context.statManager().addRateData("ntcp.invalidInboundSize", sz);
-                            fail("size is invalid", new Exception("size is " + sz));
-                            return;
-                        }
-                        if (_log.shouldLog(Log.DEBUG))
-                            _log.debug(prefix() + "got the RI size: " + sz);
-                        _aliceIdentSize  = sz;
-                        changeState(State.IB_GOT_RI_SIZE);
-
-                        // We must defer the calculations for total size of the message until
-                        //  we get the full alice ident so
-                        // we can determine how long the signature is.
-                        // See below
-
-                    }
-                    try {
-                        _sz_aliceIdent_tsA_padding_aliceSig.write(_curDecrypted);
-                    } catch (IOException ioe) {
-                        if (_log.shouldLog(Log.ERROR)) _log.error(prefix()+"Error writing to the baos?", ioe);
-                    }
-
-                    if (_state == State.IB_GOT_RI_SIZE &&
-                        _sz_aliceIdent_tsA_padding_aliceSig.size() >= 2 + _aliceIdentSize) {
-                        // we have enough to get Alice's RI and determine the sig+padding length
-                        readAliceRouterIdentity();
-                        if (_log.shouldLog(Log.DEBUG))
-                            _log.debug(prefix() + "got the RI");
-                        if (_aliceIdent == null) {
-                            // readAliceRouterIdentity already called fail
-                            return;
-                        }
-                        SigType type = _aliceIdent.getSigningPublicKey().getType();
-                        if (type == null) {
-                            fail("Unsupported sig type");
-                            return;
-                        }
-                        changeState(State.IB_GOT_RI);
-                        // handle variable signature size
-                        _sz_aliceIdent_tsA_padding_aliceSigSize = 2 + _aliceIdentSize + 4 + type.getSigLen();
-                        int rem = (_sz_aliceIdent_tsA_padding_aliceSigSize % AES_SIZE);
-                        int padding = 0;
-                        if (rem > 0)
-                            padding = AES_SIZE-rem;
-                        _sz_aliceIdent_tsA_padding_aliceSigSize += padding;
-                        if (_log.shouldLog(Log.DEBUG))
-                            _log.debug(prefix() + "alice ident size decrypted as " + _aliceIdentSize +
-                                       ", making the padding at " + padding + " and total size at " +
-                                       _sz_aliceIdent_tsA_padding_aliceSigSize);
-                    }
-
-                    if (_state == State.IB_GOT_RI &&
-                        _sz_aliceIdent_tsA_padding_aliceSig.size() >= _sz_aliceIdent_tsA_padding_aliceSigSize) {
-                        // we have the remainder of Message #3, i.e. the padding+signature
-                        // Time to verify.
-
-                            if (_log.shouldLog(Log.DEBUG))
-                                _log.debug(prefix() + "got the sig");
-                            verifyInbound();
-                            if (_state == State.VERIFIED && src.hasRemaining())
-                                prepareExtra(src);
-                            if (_log.shouldLog(Log.DEBUG))
-                                _log.debug(prefix()+"verifying size (sz=" + _sz_aliceIdent_tsA_padding_aliceSig.size()
-                                           + " expected=" + _sz_aliceIdent_tsA_padding_aliceSigSize
-                                           + ' ' + _state
-                                           + " extra=" + (_extra != null ? _extra.length : 0) + ")");
-                            return;
-                    }
-                } else {
-                    // no more bytes available in the buffer, and only a partial
-                    // block was read, so we can't decrypt it.
-                    if (_log.shouldLog(Log.DEBUG))
-                        _log.debug(prefix() + "end of available data with only a partial block read (" +
-                                   _curEncryptedOffset + ", " + _received + ")");
-                }
-        }
-
-        if (_log.shouldLog(Log.DEBUG))
-            _log.debug(prefix()+"done with the data, not yet complete or corrupt");
-    }
+    public void prepareOutbound();
 
     /**
-     *  We are Alice, so receive these bytes as part of an outbound connection.
-     *  This method receives messages 2 and 4, and sends message 3.
-     *
-     *  All data must be copied out of the buffer as Reader.processRead()
-     *  will return it to the pool.
-     *
-     *  Caller must synch.
-     *
-     *  FIXME none of the _state comparisons use _stateLock, but whole thing
-     *  is synchronized, should be OK. See isComplete()
+     *  Was this connection failed because of clock skew?
      */
-    private void receiveOutbound(ByteBuffer src) {
-
-        // recv Y+E(H(X+Y)+tsB, sk, Y[239:255])
-        // Read in Y, which is the first part of message #2
-        while (_state == State.OB_SENT_X && src.hasRemaining()) {
-            byte c = src.get();
-            _Y[_received++] = c;
-            if (_received >= XY_SIZE) {
-                try {
-                    _dh.setPeerPublicValue(_Y);
-                    _dh.getSessionKey(); // force the calc
-                    if (_log.shouldLog(Log.DEBUG))
-                        _log.debug(prefix()+"DH session key calculated (" + _dh.getSessionKey().toBase64() + ")");
-                    changeState(State.OB_GOT_Y);
-                } catch (DHSessionKeyBuilder.InvalidPublicParameterException e) {
-                    _context.statManager().addRateData("ntcp.invalidDH", 1);
-                    fail("Invalid X", e);
-                    return;
-                }
-            }
-        }
-
-        // Read in Y, which is the first part of message #2
-        // Read in the rest of message #2
-        while (_state == State.OB_GOT_Y && src.hasRemaining()) {
-            int i = _received-XY_SIZE;
-            _received++;
-            byte c = src.get();
-            _e_hXY_tsB[i] = c;
-            if (i+1 >= HXY_TSB_PAD_SIZE) {
-                if (_log.shouldLog(Log.DEBUG)) _log.debug(prefix() + "received _e_hXY_tsB fully");
-                byte hXY_tsB[] = new byte[HXY_TSB_PAD_SIZE];
-                _context.aes().decrypt(_e_hXY_tsB, 0, hXY_tsB, 0, _dh.getSessionKey(), _Y, XY_SIZE-AES_SIZE, HXY_TSB_PAD_SIZE);
-                byte XY[] = new byte[XY_SIZE + XY_SIZE];
-                System.arraycopy(_X, 0, XY, 0, XY_SIZE);
-                System.arraycopy(_Y, 0, XY, XY_SIZE, XY_SIZE);
-                byte[] h = SimpleByteCache.acquire(HXY_SIZE);
-                _context.sha().calculateHash(XY, 0, XY_SIZE + XY_SIZE, h, 0);
-                if (!DataHelper.eq(h, 0, hXY_tsB, 0, HXY_SIZE)) {
-                    SimpleByteCache.release(h);
-                    _context.statManager().addRateData("ntcp.invalidHXY", 1);
-                    fail("Invalid H(X+Y) - mitm attack attempted?");
-                    return;
-                }
-                SimpleByteCache.release(h);
-                changeState(State.OB_GOT_HXY);
-                // their (Bob's) timestamp in seconds
-                _tsB = DataHelper.fromLong(hXY_tsB, HXY_SIZE, 4);
-                long now = _context.clock().now();
-                // rtt from sending #1 to receiving #2
-                long rtt = now - _con.getCreated();
-                // our (Alice's) timestamp in seconds
-                _tsA = (now + 500) / 1000;
-                _peerSkew = (now - (_tsB * 1000) - (rtt / 2) + 500) / 1000; 
-                if (_log.shouldLog(Log.DEBUG))
-                    _log.debug(prefix()+"h(X+Y) is correct, skew = " + _peerSkew);
-
-                // the skew is not authenticated yet, but it is certainly fatal to
-                // the establishment, so fail hard if appropriate
-                long diff = 1000*Math.abs(_peerSkew);
-                if (!_context.clock().getUpdatedSuccessfully()) {
-                    // Adjust the clock one time in desperation
-                    // We are Alice, he is Bob, adjust to match Bob
-                    _context.clock().setOffset(1000 * (0 - _peerSkew), true);
-                    _peerSkew = 0;
-                    if (diff != 0)
-                        _log.logAlways(Log.WARN, "NTP failure, NTCP adjusting clock by " + DataHelper.formatDuration(diff));
-                } else if (diff >= Router.CLOCK_FUDGE_FACTOR) {
-                    _context.statManager().addRateData("ntcp.invalidOutboundSkew", diff);
-                    _transport.markReachable(_con.getRemotePeer().calculateHash(), false);
-                    // Only banlist if we know what time it is
-                    _context.banlist().banlistRouter(DataHelper.formatDuration(diff),
-                                                       _con.getRemotePeer().calculateHash(),
-                                                       _x("Excessive clock skew: {0}"));
-                    _transport.setLastBadSkew(_peerSkew);
-                    fail("Clocks too skewed (" + diff + " ms)", null, true);
-                    return;
-                } else if (_log.shouldLog(Log.DEBUG)) {
-                    _log.debug(prefix()+"Clock skew: " + diff + " ms");
-                }
-
-                // now prepare and send our response
-                // send E(#+Alice.identity+tsA+padding+S(X+Y+Bob.identHash+tsA+tsB), sk, hX_xor_Bob.identHash[16:31])
-                int sigSize = XY_SIZE + XY_SIZE + HXY_SIZE + 4+4;//+12;
-                byte preSign[] = new byte[sigSize];
-                System.arraycopy(_X, 0, preSign, 0, XY_SIZE);
-                System.arraycopy(_Y, 0, preSign, XY_SIZE, XY_SIZE);
-                System.arraycopy(_con.getRemotePeer().calculateHash().getData(), 0, preSign, XY_SIZE + XY_SIZE, HXY_SIZE);
-                DataHelper.toLong(preSign, XY_SIZE + XY_SIZE + HXY_SIZE, 4, _tsA);
-                DataHelper.toLong(preSign, XY_SIZE + XY_SIZE + HXY_SIZE + 4, 4, _tsB);
-                // hXY_tsB has 12 bytes of padding (size=48, tsB=4 + hXY=32)
-                Signature sig = _context.dsa().sign(preSign, _context.keyManager().getSigningPrivateKey());
-
-                byte ident[] = _context.router().getRouterInfo().getIdentity().toByteArray();
-                // handle variable signature size
-                int min = 2 + ident.length + 4 + sig.length();
-                int rem = min % AES_SIZE;
-                int padding = 0;
-                if (rem > 0)
-                    padding = AES_SIZE - rem;
-                byte preEncrypt[] = new byte[min+padding];
-                DataHelper.toLong(preEncrypt, 0, 2, ident.length);
-                System.arraycopy(ident, 0, preEncrypt, 2, ident.length);
-                DataHelper.toLong(preEncrypt, 2+ident.length, 4, _tsA);
-                if (padding > 0)
-                    _context.random().nextBytes(preEncrypt, 2 + ident.length + 4, padding);
-                System.arraycopy(sig.getData(), 0, preEncrypt, 2+ident.length+4+padding, sig.length());
-
-                _prevEncrypted = new byte[preEncrypt.length];
-                _context.aes().encrypt(preEncrypt, 0, _prevEncrypted, 0, _dh.getSessionKey(),
-                                       _hX_xor_bobIdentHash, _hX_xor_bobIdentHash.length-AES_SIZE, preEncrypt.length);
-
-                changeState(State.OB_SENT_RI);
-                _transport.getPumper().wantsWrite(_con, _prevEncrypted);
-            }
-        }
-
-        // Read in message #4
-        if (_state == State.OB_SENT_RI && src.hasRemaining()) {
-            // we are receiving their confirmation
-
-            // recv E(S(X+Y+Alice.identHash+tsA+tsB)+padding, sk, prev)
-            int off = 0;
-            if (_e_bobSig == null) {
-                // handle variable signature size
-                int siglen = _con.getRemotePeer().getSigningPublicKey().getType().getSigLen();
-                int rem = siglen % AES_SIZE;
-                int padding;
-                if (rem > 0)
-                    padding = AES_SIZE - rem;
-                else
-                    padding = 0;
-                _e_bobSig = new byte[siglen + padding];
-                if (_log.shouldLog(Log.DEBUG))
-                    _log.debug(prefix() + "receiving E(S(X+Y+Alice.identHash+tsA+tsB)+padding, sk, prev) (remaining? " +
-                               src.hasRemaining() + ")");
-            } else {
-                off = _received - XY_SIZE - HXY_TSB_PAD_SIZE;
-                if (_log.shouldLog(Log.DEBUG))
-                    _log.debug(prefix() + "continuing to receive E(S(X+Y+Alice.identHash+tsA+tsB)+padding, sk, prev) (remaining? " +
-                               src.hasRemaining() + " off=" + off + " recv=" + _received + ")");
-            }
-            while (_state == State.OB_SENT_RI && src.hasRemaining()) {
-                _e_bobSig[off++] = src.get();
-                _received++;
-
-                if (off >= _e_bobSig.length) {
-                    changeState(State.OB_GOT_SIG);
-                    byte bobSig[] = new byte[_e_bobSig.length];
-                    _context.aes().decrypt(_e_bobSig, 0, bobSig, 0, _dh.getSessionKey(),
-                                           _e_hXY_tsB, HXY_TSB_PAD_SIZE - AES_SIZE, _e_bobSig.length);
-                    // ignore the padding
-                    // handle variable signature size
-                    SigType type = _con.getRemotePeer().getSigningPublicKey().getType();
-                    int siglen = type.getSigLen();
-                    byte bobSigData[] = new byte[siglen];
-                    System.arraycopy(bobSig, 0, bobSigData, 0, siglen);
-                    Signature sig = new Signature(type, bobSigData);
-
-                    byte toVerify[] = new byte[XY_SIZE + XY_SIZE + HXY_SIZE +4+4];
-                    int voff = 0;
-                    System.arraycopy(_X, 0, toVerify, voff, XY_SIZE); voff += XY_SIZE;
-                    System.arraycopy(_Y, 0, toVerify, voff, XY_SIZE); voff += XY_SIZE;
-                    System.arraycopy(_context.routerHash().getData(), 0, toVerify, voff, HXY_SIZE); voff += HXY_SIZE;
-                    DataHelper.toLong(toVerify, voff, 4, _tsA); voff += 4;
-                    DataHelper.toLong(toVerify, voff, 4, _tsB); voff += 4;
-
-                    boolean ok = _context.dsa().verifySignature(sig, toVerify, _con.getRemotePeer().getSigningPublicKey());
-                    if (!ok) {
-                        _context.statManager().addRateData("ntcp.invalidSignature", 1);
-                        fail("Signature was invalid - attempt to spoof " + _con.getRemotePeer().calculateHash().toBase64() + "?");
-                    } else {
-                        if (_log.shouldLog(Log.DEBUG))
-                            _log.debug(prefix() + "signature verified from Bob.  done!");
-                        prepareExtra(src);
-                        byte nextWriteIV[] = _curEncrypted; // reuse buf
-                        System.arraycopy(_prevEncrypted, _prevEncrypted.length-AES_SIZE, nextWriteIV, 0, AES_SIZE);
-                        // this does not copy the nextWriteIV, do not release to cache
-                        // We are Alice, he is Bob, clock skew is Bob - Alice
-                        _con.finishOutboundEstablishment(_dh.getSessionKey(), _peerSkew, nextWriteIV, _e_bobSig); // skew in seconds
-                        releaseBufs(true);
-                        // if socket gets closed this will be null - prevent NPE
-                        InetAddress ia = _con.getChannel().socket().getInetAddress();
-                        if (ia != null)
-                            _transport.setIP(_con.getRemotePeer().calculateHash(), ia.getAddress());
-                        changeState(State.VERIFIED);
-                    }
-                    return;
-                }
-            }
-        }
-    }
+    public boolean getFailedBySkew();
 
     /** did the handshake fail for some reason? */
-    public boolean isCorrupt() {
-        synchronized(_stateLock) {
-            return _state == State.CORRUPT;
-        }
-    }
+    public boolean isCorrupt();
 
     /**
      *  If synchronized on this, fails with
@@ -663,554 +41,22 @@ class EstablishState {
      *
      *  @return is the handshake complete and valid?
      */
-    public boolean isComplete() {
-        synchronized(_stateLock) {
-            return _state == State.VERIFIED;
-        }
-    }
-
-    /**
-     * We are Alice.
-     * We are establishing an outbound connection, so prepare ourselves by
-     * queueing up the write of the first part of the handshake
-     * This method sends message #1 to Bob.
-     */
-    public synchronized void prepareOutbound() {
-        boolean shouldSend;
-        synchronized(_stateLock) {    
-            shouldSend = _state == State.OB_INIT;
-        }
-        if (shouldSend) {
-            if (_log.shouldLog(Log.DEBUG))
-                _log.debug(prefix() + "send X");
-            byte toWrite[] = new byte[XY_SIZE + _hX_xor_bobIdentHash.length];
-            System.arraycopy(_X, 0, toWrite, 0, XY_SIZE);
-            System.arraycopy(_hX_xor_bobIdentHash, 0, toWrite, XY_SIZE, _hX_xor_bobIdentHash.length);
-            changeState(State.OB_SENT_X);
-            _transport.getPumper().wantsWrite(_con, toWrite);
-        } else {
-            if (_log.shouldLog(Log.WARN))
-                _log.warn(prefix() + "unexpected prepareOutbound()");
-        }
-    }
-
-    /**
-     * We are Bob. We have received enough of message #3 from Alice
-     * to get Alice's RouterIdentity.
-     *
-     * _aliceIdentSize must be set.
-     * _sz_aliceIdent_tsA_padding_aliceSig must contain at least 2 + _aliceIdentSize bytes.
-     *
-     * Sets _aliceIdent so that we
-     * may determine the signature and padding sizes.
-     *
-     * After all of message #3 is received including the signature and
-     * padding, verifyIdentity() must be called.
-     *
-     *  State must be IB_GOT_RI_SIZE.
-     *  Caller must synch.
-     *
-     * @since 0.9.16 pulled out of verifyInbound()
-     */
-    private void readAliceRouterIdentity() {
-        byte b[] = _sz_aliceIdent_tsA_padding_aliceSig.toByteArray();
-
-        try {
-            int sz = _aliceIdentSize;
-            if (sz < MIN_RI_SIZE || sz > MAX_RI_SIZE ||
-                sz > b.length-2) {
-                _context.statManager().addRateData("ntcp.invalidInboundSize", sz);
-                fail("size is invalid", new Exception("size is " + sz));
-                return;
-            }
-            RouterIdentity alice = new RouterIdentity();
-            ByteArrayInputStream bais = new ByteArrayInputStream(b, 2, sz);
-            alice.readBytes(bais);
-            _aliceIdent = alice;
-        } catch (IOException ioe) {
-            _context.statManager().addRateData("ntcp.invalidInboundIOE", 1);
-            fail("Error verifying peer", ioe);
-        } catch (DataFormatException dfe) {
-            _context.statManager().addRateData("ntcp.invalidInboundDFE", 1);
-            fail("Error verifying peer", dfe);
-        }
-    }
-
-
-    /**
-     * We are Bob. Verify message #3 from Alice, then send message #4 to Alice.
-     *
-     * _aliceIdentSize and _aliceIdent must be set.
-     * _sz_aliceIdent_tsA_padding_aliceSig must contain at least
-     *  (2 + _aliceIdentSize + 4 + padding + sig) bytes.
-     *
-     * Sets _aliceIdent so that we
-     *
-     * readAliceRouterIdentity() must have been called previously
-     *
-     * Make sure the signatures are correct, and if they are, update the
-     * NIOConnection with the session key / peer ident / clock skew / iv.
-     * The NIOConnection itself is responsible for registering with the
-     * transport
-     *
-     *  State must be IB_GOT_RI.
-     *  Caller must synch.
-     */
-    private void verifyInbound() {
-        byte b[] = _sz_aliceIdent_tsA_padding_aliceSig.toByteArray();
-        try {
-            int sz = _aliceIdentSize;
-            // her timestamp from message #3
-            long tsA = DataHelper.fromLong(b, 2+sz, 4);
-            // _tsB is when we sent message #2
-            // Adjust backward by RTT/2
-            long now = _context.clock().now();
-            // rtt from sending #2 to receiving #3
-            long rtt = now - _con.getCreated();
-            _peerSkew = (now - (tsA * 1000) - (rtt / 2) + 500) / 1000; 
-
-            ByteArrayOutputStream baos = new ByteArrayOutputStream(768);
-            baos.write(_X);
-            baos.write(_Y);
-            baos.write(_context.routerHash().getData());
-            baos.write(DataHelper.toLong(4, tsA));
-            baos.write(DataHelper.toLong(4, _tsB));
-            //baos.write(b, 2+sz+4, b.length-2-sz-4-Signature.SIGNATURE_BYTES);
-
-            byte toVerify[] = baos.toByteArray();
-
-            // handle variable signature size
-            SigType type = _aliceIdent.getSigningPublicKey().getType();
-            if (type == null) {
-                fail("unsupported sig type");
-                return;
-            }
-            byte s[] = new byte[type.getSigLen()];
-            System.arraycopy(b, b.length-s.length, s, 0, s.length);
-            Signature sig = new Signature(type, s);
-            boolean ok = _context.dsa().verifySignature(sig, toVerify, _aliceIdent.getSigningPublicKey());
-            if (ok) {
-                // get inet-addr
-                InetAddress addr = this._con.getChannel().socket().getInetAddress();
-                byte[] ip = (addr == null) ? null : addr.getAddress();
-                if (_context.banlist().isBanlistedForever(_aliceIdent.calculateHash())) {
-                    if (_log.shouldLog(Log.WARN))
-                        _log.warn("Dropping inbound connection from permanently banlisted peer: " + _aliceIdent.calculateHash());
-                    // So next time we will not accept the con from this IP,
-                    // rather than doing the whole handshake
-                    if(ip != null)
-                       _context.blocklist().add(ip);
-                    fail("Peer is banlisted forever: " + _aliceIdent.calculateHash());
-                    return;
-                }
-                if(ip != null)
-                   _transport.setIP(_aliceIdent.calculateHash(), ip);
-                if (_log.shouldLog(Log.DEBUG))
-                    _log.debug(prefix() + "verification successful for " + _con);
-
-                long diff = 1000*Math.abs(_peerSkew);
-                if (!_context.clock().getUpdatedSuccessfully()) {
-                    // Adjust the clock one time in desperation
-                    // This isn't very likely, outbound will do it first
-                    // We are Bob, she is Alice, adjust to match Alice
-                    _context.clock().setOffset(1000 * (0 - _peerSkew), true);
-                    _peerSkew = 0;
-                    if (diff != 0)
-                        _log.logAlways(Log.WARN, "NTP failure, NTCP adjusting clock by " + DataHelper.formatDuration(diff));
-                } else if (diff >= Router.CLOCK_FUDGE_FACTOR) {
-                    _context.statManager().addRateData("ntcp.invalidInboundSkew", diff);
-                    _transport.markReachable(_aliceIdent.calculateHash(), true);
-                    // Only banlist if we know what time it is
-                    _context.banlist().banlistRouter(DataHelper.formatDuration(diff),
-                                                       _aliceIdent.calculateHash(),
-                                                       _x("Excessive clock skew: {0}"));
-                    _transport.setLastBadSkew(_peerSkew);
-                    fail("Clocks too skewed (" + diff + " ms)", null, true);
-                    return;
-                } else if (_log.shouldLog(Log.DEBUG)) {
-                    _log.debug(prefix()+"Clock skew: " + diff + " ms");
-                }
-
-                _con.setRemotePeer(_aliceIdent);
-                sendInboundConfirm(_aliceIdent, tsA);
-                if (_log.shouldLog(Log.DEBUG))
-                    _log.debug(prefix()+"e_bobSig is " + _e_bobSig.length + " bytes long");
-                byte iv[] = _curEncrypted;  // reuse buf
-                System.arraycopy(_e_bobSig, _e_bobSig.length-AES_SIZE, iv, 0, AES_SIZE);
-                // this does not copy the IV, do not release to cache
-                // We are Bob, she is Alice, clock skew is Alice-Bob
-                _con.finishInboundEstablishment(_dh.getSessionKey(), _peerSkew, iv, _prevEncrypted); // skew in seconds
-                releaseBufs(true);
-                if (_log.shouldLog(Log.INFO))
-                    _log.info(prefix()+"Verified remote peer as " + _aliceIdent.calculateHash());
-                changeState(State.VERIFIED);
-            } else {
-                _context.statManager().addRateData("ntcp.invalidInboundSignature", 1);
-                fail("Peer verification failed - spoof of " + _aliceIdent.calculateHash() + "?");
-            }
-        } catch (IOException ioe) {
-            _context.statManager().addRateData("ntcp.invalidInboundIOE", 1);
-            fail("Error verifying peer", ioe);
-        }
-    }
-
-    /**
-     *  We are Bob. Send message #4 to Alice.
-     *
-     *  State must be VERIFIED.
-     *  Caller must synch.
-     */
-    private void sendInboundConfirm(RouterIdentity alice, long tsA) {
-        // send Alice E(S(X+Y+Alice.identHash+tsA+tsB), sk, prev)
-        byte toSign[] = new byte[XY_SIZE + XY_SIZE + 32+4+4];
-        int off = 0;
-        System.arraycopy(_X, 0, toSign, off, XY_SIZE); off += XY_SIZE;
-        System.arraycopy(_Y, 0, toSign, off, XY_SIZE); off += XY_SIZE;
-        Hash h = alice.calculateHash();
-        System.arraycopy(h.getData(), 0, toSign, off, 32); off += 32;
-        DataHelper.toLong(toSign, off, 4, tsA); off += 4;
-        DataHelper.toLong(toSign, off, 4, _tsB); off += 4;
-
-        // handle variable signature size
-        Signature sig = _context.dsa().sign(toSign, _context.keyManager().getSigningPrivateKey());
-        int siglen = sig.length();
-        int rem = siglen % AES_SIZE;
-        int padding;
-        if (rem > 0)
-            padding = AES_SIZE - rem;
-        else
-            padding = 0;
-        byte preSig[] = new byte[siglen + padding];
-        System.arraycopy(sig.getData(), 0, preSig, 0, siglen);
-        if (padding > 0)
-            _context.random().nextBytes(preSig, siglen, padding);
-        _e_bobSig = new byte[preSig.length];
-        _context.aes().encrypt(preSig, 0, _e_bobSig, 0, _dh.getSessionKey(), _e_hXY_tsB, HXY_TSB_PAD_SIZE - AES_SIZE, _e_bobSig.length);
-
-        if (_log.shouldLog(Log.DEBUG))
-            _log.debug(prefix() + "Sending encrypted inbound confirmation");
-        _transport.getPumper().wantsWrite(_con, _e_bobSig);
-    }
-
-    /** Anything left over in the byte buffer after verification is extra
-     *
-     *  All data must be copied out of the buffer as Reader.processRead()
-     *  will return it to the pool.
-     *
-     *  State must be VERIFIED.
-     *  Caller must synch.
-     */
-    private void prepareExtra(ByteBuffer buf) {
-        int remaining = buf.remaining();
-        if (remaining > 0) {
-            _extra = new byte[remaining];
-            buf.get(_extra);
-            _received += remaining;
-        }
-        if (_log.shouldLog(Log.DEBUG))
-            _log.debug(prefix() + "prepare extra " + remaining + " (total received: " + _received + ")");
-    }
+    public boolean isComplete();
 
     /**
      * if complete, this will contain any bytes received as part of the
      * handshake that were after the actual handshake.  This may return null.
      */
-    public synchronized byte[] getExtraBytes() { return _extra; }
+    public byte[] getExtraBytes();
 
     /**
      *  Release resources on timeout.
      *  @param e may be null
      *  @since 0.9.16
      */
-    public synchronized void close(String reason, Exception e) {
-        fail(reason, e);
-    }
+    public void close(String reason, Exception e);
 
-    /** Caller must synch. */
-    private void fail(String reason) { fail(reason, null); }
-
-    /** Caller must synch. */
-    private void fail(String reason, Exception e) { fail(reason, e, false); }
-
-    /** Caller must synch. */
-    private void fail(String reason, Exception e, boolean bySkew) {
-        synchronized(_stateLock) {    
-            if (_state == State.CORRUPT || _state == State.VERIFIED)
-                return;
-            changeState(State.CORRUPT);
-        }
-        _failedBySkew = bySkew;
-        _err = reason;
-        _e = e;
-        if (_log.shouldLog(Log.WARN))
-            _log.warn(prefix()+"Failed to establish: " + _err, e);
-        releaseBufs(false);
-    }
-
-    /**
-     *  Only call once. Caller must synch.
-     *  @since 0.9.16
-     */
-    private void releaseBufs(boolean isVerified) {
-        // null or longer for OB
-        if (_prevEncrypted != null && _prevEncrypted.length == AES_SIZE)
-            SimpleByteCache.release(_prevEncrypted);
-        // Do not release _curEncrypted if verified, it is passed to
-        // NTCPConnection to use as the IV
-        if (!isVerified)
-            SimpleByteCache.release(_curEncrypted);
-        SimpleByteCache.release(_curDecrypted);
-        SimpleByteCache.release(_hX_xor_bobIdentHash);
-        if (_dh.getPeerPublicValue() == null)
-            _transport.returnUnused(_dh);
-        if (_con.isInbound())
-            SimpleByteCache.release(_X);
-        else
-            SimpleByteCache.release(_Y);
-    }
-
-    public synchronized String getError() { return _err; }
-
-    public synchronized Exception getException() { return _e; }
-    
-    /**
-     *  XOR a into b. Modifies b. a is unmodified.
-     *  @param a 32 bytes
-     *  @param b 32 bytes
-     *  @since 0.9.12
-     */
-    private static void xor32(byte[] a, byte[] b) {
-        for (int i = 0; i < 32; i++) {
-            b[i] ^= a[i];
-        }
-    }
-
-    private String prefix() { return toString(); }
-
-    @Override
-    public String toString() {
-        StringBuilder buf = new StringBuilder(64);
-        if (_con.isInbound())
-            buf.append("IBES ");
-        else
-            buf.append("OBES ");
-        buf.append(System.identityHashCode(this));
-        buf.append(' ').append(_state);
-        if (_con.isEstablished()) buf.append(" established");
-        buf.append(": ");
-        return buf.toString();
-    }
-
-    /**
-     *  @since 0.9.8
-     */
-    private static class VerifiedEstablishState extends EstablishState {
-
-        public VerifiedEstablishState() {
-            super();
-            _state = State.VERIFIED;
-        }
-
-        @Override public void prepareOutbound() {
-            Log log =RouterContext.getCurrentContext().logManager().getLog(VerifiedEstablishState.class);
-            log.warn("prepareOutbound() on verified state, doing nothing!");
-        }
-
-        @Override public String toString() { return "VerifiedEstablishState: ";}
-    }
-
-    /**
-     *  @since 0.9.16
-     */
-    private static class FailedEstablishState extends EstablishState {
-
-        public FailedEstablishState() {
-            super();
-            _state = State.CORRUPT;
-        }
-
-        @Override public void prepareOutbound() {
-            Log log =RouterContext.getCurrentContext().logManager().getLog(VerifiedEstablishState.class);
-            log.warn("prepareOutbound() on verified state, doing nothing!");
-        }
-
-        @Override public String toString() { return "FailedEstablishState: ";}
-    }
-
-/*******
-    public static void main(String args[]) {
-        if (args.length == 3) {
-            checkHost(args);
-            return;
-        }
-        I2PAppContext ctx = I2PAppContext.getGlobalContext();
-        try {
-            java.net.Socket s = new java.net.Socket("localhost", 9094);
-            OutputStream out = s.getOutputStream();
-            DHSessionKeyBuilder dh = new DHSessionKeyBuilder();
-            byte X[] = dh.getMyPublicValueBytes();
-
-            //  SEND X+(H(X) xor Bob.identHash)----------------------------->
-            out.write(X);
-            System.out.println("sent X =" + Base64.encode(X));
-            byte bih[] = Base64.decode("HuRdDx9t-RaZfYkYvacRwP~6s9mvbdkYzIMrpUCsZIo=");
-            System.out.println("bih = " + Base64.encode(bih));
-            Hash hx = ctx.sha().calculateHash(X);
-            System.out.println("hx  = " + Base64.encode(hx.getData()));
-            byte hx_xor_bih[] = DataHelper.xor(bih, hx.getData());
-            System.out.println("xor = " + Base64.encode(hx_xor_bih));
-            out.write(hx_xor_bih);
-            out.flush();
-            //  DONE SENDING X+(H(X) xor Bob.identHash)----------------------------->
-
-            //  NOW READ Y+E(H(X+Y)+tsB+padding, sk, Y[239:255])
-            InputStream in = s.getInputStream();
-            byte toRead[] = new byte[256+(32+4+12)];
-            int read = 0;
-            while (read < toRead.length) {
-                int r = in.read(toRead, read, toRead.length-read);
-                if (r == -1)
-                    throw new EOFException("eof at read=" + read);
-                read += r;
-            }
-            byte Y[] = new byte[256];
-            System.arraycopy(toRead, 0, Y, 0, Y.length);
-            dh.setPeerPublicValue(Y);
-            byte decrypted[] = new byte[(32+4+12)];
-            ctx.aes().decrypt(toRead, Y.length, decrypted, 0, dh.getSessionKey(), Y, Y.length-16, decrypted.length);
-            //display y, encrypted, decrypted, hx+y, tsb, padding
-            //unencrypted H(X+Y)+tsB+padding: bSJIv1ynFw9MhIqbObOpCqeZxtFvKEx-ilcsZQ31zYNEnVXyHCZagLbdQYRmd1oq
-            System.out.println("dh session key: " + dh.getSessionKey().toBase64());
-            System.out.println("decryption iv: " + Base64.encode(Y, Y.length-16, 16));
-            System.out.println("Y = " + Base64.encode(Y));
-            byte xy[] = new byte[512];
-            System.arraycopy(X, 0, xy, 0, X.length);
-            System.arraycopy(Y, 0, xy, X.length, Y.length);
-            System.out.println("h(x+y): " + ctx.sha().calculateHash(xy).toBase64());
-            System.out.println("encrypted H(X+Y)+tsB+padding: " + Base64.encode(toRead, Y.length, toRead.length-Y.length));
-            System.out.println("unencrypted H(X+Y)+tsB+padding: " + Base64.encode(decrypted));
-            long tsB = DataHelper.fromLong(decrypted, 32, 4);
-
-            //try { Thread.sleep(40*1000); } catch (InterruptedException ie) {}
-
-            RouterIdentity alice = new RouterIdentity();
-            Object k[] = ctx.keyGenerator().generatePKIKeypair();
-            PublicKey pub = (PublicKey)k[0];
-            PrivateKey priv = (PrivateKey)k[1];
-            k = ctx.keyGenerator().generateSigningKeypair();
-            SigningPublicKey spub = (SigningPublicKey)k[0];
-            SigningPrivateKey spriv = (SigningPrivateKey)k[1];
-            alice.setCertificate(new Certificate(Certificate.CERTIFICATE_TYPE_NULL, null));
-            alice.setPublicKey(pub);
-            alice.setSigningPublicKey(spub);
-
-            //  SEND E(#+Alice.identity+tsA+padding+S(X+Y+Bob.identHash+tsA+tsB+padding), sk, hX_xor_Bob.identHash[16:31])--->
-
-            ByteArrayOutputStream baos = new ByteArrayOutputStream(512);
-            byte aliceb[] = alice.toByteArray();
-            long tsA = ctx.clock().now()/1000l;
-            baos.write(DataHelper.toLong(2, aliceb.length));
-            baos.write(aliceb);
-            baos.write(DataHelper.toLong(4, tsA));
-
-            int base = baos.size() + Signature.SIGNATURE_BYTES;
-            int rem = base % 16;
-            int padding = 0;
-            if (rem > 0)
-                padding = 16 - rem;
-            byte pad[] = new byte[padding];
-            ctx.random().nextBytes(pad);
-            baos.write(pad);
-            base += padding;
-
-            ByteArrayOutputStream sbaos = new ByteArrayOutputStream(512);
-            sbaos.write(X);
-            sbaos.write(Y);
-            sbaos.write(bih);
-            sbaos.write(DataHelper.toLong(4, tsA));
-            sbaos.write(DataHelper.toLong(4, tsB));
-            //sbaos.write(pad);
-            Signature sig = ctx.dsa().sign(sbaos.toByteArray(), spriv);
-            baos.write(sig.toByteArray());
-
-            byte unencrypted[] = baos.toByteArray();
-            byte toWrite[] = new byte[unencrypted.length];
-            System.out.println("unencrypted.length = " + unencrypted.length + " alice.size = " + aliceb.length + " padding = " + padding + " base = " + base);
-            ctx.aes().encrypt(unencrypted, 0, toWrite, 0, dh.getSessionKey(), hx_xor_bih, 16, unencrypted.length);
-
-            out.write(toWrite);
-            out.flush();
-
-            System.out.println("unencrypted: " + Base64.encode(unencrypted));
-            System.out.println("encrypted: " + Base64.encode(toWrite));
-            System.out.println("Local peer: " + alice.calculateHash().toBase64());
-
-            // now check bob's signature
-
-            SigningPublicKey bobPubKey = null;
-            try {
-                RouterInfo info = new RouterInfo();
-                info.readBytes(new FileInputStream("/home/jrandom/routers/router1/netDb/routerInfo-HuRdDx9t-RaZfYkYvacRwP~6s9mvbdkYzIMrpUCsZIo=.dat"));
-                bobPubKey = info.getIdentity().getSigningPublicKey();
-            } catch (Exception e) {
-                e.printStackTrace();
-                return;
-            }
-
-            System.out.println("Reading in bob's sig");
-
-            byte bobRead[] = new byte[48];
-            read = 0;
-            while (read < bobRead.length) {
-                int r = in.read(bobRead, read, bobRead.length-read);
-                if (r == -1)
-                    throw new EOFException("eof at read=" + read);
-                read += r;
-            }
-
-            // bob should have sent E(S(X+Y+Alice.identHash+tsA+tsB)+padding, sk, prev)
-            byte preSig[] = new byte[Signature.SIGNATURE_BYTES+8];
-            ctx.aes().decrypt(bobRead, 0, preSig, 0, dh.getSessionKey(), toRead, toRead.length-16, preSig.length);
-            byte bobSigData[] = new byte[Signature.SIGNATURE_BYTES];
-            System.arraycopy(preSig, 0, bobSigData, 0, Signature.SIGNATURE_BYTES); // ignore the padding
-            System.out.println("Bob's sig: " + Base64.encode(bobSigData));
-
-            byte signed[] = new byte[256+256+32+4+4];
-            int off = 0;
-            System.arraycopy(X, 0, signed, off, 256); off += 256;
-            System.arraycopy(Y, 0, signed, off, 256); off += 256;
-            Hash h = alice.calculateHash();
-            System.arraycopy(h.getData(), 0, signed, off, 32); off += 32;
-            DataHelper.toLong(signed, off, 4, tsA); off += 4;
-            DataHelper.toLong(signed, off, 4, tsB); off += 4;
-
-            Signature bobSig = new Signature(bobSigData);
-            boolean ok = ctx.dsa().verifySignature(bobSig, signed, bobPubKey);
-
-            System.out.println("bob's sig matches? " + ok);
-
-            try { Thread.sleep(5*1000); } catch (InterruptedException ie) {}
-            byte fakeI2NPbuf[] = new byte[128];
-            ctx.random().nextBytes(fakeI2NPbuf);
-            out.write(fakeI2NPbuf);
-            out.flush();
-
-            try { Thread.sleep(30*1000); } catch (InterruptedException ie) {}
-            s.close();
-        } catch (Exception e) {
-            e.printStackTrace();
-        }
-    }
-*******/
-
-    /**
-     *  Mark a string for extraction by xgettext and translation.
-     *  Use this only in static initializers.
-     *  It does not translate!
-     *  @return s
-     */
-    private static final String _x(String s) {
-        return s;
-    }
+    public String getError();
 
+    public Exception getException();
 }
diff --git a/router/java/src/net/i2p/router/transport/ntcp/InboundEstablishState.java b/router/java/src/net/i2p/router/transport/ntcp/InboundEstablishState.java
new file mode 100644
index 0000000000000000000000000000000000000000..bd899b43d8bacce79ffb3183c5e064b6cf5dbea0
--- /dev/null
+++ b/router/java/src/net/i2p/router/transport/ntcp/InboundEstablishState.java
@@ -0,0 +1,486 @@
+package net.i2p.router.transport.ntcp;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+
+import net.i2p.crypto.SigType;
+import net.i2p.data.Base64;
+import net.i2p.data.DataFormatException;
+import net.i2p.data.DataHelper;
+import net.i2p.data.Hash;
+import net.i2p.data.router.RouterIdentity;
+import net.i2p.data.Signature;
+import net.i2p.router.Router;
+import net.i2p.router.RouterContext;
+import net.i2p.router.transport.crypto.DHSessionKeyBuilder;
+import net.i2p.util.Log;
+import net.i2p.util.SimpleByteCache;
+
+/**
+ *
+ *  We are Bob
+ *
+ */
+class InboundEstablishState extends EstablishBase {
+
+    /**
+     * next index in _curEncrypted to write to (equals _curEncrypted length if the block is
+     * ready to decrypt)
+     */
+    private int _curEncryptedOffset;
+
+    /** current encrypted block we are reading (IB only) or an IV buf used at the end for OB */
+    private byte _curEncrypted[];
+
+    private int _aliceIdentSize;
+    private RouterIdentity _aliceIdent;
+
+    /** contains the decrypted aliceIndexSize + aliceIdent + tsA + padding + aliceSig */
+    private ByteArrayOutputStream _sz_aliceIdent_tsA_padding_aliceSig;
+
+    /** how long we expect _sz_aliceIdent_tsA_padding_aliceSig to be when its full */
+    private int _sz_aliceIdent_tsA_padding_aliceSigSize;
+    
+    public InboundEstablishState(RouterContext ctx, NTCPTransport transport, NTCPConnection con) {
+        super(ctx, transport, con);
+        _state = State.IB_INIT;
+        _sz_aliceIdent_tsA_padding_aliceSig = new ByteArrayOutputStream(512);
+        _prevEncrypted = SimpleByteCache.acquire(AES_SIZE);
+        _curEncrypted = SimpleByteCache.acquire(AES_SIZE);
+    }
+
+    /**
+     * parse the contents of the buffer as part of the handshake.  if the
+     * handshake is completed and there is more data remaining, the data are
+     * copieed out so that the next read will be the (still encrypted) remaining
+     * data (available from getExtraBytes)
+     *
+     * All data must be copied out of the buffer as Reader.processRead()
+     * will return it to the pool.
+     */
+    @Override
+    public synchronized void receive(ByteBuffer src) {
+        super.receive(src);
+        if (!src.hasRemaining())
+            return; // nothing to receive
+        receiveInbound(src);
+    }
+
+    /**
+     *  we are Bob, so receive these bytes as part of an inbound connection
+     *  This method receives messages 1 and 3, and sends messages 2 and 4.
+     *
+     *  All data must be copied out of the buffer as Reader.processRead()
+     *  will return it to the pool.
+     *
+     *  Caller must synch.
+     *
+     *  FIXME none of the _state comparisons use _stateLock, but whole thing
+     *  is synchronized, should be OK. See isComplete()
+     */
+    private void receiveInbound(ByteBuffer src) {
+        while (_state == State.IB_INIT && src.hasRemaining()) {
+            byte c = src.get();
+            _X[_received++] = c;
+            if (_received >= XY_SIZE)
+                changeState(State.IB_GOT_X);
+        }
+        while (_state == State.IB_GOT_X && src.hasRemaining()) {
+            int i = _received - XY_SIZE;
+            _received++;
+            byte c = src.get();
+            _hX_xor_bobIdentHash[i] = c;
+            if (i >= HXY_SIZE - 1)
+                changeState(State.IB_GOT_HX);
+        }
+
+        if (_state == State.IB_GOT_HX) {
+
+                if (_log.shouldLog(Log.DEBUG))
+                    _log.debug(prefix()+"Enough data for a DH received");
+
+                // first verify that Alice knows who she is trying to talk with and that the X
+                // isn't corrupt
+                byte[] realXor = SimpleByteCache.acquire(HXY_SIZE);
+                _context.sha().calculateHash(_X, 0, XY_SIZE, realXor, 0);
+                xor32(_context.routerHash().getData(), realXor);
+                if (!DataHelper.eq(realXor, _hX_xor_bobIdentHash)) {
+                    SimpleByteCache.release(realXor);
+                    _context.statManager().addRateData("ntcp.invalidHXxorBIH", 1);
+                    fail("Invalid hX_xor");
+                    return;
+                }
+                SimpleByteCache.release(realXor);
+                if (!_transport.isHXHIValid(_hX_xor_bobIdentHash)) {
+                    // blocklist source? but spoofed IPs could DoS us
+                    _context.statManager().addRateData("ntcp.replayHXxorBIH", 1);
+                    fail("Replay hX_xor");
+                    return;
+                }
+
+                try {
+                    // ok, they're actually trying to talk to us, and we got their (unauthenticated) X
+                    _dh.setPeerPublicValue(_X);
+                    _dh.getSessionKey(); // force the calc
+                    System.arraycopy(_hX_xor_bobIdentHash, AES_SIZE, _prevEncrypted, 0, AES_SIZE);
+                    if (_log.shouldLog(Log.DEBUG))
+                        _log.debug(prefix()+"DH session key calculated (" + _dh.getSessionKey().toBase64() + ")");
+
+                    // now prepare our response: Y+E(H(X+Y)+tsB+padding, sk, Y[239:255])
+                    byte xy[] = new byte[XY_SIZE + XY_SIZE];
+                    System.arraycopy(_X, 0, xy, 0, XY_SIZE);
+                    System.arraycopy(_Y, 0, xy, XY_SIZE, XY_SIZE);
+                    byte[] hxy = SimpleByteCache.acquire(HXY_SIZE);
+                    _context.sha().calculateHash(xy, 0, XY_SIZE + XY_SIZE, hxy, 0);
+                    // our (Bob's) timestamp in seconds
+                    _tsB = (_context.clock().now() + 500) / 1000l;
+                    byte toEncrypt[] = new byte[HXY_TSB_PAD_SIZE];  // 48
+                    System.arraycopy(hxy, 0, toEncrypt, 0, HXY_SIZE);
+                    byte tsB[] = DataHelper.toLong(4, _tsB);
+                    System.arraycopy(tsB, 0, toEncrypt, HXY_SIZE, tsB.length);
+                    _context.random().nextBytes(toEncrypt, HXY_SIZE + 4, 12);
+                    if (_log.shouldLog(Log.DEBUG)) {
+                        _log.debug(prefix()+"h(x+y)="+Base64.encode(hxy));
+                        _log.debug(prefix() + "tsb = " + _tsB);
+                        _log.debug(prefix()+"unencrypted H(X+Y)+tsB+padding: " + Base64.encode(toEncrypt));
+                        _log.debug(prefix()+"encryption iv= " + Base64.encode(_Y, XY_SIZE-AES_SIZE, AES_SIZE));
+                        _log.debug(prefix()+"encryption key= " + _dh.getSessionKey().toBase64());
+                    }
+                    SimpleByteCache.release(hxy);
+                    _context.aes().encrypt(toEncrypt, 0, _e_hXY_tsB, 0, _dh.getSessionKey(),
+                                           _Y, XY_SIZE-AES_SIZE, HXY_TSB_PAD_SIZE);
+                    if (_log.shouldLog(Log.DEBUG))
+                        _log.debug(prefix()+"encrypted H(X+Y)+tsB+padding: " + Base64.encode(_e_hXY_tsB));
+                    byte write[] = new byte[XY_SIZE + HXY_TSB_PAD_SIZE];
+                    System.arraycopy(_Y, 0, write, 0, XY_SIZE);
+                    System.arraycopy(_e_hXY_tsB, 0, write, XY_SIZE, HXY_TSB_PAD_SIZE);
+
+                    // ok, now that is prepared, we want to actually send it, so make sure we are up for writing
+                    changeState(State.IB_SENT_Y);
+                    _transport.getPumper().wantsWrite(_con, write);
+                    if (!src.hasRemaining()) return;
+                } catch (DHSessionKeyBuilder.InvalidPublicParameterException e) {
+                    _context.statManager().addRateData("ntcp.invalidDH", 1);
+                    fail("Invalid X", e);
+                    return;
+                }
+
+        }
+
+        // ok, we are onto the encrypted area, i.e. Message #3
+        while ((_state == State.IB_SENT_Y ||
+                _state == State.IB_GOT_RI_SIZE ||
+                _state == State.IB_GOT_RI) && src.hasRemaining()) {
+
+                // Collect a 16-byte block
+                while (_curEncryptedOffset < AES_SIZE && src.hasRemaining()) {
+                    _curEncrypted[_curEncryptedOffset++] = src.get();
+                    _received++;
+                }
+                // Decrypt the 16-byte block
+                if (_curEncryptedOffset >= AES_SIZE) {
+                    _context.aes().decrypt(_curEncrypted, 0, _curDecrypted, 0, _dh.getSessionKey(),
+                                           _prevEncrypted, 0, AES_SIZE);
+
+                    byte swap[] = _prevEncrypted;
+                    _prevEncrypted = _curEncrypted;
+                    _curEncrypted = swap;
+                    _curEncryptedOffset = 0;
+
+                    if (_state == State.IB_SENT_Y) { // we are on the first decrypted block
+                        int sz = (int)DataHelper.fromLong(_curDecrypted, 0, 2);
+                        if (sz < MIN_RI_SIZE || sz > MAX_RI_SIZE) {
+                            _context.statManager().addRateData("ntcp.invalidInboundSize", sz);
+                            fail("size is invalid", new Exception("size is " + sz));
+                            return;
+                        }
+                        if (_log.shouldLog(Log.DEBUG))
+                            _log.debug(prefix() + "got the RI size: " + sz);
+                        _aliceIdentSize  = sz;
+                        changeState(State.IB_GOT_RI_SIZE);
+
+                        // We must defer the calculations for total size of the message until
+                        //  we get the full alice ident so
+                        // we can determine how long the signature is.
+                        // See below
+
+                    }
+                    try {
+                        _sz_aliceIdent_tsA_padding_aliceSig.write(_curDecrypted);
+                    } catch (IOException ioe) {
+                        if (_log.shouldLog(Log.ERROR)) _log.error(prefix()+"Error writing to the baos?", ioe);
+                    }
+
+                    if (_state == State.IB_GOT_RI_SIZE &&
+                        _sz_aliceIdent_tsA_padding_aliceSig.size() >= 2 + _aliceIdentSize) {
+                        // we have enough to get Alice's RI and determine the sig+padding length
+                        readAliceRouterIdentity();
+                        if (_log.shouldLog(Log.DEBUG))
+                            _log.debug(prefix() + "got the RI");
+                        if (_aliceIdent == null) {
+                            // readAliceRouterIdentity already called fail
+                            return;
+                        }
+                        SigType type = _aliceIdent.getSigningPublicKey().getType();
+                        if (type == null) {
+                            fail("Unsupported sig type");
+                            return;
+                        }
+                        changeState(State.IB_GOT_RI);
+                        // handle variable signature size
+                        _sz_aliceIdent_tsA_padding_aliceSigSize = 2 + _aliceIdentSize + 4 + type.getSigLen();
+                        int rem = (_sz_aliceIdent_tsA_padding_aliceSigSize % AES_SIZE);
+                        int padding = 0;
+                        if (rem > 0)
+                            padding = AES_SIZE-rem;
+                        _sz_aliceIdent_tsA_padding_aliceSigSize += padding;
+                        if (_log.shouldLog(Log.DEBUG))
+                            _log.debug(prefix() + "alice ident size decrypted as " + _aliceIdentSize +
+                                       ", making the padding at " + padding + " and total size at " +
+                                       _sz_aliceIdent_tsA_padding_aliceSigSize);
+                    }
+
+                    if (_state == State.IB_GOT_RI &&
+                        _sz_aliceIdent_tsA_padding_aliceSig.size() >= _sz_aliceIdent_tsA_padding_aliceSigSize) {
+                        // we have the remainder of Message #3, i.e. the padding+signature
+                        // Time to verify.
+
+                            if (_log.shouldLog(Log.DEBUG))
+                                _log.debug(prefix() + "got the sig");
+                            verifyInbound();
+                            if (_state == State.VERIFIED && src.hasRemaining())
+                                prepareExtra(src);
+                            if (_log.shouldLog(Log.DEBUG))
+                                _log.debug(prefix()+"verifying size (sz=" + _sz_aliceIdent_tsA_padding_aliceSig.size()
+                                           + " expected=" + _sz_aliceIdent_tsA_padding_aliceSigSize
+                                           + ' ' + _state
+                                           + ')');
+                            return;
+                    }
+                } else {
+                    // no more bytes available in the buffer, and only a partial
+                    // block was read, so we can't decrypt it.
+                    if (_log.shouldLog(Log.DEBUG))
+                        _log.debug(prefix() + "end of available data with only a partial block read (" +
+                                   _curEncryptedOffset + ", " + _received + ")");
+                }
+        }
+
+        if (_log.shouldLog(Log.DEBUG))
+            _log.debug(prefix()+"done with the data, not yet complete or corrupt");
+    }
+
+    /**
+     * We are Bob. We have received enough of message #3 from Alice
+     * to get Alice's RouterIdentity.
+     *
+     * _aliceIdentSize must be set.
+     * _sz_aliceIdent_tsA_padding_aliceSig must contain at least 2 + _aliceIdentSize bytes.
+     *
+     * Sets _aliceIdent so that we
+     * may determine the signature and padding sizes.
+     *
+     * After all of message #3 is received including the signature and
+     * padding, verifyIdentity() must be called.
+     *
+     *  State must be IB_GOT_RI_SIZE.
+     *  Caller must synch.
+     *
+     * @since 0.9.16 pulled out of verifyInbound()
+     */
+    private void readAliceRouterIdentity() {
+        byte b[] = _sz_aliceIdent_tsA_padding_aliceSig.toByteArray();
+
+        try {
+            int sz = _aliceIdentSize;
+            if (sz < MIN_RI_SIZE || sz > MAX_RI_SIZE ||
+                sz > b.length-2) {
+                _context.statManager().addRateData("ntcp.invalidInboundSize", sz);
+                fail("size is invalid", new Exception("size is " + sz));
+                return;
+            }
+            RouterIdentity alice = new RouterIdentity();
+            ByteArrayInputStream bais = new ByteArrayInputStream(b, 2, sz);
+            alice.readBytes(bais);
+            _aliceIdent = alice;
+        } catch (IOException ioe) {
+            _context.statManager().addRateData("ntcp.invalidInboundIOE", 1);
+            fail("Error verifying peer", ioe);
+        } catch (DataFormatException dfe) {
+            _context.statManager().addRateData("ntcp.invalidInboundDFE", 1);
+            fail("Error verifying peer", dfe);
+        }
+    }
+
+    /**
+     * We are Bob. Verify message #3 from Alice, then send message #4 to Alice.
+     *
+     * _aliceIdentSize and _aliceIdent must be set.
+     * _sz_aliceIdent_tsA_padding_aliceSig must contain at least
+     *  (2 + _aliceIdentSize + 4 + padding + sig) bytes.
+     *
+     * Sets _aliceIdent so that we
+     *
+     * readAliceRouterIdentity() must have been called previously
+     *
+     * Make sure the signatures are correct, and if they are, update the
+     * NIOConnection with the session key / peer ident / clock skew / iv.
+     * The NIOConnection itself is responsible for registering with the
+     * transport
+     *
+     *  State must be IB_GOT_RI.
+     *  Caller must synch.
+     */
+    private void verifyInbound() {
+        byte b[] = _sz_aliceIdent_tsA_padding_aliceSig.toByteArray();
+        try {
+            int sz = _aliceIdentSize;
+            // her timestamp from message #3
+            long tsA = DataHelper.fromLong(b, 2+sz, 4);
+            // _tsB is when we sent message #2
+            // Adjust backward by RTT/2
+            long now = _context.clock().now();
+            // rtt from sending #2 to receiving #3
+            long rtt = now - _con.getCreated();
+            _peerSkew = (now - (tsA * 1000) - (rtt / 2) + 500) / 1000; 
+
+            ByteArrayOutputStream baos = new ByteArrayOutputStream(768);
+            baos.write(_X);
+            baos.write(_Y);
+            baos.write(_context.routerHash().getData());
+            baos.write(DataHelper.toLong(4, tsA));
+            baos.write(DataHelper.toLong(4, _tsB));
+            //baos.write(b, 2+sz+4, b.length-2-sz-4-Signature.SIGNATURE_BYTES);
+
+            byte toVerify[] = baos.toByteArray();
+
+            // handle variable signature size
+            SigType type = _aliceIdent.getSigningPublicKey().getType();
+            if (type == null) {
+                fail("unsupported sig type");
+                return;
+            }
+            byte s[] = new byte[type.getSigLen()];
+            System.arraycopy(b, b.length-s.length, s, 0, s.length);
+            Signature sig = new Signature(type, s);
+            boolean ok = _context.dsa().verifySignature(sig, toVerify, _aliceIdent.getSigningPublicKey());
+            if (ok) {
+                // get inet-addr
+                InetAddress addr = this._con.getChannel().socket().getInetAddress();
+                byte[] ip = (addr == null) ? null : addr.getAddress();
+                if (_context.banlist().isBanlistedForever(_aliceIdent.calculateHash())) {
+                    if (_log.shouldLog(Log.WARN))
+                        _log.warn("Dropping inbound connection from permanently banlisted peer: " + _aliceIdent.calculateHash());
+                    // So next time we will not accept the con from this IP,
+                    // rather than doing the whole handshake
+                    if(ip != null)
+                       _context.blocklist().add(ip);
+                    fail("Peer is banlisted forever: " + _aliceIdent.calculateHash());
+                    return;
+                }
+                if(ip != null)
+                   _transport.setIP(_aliceIdent.calculateHash(), ip);
+                if (_log.shouldLog(Log.DEBUG))
+                    _log.debug(prefix() + "verification successful for " + _con);
+
+                long diff = 1000*Math.abs(_peerSkew);
+                if (!_context.clock().getUpdatedSuccessfully()) {
+                    // Adjust the clock one time in desperation
+                    // This isn't very likely, outbound will do it first
+                    // We are Bob, she is Alice, adjust to match Alice
+                    _context.clock().setOffset(1000 * (0 - _peerSkew), true);
+                    _peerSkew = 0;
+                    if (diff != 0)
+                        _log.logAlways(Log.WARN, "NTP failure, NTCP adjusting clock by " + DataHelper.formatDuration(diff));
+                } else if (diff >= Router.CLOCK_FUDGE_FACTOR) {
+                    _context.statManager().addRateData("ntcp.invalidInboundSkew", diff);
+                    _transport.markReachable(_aliceIdent.calculateHash(), true);
+                    // Only banlist if we know what time it is
+                    _context.banlist().banlistRouter(DataHelper.formatDuration(diff),
+                                                       _aliceIdent.calculateHash(),
+                                                       _x("Excessive clock skew: {0}"));
+                    _transport.setLastBadSkew(_peerSkew);
+                    fail("Clocks too skewed (" + diff + " ms)", null, true);
+                    return;
+                } else if (_log.shouldLog(Log.DEBUG)) {
+                    _log.debug(prefix()+"Clock skew: " + diff + " ms");
+                }
+
+                _con.setRemotePeer(_aliceIdent);
+                sendInboundConfirm(_aliceIdent, tsA);
+                if (_log.shouldLog(Log.DEBUG))
+                    _log.debug(prefix()+"e_bobSig is " + _e_bobSig.length + " bytes long");
+                byte iv[] = _curEncrypted;  // reuse buf
+                System.arraycopy(_e_bobSig, _e_bobSig.length-AES_SIZE, iv, 0, AES_SIZE);
+                // this does not copy the IV, do not release to cache
+                // We are Bob, she is Alice, clock skew is Alice-Bob
+                _con.finishInboundEstablishment(_dh.getSessionKey(), _peerSkew, iv, _prevEncrypted); // skew in seconds
+                releaseBufs(true);
+                if (_log.shouldLog(Log.INFO))
+                    _log.info(prefix()+"Verified remote peer as " + _aliceIdent.calculateHash());
+                changeState(State.VERIFIED);
+            } else {
+                _context.statManager().addRateData("ntcp.invalidInboundSignature", 1);
+                fail("Peer verification failed - spoof of " + _aliceIdent.calculateHash() + "?");
+            }
+        } catch (IOException ioe) {
+            _context.statManager().addRateData("ntcp.invalidInboundIOE", 1);
+            fail("Error verifying peer", ioe);
+        }
+    }
+
+    /**
+     *  We are Bob. Send message #4 to Alice.
+     *
+     *  State must be VERIFIED.
+     *  Caller must synch.
+     */
+    private void sendInboundConfirm(RouterIdentity alice, long tsA) {
+        // send Alice E(S(X+Y+Alice.identHash+tsA+tsB), sk, prev)
+        byte toSign[] = new byte[XY_SIZE + XY_SIZE + 32+4+4];
+        int off = 0;
+        System.arraycopy(_X, 0, toSign, off, XY_SIZE); off += XY_SIZE;
+        System.arraycopy(_Y, 0, toSign, off, XY_SIZE); off += XY_SIZE;
+        Hash h = alice.calculateHash();
+        System.arraycopy(h.getData(), 0, toSign, off, 32); off += 32;
+        DataHelper.toLong(toSign, off, 4, tsA); off += 4;
+        DataHelper.toLong(toSign, off, 4, _tsB); off += 4;
+
+        // handle variable signature size
+        Signature sig = _context.dsa().sign(toSign, _context.keyManager().getSigningPrivateKey());
+        int siglen = sig.length();
+        int rem = siglen % AES_SIZE;
+        int padding;
+        if (rem > 0)
+            padding = AES_SIZE - rem;
+        else
+            padding = 0;
+        byte preSig[] = new byte[siglen + padding];
+        System.arraycopy(sig.getData(), 0, preSig, 0, siglen);
+        if (padding > 0)
+            _context.random().nextBytes(preSig, siglen, padding);
+        _e_bobSig = new byte[preSig.length];
+        _context.aes().encrypt(preSig, 0, _e_bobSig, 0, _dh.getSessionKey(), _e_hXY_tsB, HXY_TSB_PAD_SIZE - AES_SIZE, _e_bobSig.length);
+
+        if (_log.shouldLog(Log.DEBUG))
+            _log.debug(prefix() + "Sending encrypted inbound confirmation");
+        _transport.getPumper().wantsWrite(_con, _e_bobSig);
+    }
+
+    /**
+     *  Only call once. Caller must synch.
+     *  @since 0.9.16
+     */
+    @Override
+    protected void releaseBufs(boolean isVerified) {
+        super.releaseBufs(isVerified);
+        // Do not release _curEncrypted if verified, it is passed to
+        // NTCPConnection to use as the IV
+        if (!isVerified)
+            SimpleByteCache.release(_curEncrypted);
+        SimpleByteCache.release(_X);
+    }
+}
diff --git a/router/java/src/net/i2p/router/transport/ntcp/NTCPConnection.java b/router/java/src/net/i2p/router/transport/ntcp/NTCPConnection.java
index 5c0f3992bf595df0eb04c7e661cca05b05659351..65fa325534241f0de69add1445073f3e860a1e39 100644
--- a/router/java/src/net/i2p/router/transport/ntcp/NTCPConnection.java
+++ b/router/java/src/net/i2p/router/transport/ntcp/NTCPConnection.java
@@ -190,7 +190,7 @@ public class NTCPConnection implements Closeable {
         _isInbound = true;
         _decryptBlockBuf = new byte[BLOCK_SIZE];
         _curReadState = new ReadState();
-        _establishState = new EstablishState(ctx, transport, this);
+        _establishState = new InboundEstablishState(ctx, transport, this);
         _conKey = key;
         _conKey.attach(this);
         _inboundListener = new InboundListener();
@@ -216,7 +216,7 @@ public class NTCPConnection implements Closeable {
         //_outbound = new CoDelPriorityBlockingQueue(ctx, "NTCP-Connection", 32);
         _outbound = new PriBlockingQueue<OutNetMessage>(ctx, "NTCP-Connection", 32);
         _isInbound = false;
-        _establishState = new EstablishState(ctx, transport, this);
+        _establishState = new OutboundEstablishState(ctx, transport, this);
         _decryptBlockBuf = new byte[BLOCK_SIZE];
         _curReadState = new ReadState();
         _inboundListener = new InboundListener();
@@ -309,7 +309,7 @@ public class NTCPConnection implements Closeable {
         NTCPConnection rv = _transport.inboundEstablished(this);
         _nextMetaTime = _establishedOn + (META_FREQUENCY / 2) + _context.random().nextInt(META_FREQUENCY);
         _nextInfoTime = _establishedOn + (INFO_FREQUENCY / 2) + _context.random().nextInt(INFO_FREQUENCY);
-        _establishState = EstablishState.VERIFIED;
+        _establishState = EstablishBase.VERIFIED;
         return rv;
     }
 
@@ -427,7 +427,7 @@ public class NTCPConnection implements Closeable {
     private synchronized NTCPConnection locked_close(boolean allowRequeue) {
         if (_chan != null) try { _chan.close(); } catch (IOException ioe) { }
         if (_conKey != null) _conKey.cancel();
-        _establishState = EstablishState.FAILED;
+        _establishState = EstablishBase.FAILED;
         NTCPConnection old = _transport.removeCon(this);
         _transport.getReader().connectionClosed(this);
         _transport.getWriter().connectionClosed(this);
@@ -532,7 +532,7 @@ public class NTCPConnection implements Closeable {
             _log.debug("Outbound established, prevWriteEnd: " + Base64.encode(prevWriteEnd) + " prevReadEnd: " + Base64.encode(prevReadEnd));
 
         _establishedOn = _context.clock().now();
-        _establishState = EstablishState.VERIFIED;
+        _establishState = EstablishBase.VERIFIED;
         _transport.markReachable(getRemotePeer().calculateHash(), false);
         boolean msgs = !_outbound.isEmpty();
         _nextMetaTime = _establishedOn + (META_FREQUENCY / 2) + _context.random().nextInt(META_FREQUENCY);
diff --git a/router/java/src/net/i2p/router/transport/ntcp/OutboundEstablishState.java b/router/java/src/net/i2p/router/transport/ntcp/OutboundEstablishState.java
new file mode 100644
index 0000000000000000000000000000000000000000..12decc559908d170ba13e9eeb6242a6dcdf5ef3f
--- /dev/null
+++ b/router/java/src/net/i2p/router/transport/ntcp/OutboundEstablishState.java
@@ -0,0 +1,290 @@
+package net.i2p.router.transport.ntcp;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+
+import net.i2p.crypto.SigType;
+import net.i2p.data.DataFormatException;
+import net.i2p.data.DataHelper;
+import net.i2p.data.Hash;
+import net.i2p.data.router.RouterIdentity;
+import net.i2p.data.Signature;
+import net.i2p.router.Router;
+import net.i2p.router.RouterContext;
+import net.i2p.router.transport.crypto.DHSessionKeyBuilder;
+import net.i2p.util.Log;
+import net.i2p.util.SimpleByteCache;
+
+/**
+ *
+ *  We are Alice
+ *
+ */
+class OutboundEstablishState extends EstablishBase {
+    
+    public OutboundEstablishState(RouterContext ctx, NTCPTransport transport, NTCPConnection con) {
+        super(ctx, transport, con);
+        _state = State.OB_INIT;
+        ctx.sha().calculateHash(_X, 0, XY_SIZE, _hX_xor_bobIdentHash, 0);
+        xor32(con.getRemotePeer().calculateHash().getData(), _hX_xor_bobIdentHash);
+        // _prevEncrypted will be created later
+    }
+
+    /**
+     * parse the contents of the buffer as part of the handshake.  if the
+     * handshake is completed and there is more data remaining, the data are
+     * copieed out so that the next read will be the (still encrypted) remaining
+     * data (available from getExtraBytes)
+     *
+     * All data must be copied out of the buffer as Reader.processRead()
+     * will return it to the pool.
+     */
+    @Override
+    public synchronized void receive(ByteBuffer src) {
+        super.receive(src);
+        if (!src.hasRemaining())
+            return; // nothing to receive
+        receiveOutbound(src);
+    }
+
+    /**
+     *  We are Alice, so receive these bytes as part of an outbound connection.
+     *  This method receives messages 2 and 4, and sends message 3.
+     *
+     *  All data must be copied out of the buffer as Reader.processRead()
+     *  will return it to the pool.
+     *
+     *  Caller must synch.
+     *
+     *  FIXME none of the _state comparisons use _stateLock, but whole thing
+     *  is synchronized, should be OK. See isComplete()
+     */
+    private void receiveOutbound(ByteBuffer src) {
+
+        // recv Y+E(H(X+Y)+tsB, sk, Y[239:255])
+        // Read in Y, which is the first part of message #2
+        while (_state == State.OB_SENT_X && src.hasRemaining()) {
+            byte c = src.get();
+            _Y[_received++] = c;
+            if (_received >= XY_SIZE) {
+                try {
+                    _dh.setPeerPublicValue(_Y);
+                    _dh.getSessionKey(); // force the calc
+                    if (_log.shouldLog(Log.DEBUG))
+                        _log.debug(prefix()+"DH session key calculated (" + _dh.getSessionKey().toBase64() + ")");
+                    changeState(State.OB_GOT_Y);
+                } catch (DHSessionKeyBuilder.InvalidPublicParameterException e) {
+                    _context.statManager().addRateData("ntcp.invalidDH", 1);
+                    fail("Invalid X", e);
+                    return;
+                }
+            }
+        }
+
+        // Read in Y, which is the first part of message #2
+        // Read in the rest of message #2
+        while (_state == State.OB_GOT_Y && src.hasRemaining()) {
+            int i = _received-XY_SIZE;
+            _received++;
+            byte c = src.get();
+            _e_hXY_tsB[i] = c;
+            if (i+1 >= HXY_TSB_PAD_SIZE) {
+                if (_log.shouldLog(Log.DEBUG)) _log.debug(prefix() + "received _e_hXY_tsB fully");
+                byte hXY_tsB[] = new byte[HXY_TSB_PAD_SIZE];
+                _context.aes().decrypt(_e_hXY_tsB, 0, hXY_tsB, 0, _dh.getSessionKey(), _Y, XY_SIZE-AES_SIZE, HXY_TSB_PAD_SIZE);
+                byte XY[] = new byte[XY_SIZE + XY_SIZE];
+                System.arraycopy(_X, 0, XY, 0, XY_SIZE);
+                System.arraycopy(_Y, 0, XY, XY_SIZE, XY_SIZE);
+                byte[] h = SimpleByteCache.acquire(HXY_SIZE);
+                _context.sha().calculateHash(XY, 0, XY_SIZE + XY_SIZE, h, 0);
+                if (!DataHelper.eq(h, 0, hXY_tsB, 0, HXY_SIZE)) {
+                    SimpleByteCache.release(h);
+                    _context.statManager().addRateData("ntcp.invalidHXY", 1);
+                    fail("Invalid H(X+Y) - mitm attack attempted?");
+                    return;
+                }
+                SimpleByteCache.release(h);
+                changeState(State.OB_GOT_HXY);
+                // their (Bob's) timestamp in seconds
+                _tsB = DataHelper.fromLong(hXY_tsB, HXY_SIZE, 4);
+                long now = _context.clock().now();
+                // rtt from sending #1 to receiving #2
+                long rtt = now - _con.getCreated();
+                // our (Alice's) timestamp in seconds
+                _tsA = (now + 500) / 1000;
+                _peerSkew = (now - (_tsB * 1000) - (rtt / 2) + 500) / 1000; 
+                if (_log.shouldLog(Log.DEBUG))
+                    _log.debug(prefix()+"h(X+Y) is correct, skew = " + _peerSkew);
+
+                // the skew is not authenticated yet, but it is certainly fatal to
+                // the establishment, so fail hard if appropriate
+                long diff = 1000*Math.abs(_peerSkew);
+                if (!_context.clock().getUpdatedSuccessfully()) {
+                    // Adjust the clock one time in desperation
+                    // We are Alice, he is Bob, adjust to match Bob
+                    _context.clock().setOffset(1000 * (0 - _peerSkew), true);
+                    _peerSkew = 0;
+                    if (diff != 0)
+                        _log.logAlways(Log.WARN, "NTP failure, NTCP adjusting clock by " + DataHelper.formatDuration(diff));
+                } else if (diff >= Router.CLOCK_FUDGE_FACTOR) {
+                    _context.statManager().addRateData("ntcp.invalidOutboundSkew", diff);
+                    _transport.markReachable(_con.getRemotePeer().calculateHash(), false);
+                    // Only banlist if we know what time it is
+                    _context.banlist().banlistRouter(DataHelper.formatDuration(diff),
+                                                       _con.getRemotePeer().calculateHash(),
+                                                       _x("Excessive clock skew: {0}"));
+                    _transport.setLastBadSkew(_peerSkew);
+                    fail("Clocks too skewed (" + diff + " ms)", null, true);
+                    return;
+                } else if (_log.shouldLog(Log.DEBUG)) {
+                    _log.debug(prefix()+"Clock skew: " + diff + " ms");
+                }
+
+                // now prepare and send our response
+                // send E(#+Alice.identity+tsA+padding+S(X+Y+Bob.identHash+tsA+tsB), sk, hX_xor_Bob.identHash[16:31])
+                int sigSize = XY_SIZE + XY_SIZE + HXY_SIZE + 4+4;//+12;
+                byte preSign[] = new byte[sigSize];
+                System.arraycopy(_X, 0, preSign, 0, XY_SIZE);
+                System.arraycopy(_Y, 0, preSign, XY_SIZE, XY_SIZE);
+                System.arraycopy(_con.getRemotePeer().calculateHash().getData(), 0, preSign, XY_SIZE + XY_SIZE, HXY_SIZE);
+                DataHelper.toLong(preSign, XY_SIZE + XY_SIZE + HXY_SIZE, 4, _tsA);
+                DataHelper.toLong(preSign, XY_SIZE + XY_SIZE + HXY_SIZE + 4, 4, _tsB);
+                // hXY_tsB has 12 bytes of padding (size=48, tsB=4 + hXY=32)
+                Signature sig = _context.dsa().sign(preSign, _context.keyManager().getSigningPrivateKey());
+
+                byte ident[] = _context.router().getRouterInfo().getIdentity().toByteArray();
+                // handle variable signature size
+                int min = 2 + ident.length + 4 + sig.length();
+                int rem = min % AES_SIZE;
+                int padding = 0;
+                if (rem > 0)
+                    padding = AES_SIZE - rem;
+                byte preEncrypt[] = new byte[min+padding];
+                DataHelper.toLong(preEncrypt, 0, 2, ident.length);
+                System.arraycopy(ident, 0, preEncrypt, 2, ident.length);
+                DataHelper.toLong(preEncrypt, 2+ident.length, 4, _tsA);
+                if (padding > 0)
+                    _context.random().nextBytes(preEncrypt, 2 + ident.length + 4, padding);
+                System.arraycopy(sig.getData(), 0, preEncrypt, 2+ident.length+4+padding, sig.length());
+
+                _prevEncrypted = new byte[preEncrypt.length];
+                _context.aes().encrypt(preEncrypt, 0, _prevEncrypted, 0, _dh.getSessionKey(),
+                                       _hX_xor_bobIdentHash, _hX_xor_bobIdentHash.length-AES_SIZE, preEncrypt.length);
+
+                changeState(State.OB_SENT_RI);
+                _transport.getPumper().wantsWrite(_con, _prevEncrypted);
+            }
+        }
+
+        // Read in message #4
+        if (_state == State.OB_SENT_RI && src.hasRemaining()) {
+            // we are receiving their confirmation
+
+            // recv E(S(X+Y+Alice.identHash+tsA+tsB)+padding, sk, prev)
+            int off = 0;
+            if (_e_bobSig == null) {
+                // handle variable signature size
+                int siglen = _con.getRemotePeer().getSigningPublicKey().getType().getSigLen();
+                int rem = siglen % AES_SIZE;
+                int padding;
+                if (rem > 0)
+                    padding = AES_SIZE - rem;
+                else
+                    padding = 0;
+                _e_bobSig = new byte[siglen + padding];
+                if (_log.shouldLog(Log.DEBUG))
+                    _log.debug(prefix() + "receiving E(S(X+Y+Alice.identHash+tsA+tsB)+padding, sk, prev) (remaining? " +
+                               src.hasRemaining() + ")");
+            } else {
+                off = _received - XY_SIZE - HXY_TSB_PAD_SIZE;
+                if (_log.shouldLog(Log.DEBUG))
+                    _log.debug(prefix() + "continuing to receive E(S(X+Y+Alice.identHash+tsA+tsB)+padding, sk, prev) (remaining? " +
+                               src.hasRemaining() + " off=" + off + " recv=" + _received + ")");
+            }
+            while (_state == State.OB_SENT_RI && src.hasRemaining()) {
+                _e_bobSig[off++] = src.get();
+                _received++;
+
+                if (off >= _e_bobSig.length) {
+                    changeState(State.OB_GOT_SIG);
+                    byte bobSig[] = new byte[_e_bobSig.length];
+                    _context.aes().decrypt(_e_bobSig, 0, bobSig, 0, _dh.getSessionKey(),
+                                           _e_hXY_tsB, HXY_TSB_PAD_SIZE - AES_SIZE, _e_bobSig.length);
+                    // ignore the padding
+                    // handle variable signature size
+                    SigType type = _con.getRemotePeer().getSigningPublicKey().getType();
+                    int siglen = type.getSigLen();
+                    byte bobSigData[] = new byte[siglen];
+                    System.arraycopy(bobSig, 0, bobSigData, 0, siglen);
+                    Signature sig = new Signature(type, bobSigData);
+
+                    byte toVerify[] = new byte[XY_SIZE + XY_SIZE + HXY_SIZE +4+4];
+                    int voff = 0;
+                    System.arraycopy(_X, 0, toVerify, voff, XY_SIZE); voff += XY_SIZE;
+                    System.arraycopy(_Y, 0, toVerify, voff, XY_SIZE); voff += XY_SIZE;
+                    System.arraycopy(_context.routerHash().getData(), 0, toVerify, voff, HXY_SIZE); voff += HXY_SIZE;
+                    DataHelper.toLong(toVerify, voff, 4, _tsA); voff += 4;
+                    DataHelper.toLong(toVerify, voff, 4, _tsB); voff += 4;
+
+                    boolean ok = _context.dsa().verifySignature(sig, toVerify, _con.getRemotePeer().getSigningPublicKey());
+                    if (!ok) {
+                        _context.statManager().addRateData("ntcp.invalidSignature", 1);
+                        fail("Signature was invalid - attempt to spoof " + _con.getRemotePeer().calculateHash().toBase64() + "?");
+                    } else {
+                        if (_log.shouldLog(Log.DEBUG))
+                            _log.debug(prefix() + "signature verified from Bob.  done!");
+                        prepareExtra(src);
+                        byte nextWriteIV[] = SimpleByteCache.acquire(AES_SIZE);
+                        System.arraycopy(_prevEncrypted, _prevEncrypted.length-AES_SIZE, nextWriteIV, 0, AES_SIZE);
+                        // this does not copy the nextWriteIV, do not release to cache
+                        // We are Alice, he is Bob, clock skew is Bob - Alice
+                        _con.finishOutboundEstablishment(_dh.getSessionKey(), _peerSkew, nextWriteIV, _e_bobSig); // skew in seconds
+                        releaseBufs(true);
+                        // if socket gets closed this will be null - prevent NPE
+                        InetAddress ia = _con.getChannel().socket().getInetAddress();
+                        if (ia != null)
+                            _transport.setIP(_con.getRemotePeer().calculateHash(), ia.getAddress());
+                        changeState(State.VERIFIED);
+                    }
+                    return;
+                }
+            }
+        }
+    }
+
+    /**
+     * We are Alice.
+     * We are establishing an outbound connection, so prepare ourselves by
+     * queueing up the write of the first part of the handshake
+     * This method sends message #1 to Bob.
+     */
+    public synchronized void prepareOutbound() {
+        boolean shouldSend;
+        synchronized(_stateLock) {    
+            shouldSend = _state == State.OB_INIT;
+        }
+        if (shouldSend) {
+            if (_log.shouldLog(Log.DEBUG))
+                _log.debug(prefix() + "send X");
+            byte toWrite[] = new byte[XY_SIZE + _hX_xor_bobIdentHash.length];
+            System.arraycopy(_X, 0, toWrite, 0, XY_SIZE);
+            System.arraycopy(_hX_xor_bobIdentHash, 0, toWrite, XY_SIZE, _hX_xor_bobIdentHash.length);
+            changeState(State.OB_SENT_X);
+            _transport.getPumper().wantsWrite(_con, toWrite);
+        } else {
+            if (_log.shouldLog(Log.WARN))
+                _log.warn(prefix() + "unexpected prepareOutbound()");
+        }
+    }
+
+    /**
+     *  Only call once. Caller must synch.
+     *  @since 0.9.16
+     */
+    @Override
+    protected void releaseBufs(boolean isVerified) {
+        super.releaseBufs(isVerified);
+        SimpleByteCache.release(_Y);
+    }
+}