diff --git a/router/java/src/net/i2p/router/transport/TransportManager.java b/router/java/src/net/i2p/router/transport/TransportManager.java index ba1c5d74d..92fc90e27 100644 --- a/router/java/src/net/i2p/router/transport/TransportManager.java +++ b/router/java/src/net/i2p/router/transport/TransportManager.java @@ -37,6 +37,7 @@ import net.i2p.router.OutNetMessage; import net.i2p.router.RouterContext; import static net.i2p.router.transport.Transport.AddressSource.*; import net.i2p.router.transport.crypto.DHSessionKeyBuilder; +import net.i2p.router.transport.crypto.X25519KeyFactory; import net.i2p.router.transport.ntcp.NTCPTransport; import net.i2p.router.transport.udp.UDPTransport; import net.i2p.util.Addresses; @@ -68,6 +69,7 @@ public class TransportManager implements TransportEventListener { private final RouterContext _context; private final UPnPManager _upnpManager; private final DHSessionKeyBuilder.PrecalcRunner _dhThread; + private final X25519KeyFactory _xdhThread; /** default true */ public final static String PROP_ENABLE_UDP = "i2np.udp.enable"; @@ -76,6 +78,9 @@ public class TransportManager implements TransportEventListener { /** default true */ public final static String PROP_ENABLE_UPNP = "i2np.upnp.enable"; + private static final String PROP_NTCP2_ENABLE = "i2np.ntcp2.enable"; + private static final boolean DEFAULT_NTCP2_ENABLE = false; + private static final String PROP_ADVANCED = "routerconsole.advanced"; /** not forever, since they may update */ @@ -98,6 +103,9 @@ public class TransportManager implements TransportEventListener { else _upnpManager = null; _dhThread = new DHSessionKeyBuilder.PrecalcRunner(context); + boolean enableNTCP2 = isNTCPEnabled(context) && + context.getProperty(PROP_NTCP2_ENABLE, DEFAULT_NTCP2_ENABLE); + _xdhThread = enableNTCP2 ? new X25519KeyFactory(context) : null; } /** @@ -172,7 +180,7 @@ public class TransportManager implements TransportEventListener { initializeAddress(udp); } if (isNTCPEnabled(_context)) { - Transport ntcp = new NTCPTransport(_context, _dhThread); + Transport ntcp = new NTCPTransport(_context, _dhThread, _xdhThread); addTransport(ntcp); initializeAddress(ntcp); if (udp != null) { @@ -309,6 +317,8 @@ public class TransportManager implements TransportEventListener { synchronized void startListening() { if (_dhThread.getState() == Thread.State.NEW) _dhThread.start(); + if (_xdhThread != null && _xdhThread.getState() == Thread.State.NEW) + _xdhThread.start(); // For now, only start UPnP if we have no publicly-routable addresses // so we don't open the listener ports to the world. // Maybe we need a config option to force on? Probably not. @@ -719,6 +729,7 @@ public class TransportManager implements TransportEventListener { _context.banlist().banlistRouterForever(peer, _x("Unsupported signature type")); } else if (unreachableTransports >= _transports.size() && countActivePeers() > 0) { // Don't banlist if we aren't talking to anybody, as we may have a network connection issue + // TODO if we are IPv6 only, ban for longer boolean incompat = false; RouterInfo us = _context.router().getRouterInfo(); if (us != null) { diff --git a/router/java/src/net/i2p/router/transport/ntcp/EstablishBase.java b/router/java/src/net/i2p/router/transport/ntcp/EstablishBase.java index a9bf59aa5..115ecca60 100644 --- a/router/java/src/net/i2p/router/transport/ntcp/EstablishBase.java +++ b/router/java/src/net/i2p/router/transport/ntcp/EstablishBase.java @@ -9,6 +9,11 @@ import net.i2p.util.Log; import net.i2p.util.SimpleByteCache; /** + * Inbound NTCP 1 or 2. Outbound NTCP 1 only. + * OutboundNTCP2State does not extend this. + * + * NTCP 1 establishement overview: + * * Handle the 4-phase establishment, which is as follows: * *
@@ -33,7 +38,7 @@ import net.i2p.util.SimpleByteCache;
  *    X, Y: 256 byte DH keys
  *    H(): 32 byte SHA256 Hash
  *    E(data, session key, IV): AES256 Encrypt
- *    S(): 40 byte DSA Signature
+ *    S(): 40 byte DSA Signature, or length as implied by sig type
  *    tsA, tsB: timestamps (4 bytes, seconds since epoch)
  *    sk: 32 byte Session key
  *    sz: 2 byte size of Alice identity to follow
@@ -85,17 +90,11 @@ abstract class EstablishBase implements EstablishState {
 
     /** 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;
@@ -133,6 +132,42 @@ abstract class EstablishBase implements EstablishState {
         /** got 1, sent 2, got 3 */
         IB_GOT_RI,
 
+        /**
+         * Next state IB_NTCP2_GOT_X
+         * @since 0.9.36
+         */
+        IB_NTCP2_INIT,
+        /**
+         * Got Noise part of msg 1
+         * Next state IB_NTCP2_GOT_PADDING or IB_NTCP2_READ_RANDOM on fail
+         * @since 0.9.36
+         */
+        IB_NTCP2_GOT_X,
+        /**
+         * Got msg 1 incl. padding
+         * Next state IB_NTCP2_SENT_Y
+         * @since 0.9.36
+         */
+        IB_NTCP2_GOT_PADDING,
+        /**
+         * Sent msg 2 and padding
+         * Next state IB_NTCP2_GOT_RI
+         * @since 0.9.36
+         */
+        IB_NTCP2_SENT_Y,
+        /**
+         * Got msg 3
+         * Next state VERIFIED
+         * @since 0.9.36
+         */
+        IB_NTCP2_GOT_RI,
+        /**
+         * Got msg 1 and failed AEAD
+         * Next state CORRUPT
+         * @since 0.9.36
+         */
+        IB_NTCP2_READ_RANDOM,
+
         /** OB: got and verified 4; IB: got and verified 3 and sent 4 */
         VERIFIED,
         CORRUPT
@@ -178,13 +213,13 @@ abstract class EstablishBase implements EstablishState {
     }
 
     /**
-     * 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)
+     * Parse the contents of the buffer as part of the handshake.
      *
      * All data must be copied out of the buffer as Reader.processRead()
      * will return it to the pool.
+     *
+     * If there are additional data in the buffer after the handshake is complete,
+     * the EstablishState is responsible for passing it to NTCPConnection.
      */
     public synchronized void receive(ByteBuffer src) {
         synchronized(_stateLock) {    
@@ -202,11 +237,6 @@ abstract class EstablishBase implements EstablishState {
      */
     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) {
@@ -234,31 +264,6 @@ abstract class EstablishBase implements EstablishState {
      */
     public abstract int getVersion();
 
-    /** 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
@@ -281,12 +286,12 @@ abstract class EstablishBase implements EstablishState {
                 return;
             changeState(State.CORRUPT);
         }
-        _failedBySkew = bySkew;
-        _err = reason;
-        _e = e;
         if (_log.shouldLog(Log.WARN))
-            _log.warn(prefix()+"Failed to establish: " + _err, e);
+            _log.warn(prefix() + "Failed to establish: " + reason, e);
+        if (!bySkew)
+            _context.statManager().addRateData("ntcp.receiveCorruptEstablishment", 1);
         releaseBufs(false);
+        // con.close()?
     }
 
     /**
@@ -303,10 +308,6 @@ abstract class EstablishBase implements EstablishState {
             _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
@@ -328,7 +329,7 @@ abstract class EstablishBase implements EstablishState {
             buf.append("IBES ");
         else
             buf.append("OBES ");
-        buf.append(System.identityHashCode(this));
+        buf.append(_con.toString());
         buf.append(' ').append(_state);
         if (_con.isEstablished()) buf.append(" established");
         buf.append(": ");
@@ -347,10 +348,20 @@ abstract class EstablishBase implements EstablishState {
 
         public int getVersion() { return 1; }
 
+        /*
+         * @throws IllegalStateException always
+         */
+        @Override
+        public void receive(ByteBuffer src) {
+            throw new IllegalStateException("receive() " + src.remaining() + " on verified state, doing nothing!");
+        }
+
+        /*
+         * @throws IllegalStateException always
+         */
         @Override
         public void prepareOutbound() {
-            Log log = RouterContext.getCurrentContext().logManager().getLog(VerifiedEstablishState.class);
-            log.warn("prepareOutbound() on verified state, doing nothing!");
+            throw new IllegalStateException("prepareOutbound() on verified state, doing nothing!");
         }
 
         @Override
@@ -369,10 +380,20 @@ abstract class EstablishBase implements EstablishState {
 
         public int getVersion() { return 1; }
 
+        /*
+         * @throws IllegalStateException always
+         */
+        @Override
+        public void receive(ByteBuffer src) {
+            throw new IllegalStateException("receive() " + src.remaining() + " on failed state, doing nothing!");
+        }
+
+        /*
+         * @throws IllegalStateException always
+         */
         @Override
         public void prepareOutbound() {
-            Log log = RouterContext.getCurrentContext().logManager().getLog(VerifiedEstablishState.class);
-            log.warn("prepareOutbound() on verified state, doing nothing!");
+            throw new IllegalStateException("prepareOutbound() on failed state, doing nothing!");
         }
 
         @Override
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 24695b07d..4991a2b87 100644
--- a/router/java/src/net/i2p/router/transport/ntcp/EstablishState.java
+++ b/router/java/src/net/i2p/router/transport/ntcp/EstablishState.java
@@ -9,13 +9,15 @@ import java.nio.ByteBuffer;
 interface EstablishState {
     
     /**
-     * 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)
+     * Parse the contents of the buffer as part of the handshake.
      *
      * All data must be copied out of the buffer as Reader.processRead()
      * will return it to the pool.
+     *
+     * If there are additional data in the buffer after the handshake is complete,
+     * the EstablishState is responsible for passing it to NTCPConnection.
+     *
+     * @throws IllegalStateException
      */
     public void receive(ByteBuffer src);
 
@@ -23,14 +25,11 @@ interface EstablishState {
      * 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
+     *
+     * @throws IllegalStateException
      */
     public void prepareOutbound();
 
-    /**
-     *  Was this connection failed because of clock skew?
-     */
-    public boolean getFailedBySkew();
-
     /** did the handshake fail for some reason? */
     public boolean isCorrupt();
 
@@ -43,12 +42,6 @@ interface EstablishState {
      */
     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 byte[] getExtraBytes();
-
     /**
      *  Get the NTCP version
      *  @return 1, 2, or 0 if unknown
@@ -63,7 +56,4 @@ interface EstablishState {
      */
     public void close(String reason, Exception e);
 
-    public String getError();
-
-    public Exception getException();
 }
diff --git a/router/java/src/net/i2p/router/transport/ntcp/EventPumper.java b/router/java/src/net/i2p/router/transport/ntcp/EventPumper.java
index 5fa4e4ca2..0a637405a 100644
--- a/router/java/src/net/i2p/router/transport/ntcp/EventPumper.java
+++ b/router/java/src/net/i2p/router/transport/ntcp/EventPumper.java
@@ -283,6 +283,8 @@ class EventPumper implements Runnable {
                                      con.getTimeSinceReceive() > expire) {
                                     // we haven't sent or received anything in a really long time, so lets just close 'er up
                                     con.close();
+                                    if (_log.shouldInfo())
+                                        _log.info("Failsafe or expire close for " + con);
                                     failsafeCloses++;
                                 }
                             } catch (CancelledKeyException cke) {
@@ -300,6 +302,7 @@ class EventPumper implements Runnable {
                     }
                 } else {
                     // another 100% CPU workaround 
+                    // TODO remove or only if we appear to be looping with no interest ops
                     if ((loopCount % 512) == 511) {
                         if (_log.shouldLog(Log.INFO))
                             _log.info("EventPumper throttle " + loopCount + " loops in " +
@@ -549,7 +552,9 @@ class EventPumper implements Runnable {
                 chan.socket().setKeepAlive(true);
 
             SelectionKey ckey = chan.register(_selector, SelectionKey.OP_READ);
-            new NTCPConnection(_context, _transport, chan, ckey);
+            NTCPConnection con = new NTCPConnection(_context, _transport, chan, ckey);
+            ckey.attach(con);
+            _transport.establishing(con);
         } catch (IOException ioe) {
             _log.error("Error accepting", ioe);
         }
@@ -565,6 +570,7 @@ class EventPumper implements Runnable {
             if (connected) {
                 if (shouldSetKeepAlive(chan))
                     chan.socket().setKeepAlive(true);
+                // key was already set when the channel was created, why do it again here?
                 con.setKey(key);
                 con.outboundConnected();
                 _context.statManager().addRateData("ntcp.connectSuccessful", 1);
@@ -619,7 +625,7 @@ class EventPumper implements Runnable {
                         ByteArray ba = new ByteArray(ip);
                         count = _blockedIPs.increment(ba);
                         if (_log.shouldLog(Log.WARN))
-                            _log.warn("Blocking IP " + Addresses.toString(ip) + " with count " + count + ": " + con);
+                            _log.warn("EOF on inbound before receiving any, blocking IP " + Addresses.toString(ip) + " with count " + count + ": " + con);
                     } else {
                         count = 1;
                         if (_log.shouldLog(Log.WARN))
@@ -682,11 +688,11 @@ class EventPumper implements Runnable {
                     ByteArray ba = new ByteArray(ip);
                     count = _blockedIPs.increment(ba);
                     if (_log.shouldLog(Log.WARN))
-                        _log.warn("Blocking IP " + Addresses.toString(ip) + " with count " + count + ": " + con);
+                        _log.warn("Blocking IP " + Addresses.toString(ip) + " with count " + count + ": " + con, ioe);
                 } else {
                     count = 1;
                     if (_log.shouldLog(Log.WARN))
-                        _log.warn("IOE on inbound before receiving any: " + con);
+                        _log.warn("IOE on inbound before receiving any: " + con, ioe);
                 }
                 _context.statManager().addRateData("ntcp.dropInboundNoMessage", count);
             } else {
diff --git a/router/java/src/net/i2p/router/transport/ntcp/InboundEstablishState.java b/router/java/src/net/i2p/router/transport/ntcp/InboundEstablishState.java
index 11088e05c..dd8e3d14f 100644
--- a/router/java/src/net/i2p/router/transport/ntcp/InboundEstablishState.java
+++ b/router/java/src/net/i2p/router/transport/ntcp/InboundEstablishState.java
@@ -5,17 +5,35 @@ import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.net.InetAddress;
 import java.nio.ByteBuffer;
+import java.security.GeneralSecurityException;
+import java.util.Arrays;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Set;
+
+import com.southernstorm.noise.protocol.CipherState;
+import com.southernstorm.noise.protocol.CipherStatePair;
+import com.southernstorm.noise.protocol.HandshakeState;
 
 import net.i2p.crypto.SigType;
 import net.i2p.data.Base64;
+import net.i2p.data.ByteArray;
 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.SessionKey;
 import net.i2p.data.Signature;
+import net.i2p.data.i2np.I2NPMessage;
+import net.i2p.data.i2np.I2NPMessageException;
+import net.i2p.data.router.RouterAddress;
+import net.i2p.data.router.RouterIdentity;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.router.Router;
 import net.i2p.router.RouterContext;
+import net.i2p.router.networkdb.kademlia.FloodfillNetworkDatabaseFacade;
 import net.i2p.router.transport.crypto.DHSessionKeyBuilder;
+import static net.i2p.router.transport.ntcp.OutboundNTCP2State.*;
+import net.i2p.util.ByteCache;
 import net.i2p.util.Log;
 import net.i2p.util.SimpleByteCache;
 
@@ -25,7 +43,7 @@ import net.i2p.util.SimpleByteCache;
  *
  *  @since 0.9.35 pulled out of EstablishState
  */
-class InboundEstablishState extends EstablishBase {
+class InboundEstablishState extends EstablishBase implements NTCP2Payload.PayloadCallback {
 
     /** current encrypted block we are reading (IB only) or an IV buf used at the end for OB */
     private byte _curEncrypted[];
@@ -39,7 +57,36 @@ class InboundEstablishState extends EstablishBase {
     /** how long we expect _sz_aliceIdent_tsA_padding_aliceSig to be when its full */
     private int _sz_aliceIdent_tsA_padding_aliceSigSize;
 
+    //// NTCP2 things
+
+    private HandshakeState _handshakeState;
+    private int _padlen1;
+    private int _msg3p2len;
+    private int _msg3p2FailReason = -1;
+    private ByteArray _msg3tmp;
+    private NTCP2Options _hisPadding;
+
+    // same as I2PTunnelRunner
+    private static final int BUFFER_SIZE = 4*1024;
+    private static final int MAX_DATA_READ_BUFS = 32;
+    private static final ByteCache _dataReadBufs = ByteCache.getInstance(MAX_DATA_READ_BUFS, BUFFER_SIZE);
+
     private static final int NTCP1_MSG1_SIZE = XY_SIZE + HXY_SIZE;
+    // 287 - 64 = 223
+    private static final int PADDING1_MAX = TOTAL1_MAX - MSG1_SIZE;
+    private static final int PADDING1_FAIL_MAX = 128;
+    private static final int PADDING2_MAX = 64;
+    // DSA RI, no options, no addresses
+    private static final int RI_MIN = 387 + 8 + 1 + 1 + 2 + 40;
+    private static final int MSG3P2_MIN = 1 + 2 + 1 + RI_MIN + MAC_SIZE;
+    // absolute max, let's enforce less
+    //private static final int MSG3P2_MAX = BUFFER_SIZE - MSG3P1_SIZE;
+    private static final int MSG3P2_MAX = 6000;
+
+    private static final Set STATES_NTCP2 =
+        EnumSet.of(State.IB_NTCP2_INIT, State.IB_NTCP2_GOT_X, State.IB_NTCP2_GOT_PADDING,
+                   State.IB_NTCP2_SENT_Y, State.IB_NTCP2_GOT_RI, State.IB_NTCP2_READ_RANDOM);
+
     
     public InboundEstablishState(RouterContext ctx, NTCPTransport transport, NTCPConnection con) {
         super(ctx, transport, con);
@@ -50,13 +97,13 @@ class InboundEstablishState extends EstablishBase {
     }
 
     /**
-     * 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)
+     * Parse the contents of the buffer as part of the handshake.
      *
      * All data must be copied out of the buffer as Reader.processRead()
      * will return it to the pool.
+     *
+     * If there are additional data in the buffer after the handshake is complete,
+     * the EstablishState is responsible for passing it to NTCPConnection.
      */
     @Override
     public synchronized void receive(ByteBuffer src) {
@@ -77,7 +124,8 @@ class InboundEstablishState extends EstablishBase {
         synchronized (_stateLock) {
             if (_state == State.IB_INIT)
                 return 0;
-            // TODO NTCP2 states
+            if (STATES_NTCP2.contains(_state))
+                return 2;
             return 1;
         } 
     } 
@@ -91,15 +139,24 @@ class InboundEstablishState extends EstablishBase {
      *
      *  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) {
+        if (STATES_NTCP2.contains(_state)) {
+            receiveInboundNTCP2(src);
+            return;
+        }
+        // TODO if less than 64, buffer and decide later?
         if (_state == State.IB_INIT && src.hasRemaining()) {
             int remaining = src.remaining();
-            //if (remaining < NTCP1_MSG1_SIZE && _transport.isNTCP2Enabled()) {
-            //    // NTCP2
-            //}
+            if (remaining < NTCP1_MSG1_SIZE && _transport.isNTCP2Enabled()) {
+                // NTCP2
+                // TODO can't change our mind later if we get more than 287
+                _con.setVersion(2);
+                changeState(State.IB_NTCP2_INIT);
+                receiveInboundNTCP2(src);
+                // releaseBufs() will return the unused DH
+                return;
+            }
             int toGet = Math.min(remaining, XY_SIZE - _received);
             src.get(_X, _received, toGet);
             _received += toGet;
@@ -188,6 +245,10 @@ class InboundEstablishState extends EstablishBase {
                     _context.statManager().addRateData("ntcp.invalidDH", 1);
                     fail("Invalid X", e);
                     return;
+                } catch (IllegalStateException ise) {
+                    // setPeerPublicValue()
+                    fail("reused keys?", ise);
+                    return;
                 }
 
         }
@@ -281,9 +342,7 @@ class InboundEstablishState extends EstablishBase {
 
                             if (_log.shouldLog(Log.DEBUG))
                                 _log.debug(prefix() + "got the sig");
-                            verifyInbound();
-                            if (_state == State.VERIFIED && src.hasRemaining())
-                                prepareExtra(src);
+                            verifyInbound(src);
                             if (_log.shouldLog(Log.DEBUG))
                                 _log.debug(prefix()+"verifying size (sz=" + _sz_aliceIdent_tsA_padding_aliceSig.size()
                                            + " expected=" + _sz_aliceIdent_tsA_padding_aliceSigSize
@@ -291,10 +350,15 @@ class InboundEstablishState extends EstablishBase {
                                            + ')');
                             return;
                     }
-                } else {
                 }
         }
 
+        // check for remaining data
+        if ((_state == State.VERIFIED || _state == State.CORRUPT) && src.hasRemaining()) {
+            if (_log.shouldWarn())
+                _log.warn("Received unexpected " + src.remaining() + " on " + this, new Exception());
+        }
+
         if (_log.shouldLog(Log.DEBUG))
             _log.debug(prefix()+"done with the data, not yet complete or corrupt");
     }
@@ -343,6 +407,7 @@ class InboundEstablishState extends EstablishBase {
 
     /**
      * We are Bob. Verify message #3 from Alice, then send message #4 to Alice.
+     * NTCP 1 only.
      *
      * _aliceIdentSize and _aliceIdent must be set.
      * _sz_aliceIdent_tsA_padding_aliceSig must contain at least
@@ -358,9 +423,12 @@ class InboundEstablishState extends EstablishBase {
      * transport
      *
      *  State must be IB_GOT_RI.
+     *  This will always change the state to VERIFIED or CORRUPT.
      *  Caller must synch.
+     *
+     *  @param buf possibly containing "extra" data for data phase
      */
-    private void verifyInbound() {
+    private void verifyInbound(ByteBuffer buf) {
         byte b[] = _sz_aliceIdent_tsA_padding_aliceSig.toByteArray();
         try {
             int sz = _aliceIdentSize;
@@ -393,64 +461,35 @@ class InboundEstablishState extends EstablishBase {
             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());
+            Hash aliceHash = _aliceIdent.calculateHash();
+            if (ok) {
+                ok = verifyInbound(aliceHash);
+            }
             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);
+                sendInboundConfirm(aliceHash, 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
+                // skew in seconds
+                _con.finishInboundEstablishment(_dh.getSessionKey(), _peerSkew, iv, _prevEncrypted);
+                changeState(State.VERIFIED);
+                if (buf.hasRemaining()) {
+                    // process "extra" data
+                    // This is unlikely for inbound, as we must reply with message 4
+                    if (_log.shouldWarn())
+                        _log.warn("extra data " + buf.remaining() + " on " + this);
+                     _con.recvEncryptedI2NP(buf);
+                }
                 releaseBufs(true);
                 if (_log.shouldLog(Log.INFO))
-                    _log.info(prefix()+"Verified remote peer as " + _aliceIdent.calculateHash());
-                changeState(State.VERIFIED);
+                    _log.info(prefix()+"Verified remote peer as " + aliceHash);
             } else {
                 _context.statManager().addRateData("ntcp.invalidInboundSignature", 1);
-                fail("Peer verification failed - spoof of " + _aliceIdent.calculateHash() + "?");
+                // verifyInbound(aliceHash) called fail()
             }
         } catch (IOException ioe) {
             _context.statManager().addRateData("ntcp.invalidInboundIOE", 1);
@@ -458,19 +497,76 @@ class InboundEstablishState extends EstablishBase {
         }
     }
 
+    /**
+     *  Common validation things for both NTCP 1 and 2.
+     *  Call after receiving Alice's RouterIdentity (in message 3).
+     *  _peerSkew must be set.
+     *
+     *  Side effect: sets _msg3p2FailReason when returning false
+     *
+     *  @return success or calls fail() and returns false
+     *  @since 0.9.36 pulled out of verifyInbound()
+     */
+    private boolean verifyInbound(Hash aliceHash) {
+        // get inet-addr
+        InetAddress addr = this._con.getChannel().socket().getInetAddress();
+        byte[] ip = (addr == null) ? null : addr.getAddress();
+        if (_context.banlist().isBanlistedForever(aliceHash)) {
+            if (_log.shouldLog(Log.WARN))
+                _log.warn("Dropping inbound connection from permanently banlisted peer: " + aliceHash);
+            // 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: " + aliceHash);
+            _msg3p2FailReason = NTCPConnection.REASON_BANNED;
+            return false;
+        }
+        if(ip != null)
+           _transport.setIP(aliceHash, 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(aliceHash, true);
+            // Only banlist if we know what time it is
+            _context.banlist().banlistRouter(DataHelper.formatDuration(diff),
+                                             aliceHash,
+                                               _x("Excessive clock skew: {0}"));
+            _transport.setLastBadSkew(_peerSkew);
+            fail("Clocks too skewed (" + diff + " ms)", null, true);
+            _msg3p2FailReason = NTCPConnection.REASON_SKEW;
+            return false;
+        } else if (_log.shouldLog(Log.DEBUG)) {
+            _log.debug(prefix()+"Clock skew: " + diff + " ms");
+        }
+        return true;
+    }
+
     /**
      *  We are Bob. Send message #4 to Alice.
      *
      *  State must be VERIFIED.
      *  Caller must synch.
+     *
+     *  @param h Alice's Hash
      */
-    private void sendInboundConfirm(RouterIdentity alice, long tsA) {
+    private void sendInboundConfirm(Hash h, 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;
@@ -496,6 +592,438 @@ class InboundEstablishState extends EstablishBase {
         _transport.getPumper().wantsWrite(_con, _e_bobSig);
     }
 
+    //// NTCP2 below here
+
+    /**
+     *  NTCP2 only. State must be one of IB_NTCP2_*
+     *
+     *  we are Bob, so receive these bytes as part of an inbound connection
+     *  This method receives messages 1 and 3, and sends message 2.
+     *
+     *  All data must be copied out of the buffer as Reader.processRead()
+     *  will return it to the pool.
+     *
+     *  @since 0.9.36
+     */
+    private synchronized void receiveInboundNTCP2(ByteBuffer src) {
+        if (_state == State.IB_NTCP2_INIT && src.hasRemaining()) {
+            // use _X for the buffer
+            int toGet = Math.min(src.remaining(), MSG1_SIZE - _received);
+            src.get(_X, _received, toGet);
+            _received += toGet;
+            if (_received < MSG1_SIZE) {
+                // TODO if we got less than 64 should we even be here?
+                if (_log.shouldWarn())
+                    _log.warn("Short buffer got " + toGet + " total now " + _received);
+                return;
+            }
+            changeState(State.IB_NTCP2_GOT_X);
+            _received = 0;
+
+            // replay check using encrypted key
+            if (!_transport.isHXHIValid(_X)) {
+                _context.statManager().addRateData("ntcp.replayHXxorBIH", 1);
+                fail("Replay msg 1, eX = " + Base64.encode(_X, 0, KEY_SIZE));
+                return;
+            }
+
+            try {
+                _handshakeState = new HandshakeState(HandshakeState.RESPONDER, _transport.getXDHFactory());
+            } catch (GeneralSecurityException gse) {
+                throw new IllegalStateException("bad proto", gse);
+            }
+            _handshakeState.getLocalKeyPair().setPublicKey(_transport.getNTCP2StaticPubkey(), 0);
+            _handshakeState.getLocalKeyPair().setPrivateKey(_transport.getNTCP2StaticPrivkey(), 0);
+            Hash h = _context.routerHash();
+            SessionKey bobHash = new SessionKey(h.getData());
+            // save encrypted data for CBC for msg 2
+            System.arraycopy(_X, KEY_SIZE - IV_SIZE, _prevEncrypted, 0, IV_SIZE);
+            _context.aes().decrypt(_X, 0, _X, 0, bobHash, _transport.getNTCP2StaticIV(), KEY_SIZE);
+            if (DataHelper.eqCT(_X, 0, ZEROKEY, 0, KEY_SIZE)) {
+                fail("Bad msg 1, X = 0");
+                return;
+            }
+            byte options[] = new byte[OPTIONS1_SIZE];
+            try {
+                _handshakeState.start();
+                if (_log.shouldWarn())
+                    _log.warn("After start: " + _handshakeState.toString());
+                _handshakeState.readMessage(_X, 0, MSG1_SIZE, options, 0);
+            } catch (GeneralSecurityException gse) {
+                // Read a random number of bytes, store wanted in _padlen1
+                _padlen1 = _context.random().nextInt(PADDING1_FAIL_MAX) - src.remaining();
+                if (_padlen1 > 0) {
+                    // delayed fail for probing resistance
+                    // need more bytes before failure
+                    if (_log.shouldWarn())
+                        _log.warn("Bad msg 1, X = " + Base64.encode(_X, 0, KEY_SIZE) + " with " + src.remaining() +
+                                  " more bytes, waiting for " + _padlen1 + " more bytes", gse);
+                    changeState(State.IB_NTCP2_READ_RANDOM);
+                } else {
+                    // got all we need, fail now
+                    fail("Bad msg 1, X = " + Base64.encode(_X, 0, KEY_SIZE) + " remaining = " + src.remaining(), gse);
+                }
+                return;
+            } catch (RuntimeException re) {
+                fail("Bad msg 1, X = " + Base64.encode(_X, 0, KEY_SIZE), re);
+                return;
+            }
+            if (_log.shouldWarn())
+                _log.warn("After msg 1: " + _handshakeState.toString());
+            int v = options[1] & 0xff;
+            if (v != NTCPTransport.NTCP2_INT_VERSION) {
+                fail("Bad version: " + v);
+                return;
+            }
+            _padlen1 = (int) DataHelper.fromLong(options, 2, 2);
+            _msg3p2len = (int) DataHelper.fromLong(options, 4, 2);
+            long tsA = DataHelper.fromLong(options, 8, 4);
+            long now = _context.clock().now();
+            // In NTCP1, timestamp comes in msg 3 so we know the RTT.
+            // In NTCP2, it comes in msg 1, so just guess.
+            // We could defer this to msg 3 to calculate the RTT?
+            long rtt = 250;
+            _peerSkew = (now - (tsA * 1000) - (rtt / 2) + 500) / 1000; 
+            if ((_peerSkew > MAX_SKEW || _peerSkew < 0 - MAX_SKEW) &&
+                !_context.clock().getUpdatedSuccessfully()) {
+                // If not updated successfully, allow it.
+                // This isn't very likely, outbound will do it first
+                // See verifyInbound() above.
+                fail("Clock Skew: " + _peerSkew, null, true);
+                return;
+            }
+            if (_padlen1 > PADDING1_MAX) {
+                fail("bad msg 1 padlen: " + _padlen1);
+                return;
+            }
+            if (_msg3p2len < MSG3P2_MIN || _msg3p2len > MSG3P2_MAX) {
+                fail("bad msg3p2 len: " + _msg3p2len);
+                return;
+            }
+            if (_padlen1 <= 0) {
+                // No padding specified, go straight to sending msg 2
+                changeState(State.IB_NTCP2_GOT_PADDING);
+                if (src.hasRemaining()) {
+                    // Inbound conn can never have extra data after msg 1
+                    fail("Extra data after msg 1: " + src.remaining());
+                } else {
+                    // write msg 2
+                    prepareOutbound2();
+                }
+                return;
+            }
+        }
+
+        // delayed fail for probing resistance
+        if (_state == State.IB_NTCP2_READ_RANDOM && src.hasRemaining()) {
+            // read more bytes before failing
+            _received += src.remaining();
+            if (_received < _padlen1) {
+                if (_log.shouldWarn())
+                    _log.warn("Bad msg 1, got " + src.remaining() +
+                              " more bytes, waiting for " + (_padlen1 - _received) + " more bytes");
+            } else {
+                fail("Bad msg 1, failing after getting " + src.remaining() + " more bytes");
+            }
+            return;
+        }
+
+        if (_state == State.IB_NTCP2_GOT_X && src.hasRemaining()) {
+            // skip this if _padlen1 == 0;
+            // use _X for the buffer
+            int toGet = Math.min(src.remaining(), _padlen1 - _received);
+            src.get(_X, _received, toGet);
+            _received += toGet;
+            if (_received < _padlen1)
+                return;
+            changeState(State.IB_NTCP2_GOT_PADDING);
+            _handshakeState.mixHash(_X, 0, _padlen1);
+            if (_log.shouldWarn())
+                _log.warn("After mixhash padding " + _padlen1 + " msg 1: " + _handshakeState.toString());
+            _received = 0;
+            if (src.hasRemaining()) {
+                // Inbound conn can never have extra data after msg 1
+                fail("Extra data after msg 1: " + src.remaining());
+            } else {
+                // write msg 2
+                prepareOutbound2();
+            }
+            return;
+        }
+
+        if (_state == State.IB_NTCP2_SENT_Y && src.hasRemaining()) {
+            int msg3tot = MSG3P1_SIZE + _msg3p2len;
+            if (_msg3tmp == null)
+                _msg3tmp = _dataReadBufs.acquire();
+            // use _X for the buffer FIXME too small
+            byte[] tmp = _msg3tmp.getData();
+            int toGet = Math.min(src.remaining(), msg3tot - _received);
+            src.get(tmp, _received, toGet);
+            _received += toGet;
+            if (_received < msg3tot)
+                return;
+            changeState(State.IB_NTCP2_GOT_RI);
+            _received = 0;
+            ByteArray ptmp = _dataReadBufs.acquire();
+            byte[] payload = ptmp.getData();
+            try {
+                _handshakeState.readMessage(tmp, 0, msg3tot, payload, 0);
+            } catch (GeneralSecurityException gse) {
+                // TODO delayed failure per spec, as in NTCPConnection.delayedClose()
+                _dataReadBufs.release(ptmp, false);
+                fail("Bad msg 3, part 1 is:\n" + net.i2p.util.HexDump.dump(tmp, 0, MSG3P1_SIZE), gse);
+                return;
+            } catch (RuntimeException re) {
+                _dataReadBufs.release(ptmp, false);
+                fail("Bad msg 3", re);
+                return;
+            }
+            if (_log.shouldWarn())
+                _log.warn("After msg 3: " + _handshakeState.toString());
+            try {
+                // calls callbacks below
+                NTCP2Payload.processPayload(_context, this, payload, 0, _msg3p2len - MAC_SIZE, true);
+            } catch (IOException ioe) {
+                fail("Bad msg 3 payload", ioe);
+                // probably payload frame/block problems
+                // setDataPhase() will send termination
+                if (_msg3p2FailReason < 0)
+                    _msg3p2FailReason = NTCPConnection.REASON_FRAMING;
+            } catch (DataFormatException dfe) {
+                fail("Bad msg 3 payload", dfe);
+                // probably RI problems
+                // setDataPhase() will send termination
+                if (_msg3p2FailReason < 0)
+                    _msg3p2FailReason = NTCPConnection.REASON_SIGFAIL;
+                _context.statManager().addRateData("ntcp.invalidInboundSignature", 1);
+            } catch (I2NPMessageException ime) {
+                // shouldn't happen, no I2NP msgs in msg3p2
+                fail("Bad msg 3 payload", ime);
+                // setDataPhase() will send termination
+                if (_msg3p2FailReason < 0)
+                    _msg3p2FailReason = 0;
+            } finally {
+                _dataReadBufs.release(ptmp, false);
+            }
+
+            // pass buffer for processing of "extra" data
+            setDataPhase(src);
+        }
+        // TODO check for remaining data and log/throw
+    }
+
+    /**
+     *  Write the 2nd NTCP2 message.
+     *  IV (CBC from msg 1) must be in _prevEncrypted
+     *
+     *  @since 0.9.36
+     */
+    private synchronized void prepareOutbound2() {
+        // create msg 2 payload
+        byte[] options2 = new byte[OPTIONS2_SIZE];
+        int padlen2 = _context.random().nextInt(PADDING2_MAX);
+        DataHelper.toLong(options2, 2, 2, padlen2);
+        long now = _context.clock().now() / 1000;
+        DataHelper.toLong(options2, 8, 4, now);
+        byte[] tmp = new byte[MSG2_SIZE + padlen2];
+        try {
+            _handshakeState.writeMessage(tmp, 0, options2, 0, OPTIONS2_SIZE);
+        } catch (GeneralSecurityException gse) {
+            // buffer length error
+            if (!_log.shouldWarn())
+                _log.error("Bad msg 2 out", gse);
+            fail("Bad msg 2 out", gse);
+            return;
+        } catch (RuntimeException re) {
+            if (!_log.shouldWarn())
+                _log.error("Bad msg 2 out", re);
+            fail("Bad msg 2 out", re);
+            return;
+        }
+        if (_log.shouldWarn())
+            _log.warn("After msg 2: " + _handshakeState.toString());
+        Hash h = _context.routerHash();
+        SessionKey bobHash = new SessionKey(h.getData());
+        _context.aes().encrypt(tmp, 0, tmp, 0, bobHash, _prevEncrypted, KEY_SIZE);
+        if (padlen2 > 0) {
+            _context.random().nextBytes(tmp, MSG2_SIZE, padlen2);
+            _handshakeState.mixHash(tmp, MSG2_SIZE, padlen2);
+            if (_log.shouldWarn())
+                _log.warn("After mixhash padding " + padlen2 + " msg 2: " + _handshakeState.toString());
+        }
+
+        changeState(State.IB_NTCP2_SENT_Y);
+        // send it all at once
+        _transport.getPumper().wantsWrite(_con, tmp);
+    }
+
+    /**
+     *  KDF for NTCP2 data phase,
+     *  then calls con.finishInboundEstablishment(),
+     *  passing over the final keys and states to the con.
+     *
+     *  This changes the state to VERIFIED.
+     *
+     *  @param buf possibly containing "extra" data for data phase
+     *  @since 0.9.36
+     */
+    private synchronized void setDataPhase(ByteBuffer buf) {
+        // Data phase ChaChaPoly keys
+        CipherStatePair ckp = _handshakeState.split();
+        CipherState rcvr = ckp.getReceiver();
+        CipherState sender = ckp.getSender();
+        byte[] k_ab = rcvr.getKey();
+        byte[] k_ba = sender.getKey();
+
+        // Data phase SipHash keys
+        byte[][] sipkeys = generateSipHashKeys(_context, _handshakeState);
+        byte[] sip_ab = sipkeys[0];
+        byte[] sip_ba = sipkeys[1];
+
+        if (_msg3p2FailReason >= 0) {
+            if (_log.shouldWarn())
+                _log.warn("Failed msg3p2, code " + _msg3p2FailReason + " for " + this);
+            _con.failInboundEstablishment(sender, sip_ba, _msg3p2FailReason);
+        } else {
+            if (_log.shouldWarn()) {
+                _log.warn("Finished establishment for " + this +
+                          "\nGenerated ChaCha key for A->B: " + Base64.encode(k_ab) +
+                          "\nGenerated ChaCha key for B->A: " + Base64.encode(k_ba) +
+                          "\nGenerated SipHash key for A->B: " + Base64.encode(sip_ab) +
+                          "\nGenerated SipHash key for B->A: " + Base64.encode(sip_ba));
+            }
+            // skew in seconds
+            _con.finishInboundEstablishment(sender, rcvr, sip_ba, sip_ab, _peerSkew, _hisPadding);
+            changeState(State.VERIFIED);
+            if (buf.hasRemaining()) {
+                // process "extra" data
+                // This is very likely for inbound, as data should come right after message 3
+                if (_log.shouldInfo())
+                    _log.info("extra data " + buf.remaining() + " on " + this);
+                 _con.recvEncryptedI2NP(buf);
+            }
+        }
+        // zero out everything
+        releaseBufs(true);
+        _handshakeState.destroy();
+        Arrays.fill(sip_ab, (byte) 0);
+        Arrays.fill(sip_ba, (byte) 0);
+    }
+
+    //// PayloadCallbacks
+
+    /**
+     *  Get "s" static key out of RI, compare to what we got in the handshake.
+     *  Tell NTCPConnection who it is.
+     *
+     *  @param isHandshake always true
+     *  @throws DataFormatException on bad sig, unknown SigType, no static key,
+     *                                 static key mismatch, IP checks in verifyInbound()
+     *  @since 0.9.36
+     */
+    public void gotRI(RouterInfo ri, boolean isHandshake, boolean flood) throws DataFormatException {
+        // Validate Alice static key
+        String s = null;
+        // find address with matching version
+        List addrs = ri.getTargetAddresses(NTCPTransport.STYLE, NTCPTransport.STYLE2);
+        for (RouterAddress addr : addrs) {
+            String v = addr.getOption("v");
+            if (v == null ||
+                (!v.equals(NTCPTransport.NTCP2_VERSION) && !v.startsWith(NTCPTransport.NTCP2_VERSION_ALT))) {
+                 continue;
+            }
+            s = addr.getOption("s");
+            if (s != null)
+                break;
+        }
+        if (s == null) {
+            _msg3p2FailReason = NTCPConnection.REASON_S_MISMATCH;
+            throw new DataFormatException("no s in RI");
+        }
+        byte[] sb = Base64.decode(s);
+        if (sb == null || sb.length != KEY_SIZE) {
+            _msg3p2FailReason = NTCPConnection.REASON_S_MISMATCH;
+            throw new DataFormatException("bad s in RI");
+        }
+        byte[] nb = new byte[32];
+        // compare to the _handshakeState
+        _handshakeState.getRemotePublicKey().getPublicKey(nb, 0);
+        if (!DataHelper.eqCT(sb, 0, nb, 0, KEY_SIZE)) {
+            _msg3p2FailReason = NTCPConnection.REASON_S_MISMATCH;
+            throw new DataFormatException("s mismatch in RI");
+        }
+        _aliceIdent = ri.getIdentity();
+        Hash h = _aliceIdent.calculateHash();
+        // this sets the reason
+        boolean ok = verifyInbound(h);
+        if (!ok)
+            throw new DataFormatException("NTCP2 verifyInbound() fail");
+        try {
+            RouterInfo old = _context.netDb().store(h, ri);
+            if (flood && !ri.equals(old)) {
+                FloodfillNetworkDatabaseFacade fndf = (FloodfillNetworkDatabaseFacade) _context.netDb();
+                if (fndf.floodConditional(ri)) {
+                    if (_log.shouldLog(Log.WARN))
+                        _log.warn("Flooded the RI: " + h);
+                } else {
+                    if (_log.shouldLog(Log.WARN))
+                        _log.warn("Flood request but we didn't: " + h);
+                }
+            }
+        } catch (IllegalArgumentException iae) {
+            // hash collision?
+            _msg3p2FailReason = NTCPConnection.REASON_UNSPEC;
+            throw new DataFormatException("RI store fail", iae);
+        }
+        _con.setRemotePeer(_aliceIdent);
+    }
+
+    /** @since 0.9.36 */
+    public void gotOptions(byte[] options, boolean isHandshake) {
+        if (options.length < 12) {
+            if (_log.shouldWarn())
+                _log.warn("Got options length " + options.length + " on: " + this);
+            return;
+        }
+        float tmin = (options[0] & 0xff) / 16.0f;
+        float tmax = (options[1] & 0xff) / 16.0f;
+        float rmin = (options[2] & 0xff) / 16.0f;
+        float rmax = (options[3] & 0xff) / 16.0f;
+        int tdummy = (int) DataHelper.fromLong(options, 4, 2);
+        int rdummy = (int) DataHelper.fromLong(options, 6, 2);
+        int tdelay = (int) DataHelper.fromLong(options, 8, 2);
+        int rdelay = (int) DataHelper.fromLong(options, 10, 2);
+        _hisPadding = new NTCP2Options(tmin, tmax, rmin, rmax,
+                                       tdummy, rdummy, tdelay, rdelay);
+    }
+
+    /** @since 0.9.36 */
+    public void gotPadding(int paddingLength, int frameLength) {}
+
+    // Following 4 are illegal in handshake, we will never get them
+
+    /** @since 0.9.36 */
+    public void gotTermination(int reason, long lastReceived) {}
+    /** @since 0.9.36 */
+    public void gotUnknown(int type, int len) {}
+    /** @since 0.9.36 */
+    public void gotDateTime(long time) {}
+    /** @since 0.9.36 */
+    public void gotI2NP(I2NPMessage msg) {}
+
+    /**
+     *  @since 0.9.16
+     */
+    @Override
+    protected synchronized void fail(String reason, Exception e, boolean bySkew) {
+        super.fail(reason, e, bySkew);
+        if (_handshakeState != null) {
+            if (_log.shouldWarn())
+                _log.warn("State at failure: " + _handshakeState.toString());
+            _handshakeState.destroy();
+        }
+    }
+
     /**
      *  Only call once. Caller must synch.
      *  @since 0.9.16
@@ -507,6 +1035,11 @@ class InboundEstablishState extends EstablishBase {
         // NTCPConnection to use as the IV
         if (!isVerified)
             SimpleByteCache.release(_curEncrypted);
+        Arrays.fill(_X, (byte) 0);
         SimpleByteCache.release(_X);
+        if (_msg3tmp != null) {
+            _dataReadBufs.release(_msg3tmp, false);
+            _msg3tmp = null;
+        }
     }
 }
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 685a67a61..c659772ed 100644
--- a/router/java/src/net/i2p/router/transport/ntcp/NTCPConnection.java
+++ b/router/java/src/net/i2p/router/transport/ntcp/NTCPConnection.java
@@ -6,7 +6,10 @@ import java.net.Inet6Address;
 import java.nio.ByteBuffer;
 import java.nio.channels.SelectionKey;
 import java.nio.channels.SocketChannel;
+import java.security.GeneralSecurityException;
+import java.util.Arrays;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.Queue;
 import java.util.Set;
@@ -17,9 +20,14 @@ import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicLong;
 import java.util.zip.Adler32;
 
+import com.southernstorm.noise.protocol.CipherState;
+
+import net.i2p.crypto.SipHashInline;
 import net.i2p.data.Base64;
 import net.i2p.data.ByteArray;
+import net.i2p.data.DataFormatException;
 import net.i2p.data.DataHelper;
+import net.i2p.data.Hash;
 import net.i2p.data.router.RouterAddress;
 import net.i2p.data.router.RouterIdentity;
 import net.i2p.data.router.RouterInfo;
@@ -31,18 +39,22 @@ import net.i2p.data.i2np.I2NPMessageHandler;
 import net.i2p.router.OutNetMessage;
 import net.i2p.router.Router;
 import net.i2p.router.RouterContext;
+import net.i2p.router.networkdb.kademlia.FloodfillNetworkDatabaseFacade;
 import net.i2p.router.transport.FIFOBandwidthLimiter;
 import net.i2p.router.transport.FIFOBandwidthLimiter.Request;
+import net.i2p.router.transport.ntcp.NTCP2Payload.Block;
 import net.i2p.router.util.PriBlockingQueue;
 import net.i2p.util.ByteCache;
 import net.i2p.util.ConcurrentHashSet;
 import net.i2p.util.HexDump;
 import net.i2p.util.Log;
+import net.i2p.util.SimpleTimer2;
 import net.i2p.util.SystemVersion;
 import net.i2p.util.VersionComparator;
 
 /**
  * Coordinate the connection to a single peer.
+ * NTCP 1 or 2.
  *
  * Public only for UI peers page. Not a public API, not for external use.
  *
@@ -103,21 +115,13 @@ public class NTCPConnection implements Closeable {
     //private final CoDelPriorityBlockingQueue _outbound;
     private final PriBlockingQueue _outbound;
     /**
-     *  current prepared OutNetMessage, or null - synchronize on _outbound to modify or read
-     *  FIXME why do we need this???
+     *  current prepared OutNetMessages, or empty - synchronize to modify or read
      */
-    private OutNetMessage _currentOutbound;
+    private final List _currentOutbound;
     private SessionKey _sessionKey;
-    /** encrypted block of the current I2NP message being read */
-    private byte _curReadBlock[];
-    /** next byte to which data should be placed in the _curReadBlock */
-    private int _curReadBlockIndex;
-    private final byte _decryptBlockBuf[];
-    /** last AES block of the encrypted I2NP message (to serve as the next block's IV) */
-    private byte _prevReadBlock[];
     private byte _prevWriteEnd[];
     /** current partially read I2NP message */
-    private final ReadState _curReadState;
+    private ReadState _curReadState;
     private final AtomicInteger _messagesRead = new AtomicInteger();
     private final AtomicInteger _messagesWritten = new AtomicInteger();
     private long _lastSendTime;
@@ -126,13 +130,11 @@ public class NTCPConnection implements Closeable {
     private final long _created;
     // prevent sending meta before established
     private long _nextMetaTime = Long.MAX_VALUE;
-    private int _consecutiveZeroReads;
+    private final AtomicInteger _consecutiveZeroReads = new AtomicInteger();
 
     private static final int BLOCK_SIZE = 16;
     private static final int META_SIZE = BLOCK_SIZE;
 
-    /** unencrypted outbound metadata buffer */
-    private final byte _meta[] = new byte[META_SIZE];
     private boolean _sendingMeta;
     /** how many consecutive sends were failed due to (estimated) send queue time */
     //private int _consecutiveBacklog;
@@ -170,72 +172,102 @@ public class NTCPConnection implements Closeable {
     private static final AtomicLong __connID = new AtomicLong();
     private final long _connID = __connID.incrementAndGet();
     
+    //// NTCP2 things
+
+    private static final int PADDING_RAND_MIN = 16;
+    private static final int PADDING_MAX = 64;
+    private static final int SIP_IV_LENGTH = 8;
+    private static final int NTCP2_FAIL_READ = 1024;
+    private static final long NTCP2_FAIL_TIMEOUT = 10*1000;
+    private static final long NTCP2_TERMINATION_CLOSE_DELAY = 50;
+    static final int REASON_UNSPEC = 0;
+    static final int REASON_TERMINATION = 1;
+    static final int REASON_TIMEOUT = 2;
+    static final int REASON_AEAD = 4;
+    static final int REASON_OPTIONS = 5;
+    static final int REASON_SIGTYPE = 6;
+    static final int REASON_SKEW = 7;
+    static final int REASON_PADDING = 8;
+    static final int REASON_FRAMING = 9;
+    static final int REASON_PAYLOAD = 10;
+    static final int REASON_MSG1 = 11;
+    static final int REASON_MSG2 = 12;
+    static final int REASON_MSG3 = 13;
+    static final int REASON_FRAME_TIMEOUT = 14;
+    static final int REASON_SIGFAIL = 15;
+    static final int REASON_S_MISMATCH = 16;
+    static final int REASON_BANNED = 17;
+    static final int PADDING_MIN_DEFAULT_INT = 1;
+    static final int PADDING_MAX_DEFAULT_INT = 2;
+    private static final float PADDING_MIN_DEFAULT = PADDING_MIN_DEFAULT_INT / 16.0f;
+    private static final float PADDING_MAX_DEFAULT = PADDING_MAX_DEFAULT_INT / 16.0f;
+    static final int DUMMY_DEFAULT = 0;
+    static final int DELAY_DEFAULT = 0;
+    private static final NTCP2Options OUR_PADDING = new NTCP2Options(PADDING_MIN_DEFAULT, PADDING_MAX_DEFAULT,
+                                                                     PADDING_MIN_DEFAULT, PADDING_MAX_DEFAULT,
+                                                                     DUMMY_DEFAULT, DUMMY_DEFAULT,
+                                                                     DELAY_DEFAULT, DELAY_DEFAULT);
+    private static final int MIN_PADDING_RANGE = 64;
+    private NTCP2Options _paddingConfig;
+    private int _version;
+    private CipherState _sender;
+    private long _sendSipk1, _sendSipk2;
+    private byte[] _sendSipIV;
+
+
     /**
-     * Create an inbound connected (though not established) NTCP connection
-     *
+     * Create an inbound connected (though not established) NTCP connection.
+     * Caller MUST call transport.establishing(this) after construction.
+     * Caller MUST key.attach(this) after construction.
      */
     public NTCPConnection(RouterContext ctx, NTCPTransport transport, SocketChannel chan, SelectionKey key) {
-        _context = ctx;
-        _log = ctx.logManager().getLog(getClass());
-        _created = ctx.clock().now();
-        _transport = transport;
-        _remAddr = null;
+        this(ctx, transport, null, true);
         _chan = chan;
-        _readBufs = new ConcurrentLinkedQueue();
-        _writeBufs = new ConcurrentLinkedQueue();
-        _bwInRequests = new ConcurrentHashSet(2);
-        _bwOutRequests = new ConcurrentHashSet(8);
-        //_outbound = new CoDelPriorityBlockingQueue(ctx, "NTCP-Connection", 32);
-        _outbound = new PriBlockingQueue(ctx, "NTCP-Connection", 32);
-        _isInbound = true;
-        _decryptBlockBuf = new byte[BLOCK_SIZE];
-        _curReadState = new ReadState();
-        _establishState = new InboundEstablishState(ctx, transport, this);
+        _version = 1;
         _conKey = key;
-        _conKey.attach(this);
-        _inboundListener = new InboundListener();
-        _outboundListener = new OutboundListener();
-        initialize();
+        _establishState = new InboundEstablishState(ctx, transport, this);
     }
 
     /**
-     * Create an outbound unconnected NTCP connection
+     * Create an outbound unconnected NTCP connection.
+     * Caller MUST call transport.establishing(this) after construction.
      *
      * @param version must be 1 or 2
      */
     public NTCPConnection(RouterContext ctx, NTCPTransport transport, RouterIdentity remotePeer,
                           RouterAddress remAddr, int version) {
+        this(ctx, transport, remAddr, false);
+        _remotePeer = remotePeer;
+        _version = version;
+        if (version == 1)
+            _establishState = new OutboundEstablishState(ctx, transport, this);
+        else
+            _establishState = new OutboundNTCP2State(ctx, transport, this);
+    }
+
+    /**
+     * Base constructor in/out
+     * @since 0.9.36
+     */
+    private NTCPConnection(RouterContext ctx, NTCPTransport transport, RouterAddress remAddr, boolean isIn) {
         _context = ctx;
         _log = ctx.logManager().getLog(getClass());
         _created = ctx.clock().now();
         _transport = transport;
-        _remotePeer = remotePeer;
         _remAddr = remAddr;
+        _lastSendTime = _created;
+        _lastReceiveTime = _created;
+        _lastRateUpdated = _created;
         _readBufs = new ConcurrentLinkedQueue();
         _writeBufs = new ConcurrentLinkedQueue();
         _bwInRequests = new ConcurrentHashSet(2);
         _bwOutRequests = new ConcurrentHashSet(8);
         //_outbound = new CoDelPriorityBlockingQueue(ctx, "NTCP-Connection", 32);
         _outbound = new PriBlockingQueue(ctx, "NTCP-Connection", 32);
-        _isInbound = false;
-        //if (version == 1)
-            _establishState = new OutboundEstablishState(ctx, transport, this);
-        //else
-        //    _establishState = // TODO
-        _decryptBlockBuf = new byte[BLOCK_SIZE];
-        _curReadState = new ReadState();
+        _currentOutbound = new ArrayList(1);
+        _isInbound = isIn;
         _inboundListener = new InboundListener();
         _outboundListener = new OutboundListener();
-        initialize();
-    }
-
-    private void initialize() {
-        _lastSendTime = _created;
-        _lastReceiveTime = _created;
-        _lastRateUpdated = _created;
-        _curReadBlock = new byte[BLOCK_SIZE];
-        _prevReadBlock = new byte[BLOCK_SIZE];
-        _transport.establishing(this);
     }
 
     /**
@@ -249,6 +281,7 @@ public class NTCPConnection implements Closeable {
     public SelectionKey getKey() { return _conKey; }
     public void setChannel(SocketChannel chan) { _chan = chan; }
     public void setKey(SelectionKey key) { _conKey = key; }
+
     public boolean isInbound() { return _isInbound; }
     public boolean isEstablished() { return _establishState.isComplete(); }
 
@@ -261,9 +294,10 @@ public class NTCPConnection implements Closeable {
     }
 
     /**
-     *  Only valid during establishment; null later
+     *  Only valid during establishment;
+     *  replaced with EstablishState.VERIFIED or FAILED afterward
      */
-    public EstablishState getEstablishState() { return _establishState; }
+    EstablishState getEstablishState() { return _establishState; }
 
     /**
      *  Only valid for outbound; null for inbound
@@ -274,17 +308,23 @@ public class NTCPConnection implements Closeable {
      *  Valid for outbound; valid for inbound after handshake
      */
     public RouterIdentity getRemotePeer() { return _remotePeer; }
+
+    /**
+     *  Valid for outbound; valid for inbound after handshake
+     */
     public void setRemotePeer(RouterIdentity ident) { _remotePeer = ident; }
 
     /** 
-     * We are Bob.
+     * We are Bob. NTCP1 only.
+     *
+     * Caller MUST call recvEncryptedI2NP() after, for any remaining bytes in receive buffer
      *
      * @param clockSkew OUR clock minus ALICE's clock in seconds (may be negative, obviously, but |val| should
      *                  be under 1 minute)
-     * @param prevWriteEnd exactly 16 bytes, not copied, do not corrupt
-     * @param prevReadEnd 16 or more bytes, last 16 bytes copied
+     * @param prevWriteEnd exactly 16 bytes, not copied, do not corrupt, the write AES IV
+     * @param prevReadEnd 16 or more bytes, last 16 bytes copied as the read AES IV
      */
-    public void finishInboundEstablishment(SessionKey key, long clockSkew, byte prevWriteEnd[], byte prevReadEnd[]) {
+    void finishInboundEstablishment(SessionKey key, long clockSkew, byte prevWriteEnd[], byte prevReadEnd[]) {
         NTCPConnection toClose = locked_finishInboundEstablishment(key, clockSkew, prevWriteEnd, prevReadEnd);
         if (toClose != null) {
             if (_log.shouldLog(Log.DEBUG))
@@ -296,20 +336,27 @@ public class NTCPConnection implements Closeable {
     }
     
     /** 
-     * We are Bob.
+     * We are Bob. NTCP1 only.
      *
      * @param clockSkew OUR clock minus ALICE's clock in seconds (may be negative, obviously, but |val| should
      *                  be under 1 minute)
-     * @param prevWriteEnd exactly 16 bytes, not copied, do not corrupt
-     * @param prevReadEnd 16 or more bytes, last 16 bytes copied
+     * @param prevWriteEnd exactly 16 bytes, not copied, do not corrupt, the write AES IV
+     * @param prevReadEnd 16 or more bytes, last 16 bytes copied as the read AES IV
      * @return old conn to be closed by caller, or null
      */
     private synchronized NTCPConnection locked_finishInboundEstablishment(
             SessionKey key, long clockSkew, byte prevWriteEnd[], byte prevReadEnd[]) {
+        if (_establishState == EstablishBase.VERIFIED) {
+            IllegalStateException ise = new IllegalStateException("Already finished on " + this);
+            _log.error("Already finished", ise);
+            throw ise;
+        }
+        byte[] prevReadBlock = new byte[BLOCK_SIZE];
+        System.arraycopy(prevReadEnd, prevReadEnd.length - BLOCK_SIZE, prevReadBlock, 0, BLOCK_SIZE);
+        _curReadState = new NTCP1ReadState(prevReadBlock);
         _sessionKey = key;
         _clockSkew = clockSkew;
         _prevWriteEnd = prevWriteEnd;
-        System.arraycopy(prevReadEnd, prevReadEnd.length - BLOCK_SIZE, _prevReadBlock, 0, BLOCK_SIZE);
         _establishedOn = _context.clock().now();
         NTCPConnection rv = _transport.inboundEstablished(this);
         _nextMetaTime = _establishedOn + (META_FREQUENCY / 2) + _context.random().nextInt(META_FREQUENCY);
@@ -337,21 +384,20 @@ public class NTCPConnection implements Closeable {
     public int getMessagesReceived() { return _messagesRead.get(); }
 
     public int getOutboundQueueSize() {
-            int queued;
-            synchronized(_outbound) {
-                queued = _outbound.size();
-                if (getCurrentOutbound() != null)
-                    queued++;
+            int queued = _outbound.size();
+            synchronized(_currentOutbound) {
+                queued += _currentOutbound.size();
             }
             return queued;
     }
-    
-    private OutNetMessage getCurrentOutbound() {
-        synchronized(_outbound) {
-            return _currentOutbound;
+
+    /** @since 0.9.36 */
+    private boolean hasCurrentOutbound() {
+        synchronized(_currentOutbound) {
+            return ! _currentOutbound.isEmpty();
         }
     }
-
+    
     /** @return milliseconds */
     public long getTimeSinceSend() { return _context.clock().now()-_lastSendTime; }
 
@@ -367,6 +413,24 @@ public class NTCPConnection implements Closeable {
      */
     public long getCreated() { return _created; }
 
+    /**
+     *  The NTCP2 version, for the console.
+     *  For outbound, will not change.
+     *  For inbound, defaults to 1, may change to 2 after establishment.
+     *
+     *  @return the version, 1 or 2
+     *  @since 0.9.36
+     */
+    public int getVersion() { return _version; }
+
+    /**
+     *  Set version 2 from InboundEstablishState.
+     *  Just for logging, so we know before finishInboundEstablish() is called.
+     *
+     *  @since 0.9.36
+     */
+    public void setVersion(int ver) { _version = ver; }
+
     /**
      * Sets to true.
      * @since 0.9.24
@@ -382,8 +446,8 @@ public class NTCPConnection implements Closeable {
      *  workaround for EventPumper
      *  @since 0.8.12
      */
-    public void clearZeroRead() {
-        _consecutiveZeroReads = 0;
+    void clearZeroRead() {
+        _consecutiveZeroReads.set(0);
     }
 
     /**
@@ -391,8 +455,8 @@ public class NTCPConnection implements Closeable {
      *  @return value after incrementing
      *  @since 0.8.12
      */
-    public int gotZeroRead() {
-        return ++_consecutiveZeroReads;
+    int gotZeroRead() {
+        return _consecutiveZeroReads.incrementAndGet();
     }
 
     public boolean isClosed() { return _closed.get(); }
@@ -404,8 +468,13 @@ public class NTCPConnection implements Closeable {
             _log.logCloseLoop("NTCPConnection", this);
             return;
         }
-        if (_log.shouldLog(Log.INFO))
+        if (_version == 2) {
+            // for debugging
+            if (_log.shouldWarn())
+                _log.warn("Closing connection " + toString(), new Exception("cause"));
+        } else if (_log.shouldLog(Log.INFO)) {
             _log.info("Closing connection " + toString(), new Exception("cause"));
+        }
         NTCPConnection toClose = locked_close(allowRequeue);
         if (toClose != null && toClose != this) {
             if (_log.shouldLog(Log.WARN))
@@ -420,7 +489,7 @@ public class NTCPConnection implements Closeable {
      *  @param e may be null
      *  @since 0.9.16
      */
-    public void closeOnTimeout(String cause, Exception e) {
+    void closeOnTimeout(String cause, Exception e) {
         EstablishState es = _establishState;
         close();
         es.close(cause, e);
@@ -457,13 +526,29 @@ public class NTCPConnection implements Closeable {
         List pending = new ArrayList();
         //_outbound.drainAllTo(pending);
         _outbound.drainTo(pending);
-        for (OutNetMessage msg : pending) 
+        synchronized(_currentOutbound) {
+            if (!_currentOutbound.isEmpty())
+                pending.addAll(_currentOutbound);
+            _currentOutbound.clear();
+        }
+        for (OutNetMessage msg : pending) {
             _transport.afterSend(msg, false, allowRequeue, msg.getLifetime());
-
-        OutNetMessage msg = getCurrentOutbound();
-        if (msg != null) 
-            _transport.afterSend(msg, false, allowRequeue, msg.getLifetime());
-        
+        }
+        // zero out everything we can
+        if (_curReadState != null) {
+            _curReadState.destroy();
+            _curReadState = null;
+        }
+        if (_sender != null) {
+            _sender.destroy();
+            _sender = null;
+        }
+        _sendSipk1 = 0;
+        _sendSipk2 = 0;
+        if (_sendSipIV != null) {
+            Arrays.fill(_sendSipIV, (byte) 0);
+            _sendSipIV = null;
+        }
         return old;
     }
     
@@ -477,8 +562,7 @@ public class NTCPConnection implements Closeable {
             _transport.afterSend(msg, false, false, msg.getLifetime());
             return;
         }
-        boolean noOutbound = (getCurrentOutbound() == null);
-        if (isEstablished() && noOutbound)
+        if (isEstablished() && !hasCurrentOutbound())
             _transport.getWriter().wantsWrite(this, "enqueued");
     }
 
@@ -493,13 +577,19 @@ public class NTCPConnection implements Closeable {
         if (_outbound.isBacklogged()) { // bloody arbitrary.  well, its half the average message lifetime...
             int size = _outbound.size();
             if (_log.shouldLog(Log.WARN)) {
-	        int writeBufs = _writeBufs.size();
-                boolean currentOutboundSet = getCurrentOutbound() != null;
+                int writeBufs = _writeBufs.size();
+                boolean currentOutboundSet;
+                long seq;
+                synchronized(_currentOutbound) {
+                    currentOutboundSet = !_currentOutbound.isEmpty();
+                    seq = currentOutboundSet ? _currentOutbound.get(0).getSeqNum() : -1;
+                }
                 try {
                     _log.warn("Too backlogged: size is " + size 
-                          + ", wantsWrite? " + (0 != (_conKey.interestOps()&SelectionKey.OP_WRITE))
-                          + ", currentOut set? " + currentOutboundSet
-			  + ", writeBufs: " + writeBufs + " on " + toString());
+                              + ", wantsWrite? " + (0 != (_conKey.interestOps()&SelectionKey.OP_WRITE))
+                              + ", currentOut set? " + currentOutboundSet
+                              + ", id: " + seq
+                              + ", writeBufs: " + writeBufs + " on " + toString());
                 } catch (RuntimeException e) {}  // java.nio.channels.CancelledKeyException
             }
             return true;
@@ -509,9 +599,30 @@ public class NTCPConnection implements Closeable {
     }
     
     /**
-     *  Inject a DatabaseStoreMessage with our RouterInfo
+     *  Inject a DatabaseStoreMessage with our RouterInfo. NTCP 1 or 2.
+     *
+     *  Externally, this is only called by NTCPTransport for outbound cons,
+     *  before the con is established, but we know what version it is.
+     *
+     *  Internally, may be called for outbound or inbound, but only after the
+     *  con is established, so we know what the version is.
      */
-    public void enqueueInfoMessage() {
+    void enqueueInfoMessage() {
+        if (_version == 1) {
+            enqueueInfoMessageNTCP1();
+            // may change to 2 for inbound
+        } else if (_isInbound) {
+            // TODO or if outbound and it's not right at the beginning
+            // TODO flood
+            sendOurRouterInfo(false);
+        }
+        // don't need to send for NTCP 2 outbound, it's in msg 3
+    }
+    
+    /**
+     *  Inject a DatabaseStoreMessage with our RouterInfo. NTCP 1 only.
+     */
+    private void enqueueInfoMessageNTCP1() {
         int priority = INFO_PRIORITY;
         if (_log.shouldLog(Log.INFO))
             _log.info("SENDING INFO message pri. " + priority + ": " + toString());
@@ -524,53 +635,50 @@ public class NTCPConnection implements Closeable {
     }
 
     /** 
-     * We are Alice.
+     * We are Alice. NTCP1 only.
+     *
+     * Caller MUST call recvEncryptedI2NP() after, for any remaining bytes in receive buffer
      *
      * @param clockSkew OUR clock minus BOB's clock in seconds (may be negative, obviously, but |val| should
      *                  be under 1 minute)
      * @param prevWriteEnd exactly 16 bytes, not copied, do not corrupt
      * @param prevReadEnd 16 or more bytes, last 16 bytes copied
      */
-    public synchronized void finishOutboundEstablishment(SessionKey key, long clockSkew, byte prevWriteEnd[], byte prevReadEnd[]) {
-        if (_log.shouldLog(Log.DEBUG))
-            _log.debug("outbound established (key=" + key + " skew=" + clockSkew + " prevWriteEnd=" + Base64.encode(prevWriteEnd) + ")");
+    synchronized void finishOutboundEstablishment(SessionKey key, long clockSkew, byte prevWriteEnd[], byte prevReadEnd[]) {
+        if (_establishState == EstablishBase.VERIFIED) {
+            IllegalStateException ise = new IllegalStateException("Already finished on " + this);
+            _log.error("Already finished", ise);
+            throw ise;
+        }
+        byte[] prevReadBlock = new byte[BLOCK_SIZE];
+        System.arraycopy(prevReadEnd, prevReadEnd.length - BLOCK_SIZE, prevReadBlock, 0, BLOCK_SIZE);
+        _curReadState = new NTCP1ReadState(prevReadBlock);
         _sessionKey = key;
         _clockSkew = clockSkew;
         _prevWriteEnd = prevWriteEnd;
-        System.arraycopy(prevReadEnd, prevReadEnd.length - BLOCK_SIZE, _prevReadBlock, 0, BLOCK_SIZE);
         if (_log.shouldLog(Log.DEBUG))
-            _log.debug("Outbound established, prevWriteEnd: " + Base64.encode(prevWriteEnd) + " prevReadEnd: " + Base64.encode(prevReadEnd));
+            _log.debug("outbound established (key=" + key + " skew=" + clockSkew +
+                       " prevWriteEnd: " + Base64.encode(prevWriteEnd) + " prevReadBlock: " + Base64.encode(prevReadBlock));
 
         _establishedOn = _context.clock().now();
         _establishState = EstablishBase.VERIFIED;
         _transport.markReachable(getRemotePeer().calculateHash(), false);
-        boolean msgs = !_outbound.isEmpty();
         _nextMetaTime = _establishedOn + (META_FREQUENCY / 2) + _context.random().nextInt(META_FREQUENCY);
         _nextInfoTime = _establishedOn + (INFO_FREQUENCY / 2) + _context.random().nextInt(INFO_FREQUENCY);
-        if (msgs)
+        if (!_outbound.isEmpty())
             _transport.getWriter().wantsWrite(this, "outbound established");
     }
     
     /**
-     * prepare the next i2np message for transmission.  this should be run from
-     * the Writer thread pool.
+     * Prepare the next I2NP message for transmission.  This should be run from
+     * the Writer thread pool. NTCP 1 or 2.
+     * 
+     * This is the entry point as called from Writer.Runner.run()
      * 
      * @param prep an instance of PrepBuffer to use as scratch space
      *
      */
     synchronized void prepareNextWrite(PrepBuffer prep) {
-            prepareNextWriteFast(prep);
-    }
-
-    /**
-     * prepare the next i2np message for transmission.  this should be run from
-     * the Writer thread pool.
-     *
-     * Caller must synchronize.
-     * @param buf a PrepBuffer to use as scratch space
-     *
-     */
-    private void prepareNextWriteFast(PrepBuffer buf) {
         if (_closed.get())
             return;
         // Must be established or else session key is null and we can't encrypt
@@ -580,29 +688,48 @@ public class NTCPConnection implements Closeable {
         if (!isEstablished()) {
             return;
         }
-        
+        if (_version == 1)
+            prepareNextWriteFast(prep);
+        else
+            prepareNextWriteNTCP2(prep);
+    }
+
+    /**
+     * Prepare the next I2NP message for transmission.  This should be run from
+     * the Writer thread pool. NTCP 1 only.
+     *
+     * Caller must synchronize.
+     * @param buf a PrepBuffer to use as scratch space
+     *
+     */
+    private void prepareNextWriteFast(PrepBuffer buf) {
         long now = _context.clock().now();
         if (_nextMetaTime <= now) {
             sendMeta();
             _nextMetaTime = now + (META_FREQUENCY / 2) + _context.random().nextInt(META_FREQUENCY / 2);
         }
       
-        OutNetMessage msg = null;
-        // this is synchronized only for _currentOutbound
-        // Todo: figure out how to remove the synchronization
-        synchronized (_outbound) {
-            if (_currentOutbound != null) {
+        OutNetMessage msg;
+        synchronized (_currentOutbound) {
+            if (!_currentOutbound.isEmpty()) {
                 if (_log.shouldLog(Log.INFO))
-                    _log.info("attempt for multiple outbound messages with " + System.identityHashCode(_currentOutbound) + " already waiting and " + _outbound.size() + " queued");
+                    _log.info("attempt for multiple outbound messages with " + _currentOutbound.size() + " already waiting and " + _outbound.size() + " queued");
                 return;
             }
+            while (true) {
                 msg = _outbound.poll();
                 if (msg == null)
                     return;
-            _currentOutbound = msg;
+                if (msg.getExpiration() >= now)
+                    break;
+                if (_log.shouldWarn())
+                    _log.warn("dropping message expired on queue: " + msg + " on " + this);
+                _transport.afterSend(msg, false, false, msg.getLifetime());
+            }
+            _currentOutbound.add(msg);
         }
-        
-        bufferedPrepare(msg,buf);
+
+        bufferedPrepare(msg, buf);
         _context.aes().encrypt(buf.unencrypted, 0, buf.encrypted, 0, _sessionKey, _prevWriteEnd, 0, buf.unencryptedLength);
         System.arraycopy(buf.encrypted, buf.encrypted.length-16, _prevWriteEnd, 0, _prevWriteEnd.length);
         _transport.getPumper().wantsWrite(this, buf.encrypted);
@@ -626,17 +753,15 @@ public class NTCPConnection implements Closeable {
      */
     private void bufferedPrepare(OutNetMessage msg, PrepBuffer buf) {
         I2NPMessage m = msg.getMessage();
-        buf.baseLength = m.toByteArray(buf.base);
-        int sz = buf.baseLength;
+        // 2 offset for size
+        int sz = m.toByteArray(buf.unencrypted, 2) - 2;
         int min = 2 + sz + 4;
         int rem = min % 16;
         int padding = 0;
         if (rem > 0)
             padding = 16 - rem;
-        
         buf.unencryptedLength = min+padding;
         DataHelper.toLong(buf.unencrypted, 0, 2, sz);
-        System.arraycopy(buf.base, 0, buf.unencrypted, 2, buf.baseLength);
         if (padding > 0) {
             _context.random().nextBytes(buf.unencrypted, 2+sz, padding);
         }
@@ -657,33 +782,252 @@ public class NTCPConnection implements Closeable {
         buf.encrypted = new byte[buf.unencryptedLength];
     }
 
-    public static class PrepBuffer {
+    static class PrepBuffer {
         final byte unencrypted[];
         int unencryptedLength;
-        final byte base[];
-        int baseLength;
         final Adler32 crc;
         byte encrypted[];
         
         public PrepBuffer() {
             unencrypted = new byte[BUFFER_SIZE];
-            base = new byte[BUFFER_SIZE];
             crc = new Adler32();
         }
 
         public void init() {
             unencryptedLength = 0;
-            baseLength = 0;
             encrypted = null;
             crc.reset();
         }
     }
+
+    /**
+     * Prepare the next I2NP message for transmission.  This should be run from
+     * the Writer thread pool.
+     *
+     * Caller must synchronize.
+     *
+     * @param buf we use buf.enencrypted only
+     * @since 0.9.36
+     */
+    private void prepareNextWriteNTCP2(PrepBuffer buf) {
+        int size = OutboundNTCP2State.MAC_SIZE;
+        List blocks = new ArrayList(4);
+        long now = _context.clock().now();
+        synchronized (_currentOutbound) {
+            if (!_currentOutbound.isEmpty()) {
+                if (_log.shouldLog(Log.INFO))
+                    _log.info("attempt for multiple outbound messages with " + _currentOutbound.size() + " already waiting and " + _outbound.size() + " queued");
+                return;
+            }
+            OutNetMessage msg;
+            while (true) {
+                msg = _outbound.poll();
+                if (msg == null)
+                    return;
+                if (msg.getExpiration() >= now)
+                    break;
+                if (_log.shouldWarn())
+                    _log.warn("dropping message expired on queue: " + msg + " on " + this);
+                _transport.afterSend(msg, false, false, msg.getLifetime());
+            }
+            _currentOutbound.add(msg);
+            // don't make combined msgs too big to minimize latency
+            final int MAX_MSG_SIZE = 5000;
+            I2NPMessage m = msg.getMessage();
+            Block block = new NTCP2Payload.I2NPBlock(m);
+            blocks.add(block);
+            size += block.getTotalLength();
+            // now add more (maybe)
+            if (size < MAX_MSG_SIZE) {
+                // keep adding as long as we will be under 5 KB
+                while (true) {
+                    msg = _outbound.peek();
+                    if (msg == null)
+                        break;
+                    m = msg.getMessage();
+                    int msz = m.getMessageSize() - 7;
+                    if (size + msz > MAX_MSG_SIZE)
+                        break;
+                    OutNetMessage msg2 = _outbound.poll();
+                    if (msg2 == null)
+                        break;
+                    if (msg2 != msg) {
+                        // if it wasn't the one we sized, put it back
+                        _outbound.offer(msg2);
+                        break;
+                    }
+                    if (msg.getExpiration() >= now) {
+                        block = new NTCP2Payload.I2NPBlock(m);
+                        blocks.add(block);
+                        size += NTCP2Payload.BLOCK_HEADER_SIZE + msz;
+                    } else {
+                        if (_log.shouldWarn())
+                            _log.warn("dropping message expired on queue: " + msg + " on " + this);
+                        _transport.afterSend(msg, false, false, msg.getLifetime());
+                    }
+                }
+            }
+        }
+        if (_nextMetaTime <= now && size + (NTCP2Payload.BLOCK_HEADER_SIZE + 4) <= BUFFER_SIZE) {
+            Block block = new NTCP2Payload.DateTimeBlock(_context);
+            blocks.add(block);
+            size += block.getTotalLength();
+            _nextMetaTime = now + (META_FREQUENCY / 2) + _context.random().nextInt(META_FREQUENCY / 2);
+            if (_log.shouldLog(Log.DEBUG))
+                _log.debug("Sending NTCP2 datetime block");
+        }
+        // 1024 is an estimate, do final check below
+        if (_nextInfoTime <= now && size + 1024 <= BUFFER_SIZE) {
+            RouterInfo ri = _context.router().getRouterInfo();
+            Block block = new NTCP2Payload.RIBlock(ri, false);
+            int sz = block.getTotalLength();
+            if (size + sz <= BUFFER_SIZE) {
+                blocks.add(block);
+                size += sz;
+                _nextInfoTime = now + (INFO_FREQUENCY / 2) + _context.random().nextInt(INFO_FREQUENCY);
+                if (_log.shouldLog(Log.INFO))
+                    _log.info("SENDING NTCP2 RI block");
+            } // else wait until next time
+        }
+        int availForPad = BUFFER_SIZE - (size + NTCP2Payload.BLOCK_HEADER_SIZE);
+        if (availForPad > 0) {
+            // what we want to send, calculated in proportion to data size
+            int minSend = (int) (size * _paddingConfig.getSendMin());
+            int maxSend = (int) (size * _paddingConfig.getSendMax());
+            // the absolute min and max we can send
+            int min = Math.min(minSend, availForPad);
+            int max = Math.min(maxSend, availForPad);
+            int range = max - min;
+            if (range < MIN_PADDING_RANGE) {
+                // reduce min to enforce minimum range if possible
+                min = Math.max(0, min - (MIN_PADDING_RANGE - range));
+                range = max - min;
+            }
+            int padlen = min;
+            if (range > 0)
+                padlen += _context.random().nextInt(1 + range);
+            if (_log.shouldWarn())
+                _log.warn("Padding params:" +
+                          " size: " + size +
+                          " avail: " + availForPad +
+                          " minSend: " + minSend +
+                          " maxSend: " + maxSend +
+                          " min: " + min +
+                          " max: " + max +
+                          " range: " + range +
+                          " padlen: " + padlen);
+            // all zeros is fine here
+            //Block block = new NTCP2Payload.PaddingBlock(_context, padlen);
+            Block block = new NTCP2Payload.PaddingBlock(padlen);
+            blocks.add(block);
+            size += block.getTotalLength();
+        }
+        sendNTCP2(buf.unencrypted, blocks);
+    }
+
+    /**
+     *  NTCP2 only
+     *
+     *  @since 0.9.36
+     */
+    private void sendOurRouterInfo(boolean shouldFlood) {
+        sendRouterInfo(_context.router().getRouterInfo(), shouldFlood);
+    }
+
+    /**
+     *  NTCP2 only
+     *
+     *  @since 0.9.36
+     */
+    private void sendRouterInfo(RouterInfo ri, boolean shouldFlood) {
+        // no synch needed, sendNTCP2() is synched
+        if (_log.shouldWarn())
+            _log.warn("Sending router info for: " + ri.getHash() + " flood? " + shouldFlood);
+        List blocks = new ArrayList(2);
+        int plen = 2;
+        Block block = new NTCP2Payload.RIBlock(ri, shouldFlood);
+        plen += block.getTotalLength();
+        blocks.add(block);
+        int padlen = 1 + _context.random().nextInt(PADDING_MAX);
+        // all zeros is fine here
+        //block = new NTCP2Payload.PaddingBlock(_context, padlen);
+        block = new NTCP2Payload.PaddingBlock(padlen);
+        plen += block.getTotalLength();
+        blocks.add(block);
+        byte[] tmp = new byte[plen];
+        sendNTCP2(tmp, blocks);
+    }
+
+    /**
+     *  NTCP2 only
+     *
+     *  @since 0.9.36
+     */
+    private void sendTermination(int reason, int validFramesRcvd) {
+        // TODO add param to clear queues?
+        // no synch needed, sendNTCP2() is synched
+        if (_log.shouldWarn())
+            _log.warn("Sending termination, reason: " + reason + ", vaild frames rcvd: " + validFramesRcvd);
+        List blocks = new ArrayList(2);
+        int plen = 2;
+        Block block = new NTCP2Payload.TerminationBlock(reason, validFramesRcvd);
+        plen += block.getTotalLength();
+        blocks.add(block);
+        int padlen = 1 + _context.random().nextInt(PADDING_MAX);
+        // all zeros is fine here
+        //block = new NTCP2Payload.PaddingBlock(_context, padlen);
+        block = new NTCP2Payload.PaddingBlock(padlen);
+        plen += block.getTotalLength();
+        blocks.add(block);
+        byte[] tmp = new byte[plen];
+        sendNTCP2(tmp, blocks);
+    }
+
+    /**
+     *  This constructs the payload from the blocks, using the
+     *  tmp byte array, then encrypts the payload and
+     *  passes it to the pumper for writing.
+     *
+     *  @param tmp to be used for output of NTCP2Payload.writePayload(),
+     *         must have room for 2 byte length and block output
+     *  @since 0.9.36
+     */
+    private synchronized void sendNTCP2(byte[] tmp, List blocks) {
+        int payloadlen = NTCP2Payload.writePayload(tmp, 0, blocks);
+        int framelen = payloadlen + OutboundNTCP2State.MAC_SIZE;
+        // TODO use a buffer
+        byte[] enc = new byte[2 + framelen];
+        try {
+            _sender.encryptWithAd(null, tmp, 0, enc, 2, payloadlen);
+        } catch (GeneralSecurityException gse) {
+            // TODO anything else?
+            _log.error("data enc", gse);
+            return;
+        }
+
+        // siphash ^ len
+        long sipIV = SipHashInline.hash24(_sendSipk1, _sendSipk2, _sendSipIV);
+        enc[0] = (byte) ((framelen >> 8) ^ (sipIV >> 8));
+        enc[1] = (byte) (framelen ^ sipIV);
+        if (_log.shouldWarn()) {
+            StringBuilder buf = new StringBuilder(256);
+            buf.append("Sending ").append(blocks.size())
+               .append(" blocks in ").append(framelen)
+               .append(" byte NTCP2 frame:");
+            for (int i = 0; i < blocks.size(); i++) {
+                buf.append("\n    ").append(i).append(": ").append(blocks.get(i).toString());
+            }
+            _log.warn(buf.toString());
+        }
+        _transport.getPumper().wantsWrite(this, enc);
+        toLong8LE(_sendSipIV, 0, sipIV);
+    }
     
     /** 
      * async callback after the outbound connection was completed (this should NOT block, 
      * as it occurs in the selector thread)
      */
-    public void outboundConnected() {
+    void outboundConnected() {
         _conKey.interestOps(_conKey.interestOps() | SelectionKey.OP_READ);
         // schedule up the beginning of our handshaking by calling prepareNextWrite on the
         // writer thread pool
@@ -748,14 +1092,14 @@ public class NTCPConnection implements Closeable {
      * the buffer (not copy) and register ourselves to be notified when the 
      * contents have been fully allocated
      */
-    public void queuedRecv(ByteBuffer buf, FIFOBandwidthLimiter.Request req) {
+    void queuedRecv(ByteBuffer buf, FIFOBandwidthLimiter.Request req) {
         req.attach(buf);
         req.setCompleteListener(_inboundListener);
         addIBRequest(req);
     }
 
     /** ditto for writes */
-    public void queuedWrite(ByteBuffer buf, FIFOBandwidthLimiter.Request req) {
+    void queuedWrite(ByteBuffer buf, FIFOBandwidthLimiter.Request req) {
         req.attach(buf);
         req.setCompleteListener(_outboundListener);
         addOBRequest(req);
@@ -767,24 +1111,31 @@ public class NTCPConnection implements Closeable {
      * to do with as it pleases BUT it should eventually copy out the data
      * and call EventPumper.releaseBuf().
      */
-    public void recv(ByteBuffer buf) {
-        _bytesReceived += buf.remaining();
+    void recv(ByteBuffer buf) {
+        if (isClosed()) {
+            if (_log.shouldWarn())
+                _log.warn("recv() on closed con");
+            return;
+        }
+        synchronized(this) {
+            _bytesReceived += buf.remaining();
+            updateStats();
+        }
         _readBufs.offer(buf);
         _transport.getReader().wantsRead(this);
-        updateStats();
     }
 
     /**
      * The contents of the buffer have been encrypted / padded / etc and have
      * been fully allocated for the bandwidth limiter.
      */
-    public void write(ByteBuffer buf) {
+    void write(ByteBuffer buf) {
         _writeBufs.offer(buf);
         _transport.getPumper().wantsWrite(this);
     }
     
     /** @return null if none available */
-    public ByteBuffer getNextReadBuf() {
+    ByteBuffer getNextReadBuf() {
         return _readBufs.poll();
     }
 
@@ -792,57 +1143,66 @@ public class NTCPConnection implements Closeable {
      * Replaces getWriteBufCount()
      * @since 0.8.12
      */
-    public boolean isWriteBufEmpty() {
+    boolean isWriteBufEmpty() {
         return _writeBufs.isEmpty();
     }
 
     /** @return null if none available */
-    public ByteBuffer getNextWriteBuf() {
+    ByteBuffer getNextWriteBuf() {
         return _writeBufs.peek(); // not remove!  we removeWriteBuf afterwards
     }
     
     /**
      *  Remove the buffer, which _should_ be the one at the head of _writeBufs
      */
-    public void removeWriteBuf(ByteBuffer buf) {
-        _bytesSent += buf.capacity();
-        OutNetMessage msg = null;
-        boolean clearMessage = false;
-        if (_sendingMeta && (buf.capacity() == _meta.length)) {
-            _sendingMeta = false;
-        } else {
-            clearMessage = true;
+    void removeWriteBuf(ByteBuffer buf) {
+        // never clear OutNetMessages during establish phase
+        boolean clearMessage = isEstablished();
+        synchronized(this) {
+            _bytesSent += buf.capacity();
+            if (_sendingMeta && (buf.capacity() == META_SIZE)) {
+                _sendingMeta = false;
+                clearMessage = false;
+            }
+            updateStats();
         }
         _writeBufs.remove(buf);
         if (clearMessage) {
+            List msgs = null;
             // see synchronization comments in prepareNextWriteFast()
-            synchronized (_outbound) {
-                if (_currentOutbound != null) {
-                    msg = _currentOutbound;
-                    _currentOutbound = null;
+            synchronized (_currentOutbound) {
+                if (!_currentOutbound.isEmpty()) {
+                    msgs = new ArrayList(_currentOutbound);
+                    _currentOutbound.clear();
                 }
             }
-            if (msg != null) {
+            // push through the bw limiter to reach _writeBufs
+            if (!_outbound.isEmpty())
+                _transport.getWriter().wantsWrite(this, "write completed");
+            if (msgs != null) {
                 _lastSendTime = _context.clock().now();
-                _context.statManager().addRateData("ntcp.sendTime", msg.getSendTime());
-                if (_log.shouldLog(Log.DEBUG)) {
-                    _log.debug("I2NP message " + _messagesWritten + "/" + msg.getMessageId() + " sent after " 
-                              + msg.getSendTime() + "/"
-                              + msg.getLifetime()
-                              + " with " + buf.capacity() + " bytes (uid=" + System.identityHashCode(msg)+" on " + toString() + ")");
+                // stats once is fine for all of them
+                _context.statManager().addRateData("ntcp.sendTime", msgs.get(0).getSendTime());
+                for (OutNetMessage msg : msgs) {
+                    if (_log.shouldLog(Log.DEBUG)) {
+                        _log.debug("I2NP message " + _messagesWritten + "/" + msg.getMessageId() + " sent after " 
+                                  + msg.getSendTime() + "/"
+                                  + msg.getLifetime()
+                                  + " with " + buf.capacity() + " bytes (uid=" + System.identityHashCode(msg)+" on " + toString() + ")");
+                    }
+                    _transport.sendComplete(msg);
                 }
-                _messagesWritten.incrementAndGet();
-                _transport.sendComplete(msg);
+                _messagesWritten.addAndGet(msgs.size());
             }
         } else {
+            // push through the bw limiter to reach _writeBufs
+            if (!_outbound.isEmpty())
+                _transport.getWriter().wantsWrite(this, "write completed");
             if (_log.shouldLog(Log.INFO))
                 _log.info("I2NP meta message sent completely");
+            // need to increment as EventPumper will close conn if not completed
+            _messagesWritten.incrementAndGet();
         }
-        
-        if (getOutboundQueueSize() > 0) // push through the bw limiter to reach _writeBufs
-            _transport.getWriter().wantsWrite(this, "write completed");
-
-        updateStats();
     }
         
     private long _bytesReceived;
@@ -854,13 +1214,13 @@ public class NTCPConnection implements Closeable {
     private float _sendBps;
     private float _recvBps;
     
-    public float getSendRate() { return _sendBps; }
-    public float getRecvRate() { return _recvBps; }
+    public synchronized float getSendRate() { return _sendBps; }
+    public synchronized float getRecvRate() { return _recvBps; }
     
     /**
      *  Stats only for console
      */
-    private void updateStats() {
+    private synchronized void updateStats() {
         long now = _context.clock().now();
         long time = now - _lastRateUpdated;
         // If enough time has passed...
@@ -892,151 +1252,76 @@ public class NTCPConnection implements Closeable {
      * The NTCP connection now owns the buffer
      * BUT it must copy out the data
      * as reader will call EventPumper.releaseBuf().
+     *
+     * This is the entry point as called from Reader.processRead()
      */
     synchronized void recvEncryptedI2NP(ByteBuffer buf) {
-        // hasArray() is false for direct buffers, at least on my system...
-        if (_curReadBlockIndex == 0 && buf.hasArray()) {
-            // fast way
-            int tot = buf.remaining();
-            if (tot >= 32 && tot % 16 == 0) {
-                recvEncryptedFast(buf);
-                return;
-            }
-        }
-
-        while (buf.hasRemaining() && !_closed.get()) {
-            int want = Math.min(buf.remaining(), BLOCK_SIZE - _curReadBlockIndex);
-            if (want > 0) {
-                buf.get(_curReadBlock, _curReadBlockIndex, want);
-                _curReadBlockIndex += want;
-            }
-            if (_curReadBlockIndex >= BLOCK_SIZE) {
-                // cbc
-                _context.aes().decryptBlock(_curReadBlock, 0, _sessionKey, _decryptBlockBuf, 0);
-                for (int i = 0; i < BLOCK_SIZE; i++) {
-                    _decryptBlockBuf[i] ^= _prevReadBlock[i];
-                }
-                boolean ok = recvUnencryptedI2NP();
-                if (!ok) {
-                    if (_log.shouldLog(Log.INFO))
-                        _log.info("Read buffer " + System.identityHashCode(buf) + " contained corrupt data");
-                    _context.statManager().addRateData("ntcp.corruptDecryptedI2NP", 1);
-                    return;
-                }
-                byte swap[] = _prevReadBlock;
-                _prevReadBlock = _curReadBlock;
-                _curReadBlock = swap;
-                _curReadBlockIndex = 0;
-            }
-        }
-    }
-
-    /**
-     *  Decrypt directly out of the ByteBuffer instead of copying the bytes
-     *  16 at a time to the _curReadBlock / _prevReadBlock flip buffers.
-     *
-     *  More efficient but can only be used if buf.hasArray == true AND
-     *  _curReadBlockIndex must be 0 and buf.getRemaining() % 16 must be 0
-     *  and buf.getRemaining() must be >= 16.
-     *  All this is true for most buffers.
-     *  In theory this could be fixed up to handle the other cases too but that's hard.
-     *  Caller must synchronize!
-     *  @since 0.8.12
-     */
-    private void recvEncryptedFast(ByteBuffer buf) {
-        byte[] array = buf.array();
-        int pos = buf.arrayOffset();
-        int end = pos + buf.remaining();
-        boolean first = true;
-
-        for ( ; pos < end && !_closed.get(); pos += BLOCK_SIZE) {
-            _context.aes().decryptBlock(array, pos, _sessionKey, _decryptBlockBuf, 0);
-            if (first) {
-                for (int i = 0; i < BLOCK_SIZE; i++) {
-                    _decryptBlockBuf[i] ^= _prevReadBlock[i];
-                }
-                first = false;
-            } else {
-                int start = pos - BLOCK_SIZE;
-                for (int i = 0; i < BLOCK_SIZE; i++) {
-                    _decryptBlockBuf[i] ^= array[start + i];
-                }
-            }
-            boolean ok = recvUnencryptedI2NP();
-            if (!ok) {
-                if (_log.shouldLog(Log.INFO))
-                    _log.info("Read buffer " + System.identityHashCode(buf) + " contained corrupt data");
-                _context.statManager().addRateData("ntcp.corruptDecryptedI2NP", 1);
-                return;
-            }
-        }
-        // ...and copy to _prevReadBlock the last time
-        System.arraycopy(array, end - BLOCK_SIZE, _prevReadBlock, 0, BLOCK_SIZE);
-    }
-    
-    /**
-     *  Append the next 16 bytes of cleartext to the read state.
-     *  _decryptBlockBuf contains another cleartext block of I2NP to parse.
-     *  Caller must synchronize!
-     *  @return success
-     */
-    private boolean recvUnencryptedI2NP() {
-        _curReadState.receiveBlock(_decryptBlockBuf);
-        // FIXME move check to ReadState; must we close? possible attack vector?
-        if (_curReadState.getSize() > BUFFER_SIZE) {
-            if (_log.shouldLog(Log.WARN))
-                _log.warn("I2NP message too big - size: " + _curReadState.getSize() + " Dropping " + toString());
-            _context.statManager().addRateData("ntcp.corruptTooLargeI2NP", _curReadState.getSize());
-            close();
-            return false;
-        } else {
-            return true;
-        }
+        if (_curReadState == null)
+            throw new IllegalStateException("not established");
+        _curReadState.receive(buf);
     }
     
    /* 
     * One special case is a metadata message where the sizeof(data) is 0.  In
     * that case, the unencrypted message is encoded as:
+    * 
+    * 
     *  +-------+-------+-------+-------+-------+-------+-------+-------+
     *  |       0       |      timestamp in seconds     | uninterpreted             
     *  +-------+-------+-------+-------+-------+-------+-------+-------+
     *          uninterpreted           | adler checksum of sz+data+pad |
     *  +-------+-------+-------+-------+-------+-------+-------+-------+
+    * 
* + * Caller must synch + * + * @param unencrypted 16 bytes starting at off + * @param off the offset */ - private void readMeta(byte unencrypted[]) { - long ourTs = (_context.clock().now() + 500) / 1000; - long ts = DataHelper.fromLong(unencrypted, 2, 4); + private void readMeta(byte unencrypted[], int off) { Adler32 crc = new Adler32(); - crc.update(unencrypted, 0, unencrypted.length-4); + crc.update(unencrypted, off, META_SIZE - 4); long expected = crc.getValue(); - long read = DataHelper.fromLong(unencrypted, unencrypted.length-4, 4); + long read = DataHelper.fromLong(unencrypted, off + META_SIZE - 4, 4); if (read != expected) { if (_log.shouldLog(Log.WARN)) _log.warn("I2NP metadata message had a bad CRC value"); _context.statManager().addRateData("ntcp.corruptMetaCRC", 1); close(); return; - } else { - long newSkew = (ourTs - ts); - if (Math.abs(newSkew*1000) > Router.CLOCK_FUDGE_FACTOR) { - if (_log.shouldLog(Log.WARN)) - _log.warn("Peer's skew jumped too far (from " + _clockSkew + " s to " + newSkew + " s): " + toString()); - _context.statManager().addRateData("ntcp.corruptSkew", newSkew); - close(); - return; - } - _context.statManager().addRateData("ntcp.receiveMeta", newSkew); - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Received NTCP metadata, old skew of " + _clockSkew + " s, new skew of " + newSkew + "s."); - // FIXME does not account for RTT - _clockSkew = newSkew; } + long ts = DataHelper.fromLong(unencrypted, off + 2, 4); + receiveTimestamp(ts); + } + + /** + * Handle a received timestamp, NTCP 1 or 2. + * Caller must synch + * + * @param ts his timestamp in seconds, NOT ms + * @since 0.9.36 pulled out of readMeta() above + */ + private void receiveTimestamp(long ts) { + long ourTs = (_context.clock().now() + 500) / 1000; + long newSkew = (ourTs - ts); + if (Math.abs(newSkew*1000) > Router.CLOCK_FUDGE_FACTOR) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Peer's skew jumped too far (from " + _clockSkew + " s to " + newSkew + " s): " + toString()); + _context.statManager().addRateData("ntcp.corruptSkew", newSkew); + close(); + return; + } + _context.statManager().addRateData("ntcp.receiveMeta", newSkew); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Received NTCP metadata, old skew of " + _clockSkew + " s, new skew of " + newSkew + "s."); + // FIXME does not account for RTT + _clockSkew = newSkew; } /** * One special case is a metadata message where the sizeof(data) is 0. In * that case, the unencrypted message is encoded as: + * *
      *  +-------+-------+-------+-------+-------+-------+-------+-------+
      *  |       0       |      timestamp in seconds     | uninterpreted             
@@ -1044,24 +1329,24 @@ public class NTCPConnection implements Closeable {
      *          uninterpreted           | adler checksum of sz+data+pad |
      *  +-------+-------+-------+-------+-------+-------+-------+-------+
      *
+ * + * Caller must synchronize. */ private void sendMeta() { - byte encrypted[] = new byte[_meta.length]; - synchronized (_meta) { - DataHelper.toLong(_meta, 0, 2, 0); - DataHelper.toLong(_meta, 2, 4, (_context.clock().now() + 500) / 1000); - _context.random().nextBytes(_meta, 6, 6); - Adler32 crc = new Adler32(); - crc.update(_meta, 0, _meta.length-4); - DataHelper.toLong(_meta, _meta.length-4, 4, crc.getValue()); - _context.aes().encrypt(_meta, 0, encrypted, 0, _sessionKey, _prevWriteEnd, 0, _meta.length); - } - System.arraycopy(encrypted, encrypted.length-16, _prevWriteEnd, 0, _prevWriteEnd.length); + byte[] data = new byte[META_SIZE]; + DataHelper.toLong(data, 0, 2, 0); + DataHelper.toLong(data, 2, 4, (_context.clock().now() + 500) / 1000); + _context.random().nextBytes(data, 6, 6); + Adler32 crc = new Adler32(); + crc.update(data, 0, META_SIZE - 4); + DataHelper.toLong(data, META_SIZE - 4, 4, crc.getValue()); + _context.aes().encrypt(data, 0, data, 0, _sessionKey, _prevWriteEnd, 0, META_SIZE); + System.arraycopy(data, META_SIZE - 16, _prevWriteEnd, 0, _prevWriteEnd.length); // perhaps this should skip the bw limiter to reduce clock skew issues? if (_log.shouldLog(Log.DEBUG)) _log.debug("Sending NTCP metadata"); _sendingMeta = true; - _transport.getPumper().wantsWrite(this, encrypted); + _transport.getPumper().wantsWrite(this, data); } private static final int MAX_HANDLERS = 4; @@ -1098,6 +1383,11 @@ public class NTCPConnection implements Closeable { _i2npHandlers.clear(); } + private interface ReadState { + public void receive(ByteBuffer buf); + public void destroy(); + } + /** * Read the unencrypted message (16 bytes at a time). * verify the checksum, and pass it on to @@ -1115,95 +1405,250 @@ public class NTCPConnection implements Closeable { * the ReadState._data and ._bais when _size is > 0, so there are only * J 16KB buffers for the cons actually transmitting, instead of one per * con (including idle ones) + * + * Call all methods from synchronized parent method. + * */ - private class ReadState { + private class NTCP1ReadState implements ReadState { private int _size; private ByteArray _dataBuf; private int _nextWrite; - private long _expectedCrc; private final Adler32 _crc; private long _stateBegin; private int _blocks; + /** encrypted block of the current I2NP message being read */ + private byte _curReadBlock[]; + /** next byte to which data should be placed in the _curReadBlock */ + private int _curReadBlockIndex; + private final byte _decryptBlockBuf[]; + /** last AES block of the encrypted I2NP message (to serve as the next block's IV) */ + private byte _prevReadBlock[]; - public ReadState() { + /** + * @param prevReadBlock 16 bytes AES IV + */ + public NTCP1ReadState(byte[] prevReadBlock) { _crc = new Adler32(); + _prevReadBlock = prevReadBlock; + _curReadBlock = new byte[BLOCK_SIZE]; + _decryptBlockBuf = new byte[BLOCK_SIZE]; init(); } private void init() { _size = -1; _nextWrite = 0; - _expectedCrc = -1; _stateBegin = -1; _blocks = -1; _crc.reset(); if (_dataBuf != null) releaseReadBuf(_dataBuf); _dataBuf = null; + _curReadBlockIndex = 0; } - public int getSize() { return _size; } - + /** @since 0.9.36 */ + public void destroy() { + if (_dataBuf != null) { + releaseReadBuf(_dataBuf); + _dataBuf = null; + } + // TODO zero things out + } + /** - * Caller must synchronize - * @param buf 16 bytes + * Connection must be established! + * + * The contents of the buffer include some fraction of one or more + * encrypted and encoded I2NP messages. individual i2np messages are + * encoded as "sizeof(data)+data+pad+crc", and those are encrypted + * with the session key and the last 16 bytes of the previous encrypted + * i2np message. + * + * The NTCP connection now owns the buffer + * BUT it must copy out the data + * as reader will call EventPumper.releaseBuf(). + * + * @since 0.9.36 moved from parent class */ - public void receiveBlock(byte buf[]) { - if (_size == -1) { - receiveInitial(buf); - } else { - receiveSubsequent(buf); + public void receive(ByteBuffer buf) { + // hasArray() is false for direct buffers, at least on my system... + if (_curReadBlockIndex == 0 && buf.hasArray()) { + // fast way + int tot = buf.remaining(); + if (tot >= 32 && tot % 16 == 0) { + recvEncryptedFast(buf); + return; + } + } + + while (buf.hasRemaining() && !_closed.get()) { + int want = Math.min(buf.remaining(), BLOCK_SIZE - _curReadBlockIndex); + if (want > 0) { + buf.get(_curReadBlock, _curReadBlockIndex, want); + _curReadBlockIndex += want; + } + if (_curReadBlockIndex >= BLOCK_SIZE) { + // cbc + _context.aes().decryptBlock(_curReadBlock, 0, _sessionKey, _decryptBlockBuf, 0); + xor16(_prevReadBlock, _decryptBlockBuf); + boolean ok = recvUnencryptedI2NP(); + if (!ok) { + if (_log.shouldLog(Log.INFO)) + _log.info("Read buffer " + System.identityHashCode(buf) + " contained corrupt data, IV was: " + Base64.encode(_decryptBlockBuf)); + _context.statManager().addRateData("ntcp.corruptDecryptedI2NP", 1); + return; + } + byte swap[] = _prevReadBlock; + _prevReadBlock = _curReadBlock; + _curReadBlock = swap; + _curReadBlockIndex = 0; + } } } - /** @param buf 16 bytes */ - private void receiveInitial(byte buf[]) { - _size = (int)DataHelper.fromLong(buf, 0, 2); + /** + * Decrypt directly out of the ByteBuffer instead of copying the bytes + * 16 at a time to the _curReadBlock / _prevReadBlock flip buffers. + * + * More efficient but can only be used if buf.hasArray == true AND + * _curReadBlockIndex must be 0 and buf.getRemaining() % 16 must be 0 + * and buf.getRemaining() must be >= 16. + * All this is true for most incoming buffers. + * In theory this could be fixed up to handle the other cases too but that's hard. + * Caller must synchronize! + * + * @since 0.8.12, moved from parent class in 0.9.36 + */ + private void recvEncryptedFast(ByteBuffer buf) { + byte[] array = buf.array(); + int pos = buf.arrayOffset() + buf.position(); + int end = pos + buf.remaining(); + + // Copy to _curReadBlock for next IV... + System.arraycopy(array, end - BLOCK_SIZE, _curReadBlock, 0, BLOCK_SIZE); + // call aes().decrypt() to decrypt all at once, in place + // decrypt() will offload to the JVM/OS for larger sizes + _context.aes().decrypt(array, pos, array, pos, _sessionKey, _prevReadBlock, buf.remaining()); + + for ( ; pos < end; pos += BLOCK_SIZE) { + boolean ok = receiveBlock(array, pos); + if (!ok) { + if (_log.shouldLog(Log.INFO)) + _log.info("Read buffer " + System.identityHashCode(buf) + " contained corrupt data"); + _context.statManager().addRateData("ntcp.corruptDecryptedI2NP", 1); + return; + } + } + // ...and flip to _prevReadBlock for next time + byte swap[] = _prevReadBlock; + _prevReadBlock = _curReadBlock; + _curReadBlock = swap; + } + + /** + * Append the next 16 bytes of cleartext to the read state. + * _decryptBlockBuf contains another cleartext block of I2NP to parse. + * Caller must synchronize! + * + * @return success + * @since 0.9.36 moved from parent class + */ + private boolean recvUnencryptedI2NP() { + return receiveBlock(_decryptBlockBuf, 0); + } + + /** + * Caller must synchronize + * @param buf 16 bytes starting at off + * @param off offset + * @return success, only false on initial block with invalid size + */ + private boolean receiveBlock(byte buf[], int off) { + if (_size == -1) { + return receiveInitial(buf, off); + } else { + receiveSubsequent(buf, off); + return true; + } + } + + /** + * Caller must synchronize + * + * @param buf 16 bytes starting at off + * @param off offset + * @return success + */ + private boolean receiveInitial(byte buf[], int off) { + _size = (int)DataHelper.fromLong(buf, off, 2); + if (_size > BUFFER_SIZE) { + // this is typically an AES decryption error, not actually a large I2NP message + if (_log.shouldLog(Log.WARN)) + _log.warn("I2NP message too big - size: " + _size + " Closing " + NTCPConnection.this.toString(), new Exception()); + _context.statManager().addRateData("ntcp.corruptTooLargeI2NP", _size); + close(); + return false; + } if (_size == 0) { - readMeta(buf); + readMeta(buf, off); init(); } else { _stateBegin = _context.clock().now(); _dataBuf = acquireReadBuf(); - System.arraycopy(buf, 2, _dataBuf.getData(), 0, buf.length-2); - _nextWrite += buf.length-2; - _crc.update(buf); + System.arraycopy(buf, off + 2, _dataBuf.getData(), 0, BLOCK_SIZE - 2); + _nextWrite += BLOCK_SIZE - 2; + _crc.update(buf, off, BLOCK_SIZE); _blocks++; if (_log.shouldLog(Log.DEBUG)) _log.debug("new I2NP message with size: " + _size + " for message " + _messagesRead); } + return true; } - /** @param buf 16 bytes */ - private void receiveSubsequent(byte buf[]) { + /** + * Caller must synchronize + * + * @param buf 16 bytes starting at off + * @param off offset + */ + private void receiveSubsequent(byte buf[], int off) { _blocks++; int remaining = _size - _nextWrite; - int blockUsed = Math.min(buf.length, remaining); + int blockUsed = Math.min(BLOCK_SIZE, remaining); if (remaining > 0) { - System.arraycopy(buf, 0, _dataBuf.getData(), _nextWrite, blockUsed); + System.arraycopy(buf, off, _dataBuf.getData(), _nextWrite, blockUsed); _nextWrite += blockUsed; remaining -= blockUsed; } - if ( (remaining <= 0) && (buf.length-blockUsed < 4) ) { + if ( (remaining <= 0) && (BLOCK_SIZE - blockUsed < 4) ) { // we've received all the data but not the 4-byte checksum if (_log.shouldLog(Log.DEBUG)) _log.debug("crc wraparound required on block " + _blocks + " in message " + _messagesRead); - _crc.update(buf); + _crc.update(buf, off, BLOCK_SIZE); return; } else if (remaining <= 0) { - receiveLastBlock(buf); + receiveLastBlock(buf, off); } else { - _crc.update(buf); + _crc.update(buf, off, BLOCK_SIZE); } } - /** @param buf 16 bytes */ - private void receiveLastBlock(byte buf[]) { + /** + * This checks the checksum in buf only. + * All previous data, including that in buf, must have been copied to _dataBuf. + * Note that the checksum does not cover the padding. + * Caller must synchronize. + * + * @param buf 16 bytes starting at off + * @param off offset of the 16-byte block (NOT of the checksum only) + */ + private void receiveLastBlock(byte buf[], int off) { // on the last block - _expectedCrc = DataHelper.fromLong(buf, buf.length-4, 4); - _crc.update(buf, 0, buf.length-4); + long expectedCrc = DataHelper.fromLong(buf, off + BLOCK_SIZE - 4, 4); + _crc.update(buf, off, BLOCK_SIZE - 4); long val = _crc.getValue(); - if (val == _expectedCrc) { + if (val == expectedCrc) { try { I2NPMessageHandler h = acquireHandler(_context); @@ -1232,16 +1677,16 @@ public class NTCPConnection implements Closeable { } } catch (I2NPMessageException ime) { if (_log.shouldLog(Log.WARN)) { - _log.warn("Error parsing I2NP message" + - "\nDUMP:\n" + HexDump.dump(_dataBuf.getData(), 0, _size) + - "\nRAW:\n" + Base64.encode(_dataBuf.getData(), 0, _size) + + _log.warn("Error parsing I2NP message on " + NTCPConnection.this + + "\nDUMP:\n" + HexDump.dump(_dataBuf.getData(), 0, _size), ime); } _context.statManager().addRateData("ntcp.corruptI2NPIME", 1); } } else { if (_log.shouldLog(Log.WARN)) - _log.warn("CRC incorrect for message " + _messagesRead + " (calc=" + val + " expected=" + _expectedCrc + ") size=" + _size + " blocks " + _blocks); + _log.warn("CRC incorrect for message " + _messagesRead + " (calc=" + val + " expected=" + expectedCrc + + ") size=" + _size + " blocks=" + _blocks + " on: " + NTCPConnection.this); _context.statManager().addRateData("ntcp.corruptI2NPCRC", 1); } // get it ready for the next I2NP message @@ -1249,17 +1694,523 @@ public class NTCPConnection implements Closeable { } } + //// NTCP2 below here + + /** + * We are Alice. NTCP2 only. + * + * Caller MUST call recvEncryptedI2NP() after, for any remaining bytes in receive buffer + * + * @param clockSkew OUR clock minus BOB's clock in seconds (may be negative, obviously, but |val| should + * be under 1 minute) + * @param sender use to send to Bob + * @param receiver use to receive from Bob + * @param sip_ab 24 bytes to init SipHash to Bob + * @param sip_ba 24 bytes to init SipHash from Bob + * @since 0.9.36 + */ + synchronized void finishOutboundEstablishment(CipherState sender, CipherState receiver, + byte[] sip_ab, byte[] sip_ba, long clockSkew) { + finishEstablishment(sender, receiver, sip_ab, sip_ba, clockSkew); + _paddingConfig = OUR_PADDING; + _transport.markReachable(getRemotePeer().calculateHash(), false); + if (!_outbound.isEmpty()) + _transport.getWriter().wantsWrite(this, "outbound established"); + // NTCP2 outbound cannot have extra data + } + + /** + * We are Bob. NTCP2 only. + * + * Caller MUST call recvEncryptedI2NP() after, for any remaining bytes in receive buffer + * + * @param clockSkew OUR clock minus ALICE's clock in seconds (may be negative, obviously, but |val| should + * be under 1 minute) + * @param sender use to send to Alice + * @param receiver use to receive from Alice + * @param sip_ba 24 bytes to init SipHash to Alice + * @param sip_ab 24 bytes to init SipHash from Alice + * @param hisPadding may be null + * @since 0.9.36 + */ + synchronized void finishInboundEstablishment(CipherState sender, CipherState receiver, + byte[] sip_ba, byte[] sip_ab, long clockSkew, + NTCP2Options hisPadding) { + finishEstablishment(sender, receiver, sip_ba, sip_ab, clockSkew); + if (hisPadding != null) { + _paddingConfig = OUR_PADDING.merge(hisPadding); + if (_log.shouldWarn()) + _log.warn("Got padding options:" + + "\nhis padding options: " + hisPadding + + "\nour padding options: " + OUR_PADDING + + "\nmerged config is: " + _paddingConfig); + } else { + _paddingConfig = OUR_PADDING; + } + NTCPConnection toClose = _transport.inboundEstablished(this); + if (toClose != null) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Old connection closed: " + toClose + " replaced by " + this); + _context.statManager().addRateData("ntcp.inboundEstablishedDuplicate", toClose.getUptime()); + toClose.close(); + } + enqueueInfoMessage(); + } + + /** + * We are Bob. NTCP2 only. + * This is only for invalid payload received in message 3. We send a termination and close. + * There will be no receiving. + * + * @param sender use to send to Alice + * @param sip_ba 24 bytes to init SipHash to Alice + * @since 0.9.36 + */ + synchronized void failInboundEstablishment(CipherState sender, byte[] sip_ba, int reason) { + _sender = sender; + _sendSipk1 = fromLong8LE(sip_ba, 0); + _sendSipk2 = fromLong8LE(sip_ba, 8); + _sendSipIV = new byte[SIP_IV_LENGTH]; + System.arraycopy(sip_ba, 16, _sendSipIV, 0, SIP_IV_LENGTH); + if (_log.shouldWarn()) + _log.warn("Send SipHash keys: " + _sendSipk1 + ' ' + _sendSipk2 + ' ' + Base64.encode(_sendSipIV)); + _establishState = EstablishBase.VERIFIED; + _establishedOn = _context.clock().now(); + _nextMetaTime = Long.MAX_VALUE; + _nextInfoTime = Long.MAX_VALUE; + sendTermination(reason, 0); + try { Thread.sleep(NTCP2_TERMINATION_CLOSE_DELAY); } catch (InterruptedException ie) {} + close(); + } + + /** + * We are Alice or Bob. NTCP2 only. + * + * @param clockSkew see above + * @param sender use to send + * @param receiver use to receive + * @param sip_send 24 bytes to init SipHash out + * @param sip_recv 24 bytes to init SipHash in + * @since 0.9.36 + */ + private synchronized void finishEstablishment(CipherState sender, CipherState receiver, + byte[] sip_send, byte[] sip_recv, long clockSkew) { + if (_establishState == EstablishBase.VERIFIED) { + IllegalStateException ise = new IllegalStateException("Already finished on " + this); + _log.error("Already finished", ise); + throw ise; + } + _sender = sender; + _sendSipk1 = fromLong8LE(sip_send, 0); + _sendSipk2 = fromLong8LE(sip_send, 8); + _sendSipIV = new byte[SIP_IV_LENGTH]; + System.arraycopy(sip_send, 16, _sendSipIV, 0, SIP_IV_LENGTH); + if (_log.shouldWarn()) + _log.warn("Send SipHash keys: " + _sendSipk1 + ' ' + _sendSipk2 + ' ' + Base64.encode(_sendSipIV)); + _clockSkew = clockSkew; + _establishState = EstablishBase.VERIFIED; + _establishedOn = _context.clock().now(); + _nextMetaTime = _establishedOn + (META_FREQUENCY / 2) + _context.random().nextInt(META_FREQUENCY); + _nextInfoTime = _establishedOn + (INFO_FREQUENCY / 2) + _context.random().nextInt(INFO_FREQUENCY); + _curReadState = new NTCP2ReadState(receiver, sip_recv); + } + + /** + * Read the encrypted message + * + * Call all methods from synchronized parent method. + * + * @since 0.9.36 + */ + private class NTCP2ReadState implements ReadState, NTCP2Payload.PayloadCallback { + // temp to read the encrypted lengh into + private final byte[] _recvLen = new byte[2]; + private final long _sipk1, _sipk2; + // the siphash ratchet, as a byte array + private final byte[] _sipIV = new byte[SIP_IV_LENGTH]; + private final CipherState _rcvr; + // the size of the next frame, only valid if _received >= 0 + private int _framelen; + // bytes received, -2 to _framelen + private int _received = -2; + private ByteArray _dataBuf; + // Valid frames received in data phase + private int _frameCount; + // for logging only + private int _blockCount; + private boolean _terminated; + + /** + * @param keyData using first 24 bytes + */ + public NTCP2ReadState(CipherState rcvr, byte[] keyData) { + _rcvr = rcvr; + _sipk1 = fromLong8LE(keyData, 0); + _sipk2 = fromLong8LE(keyData, 8); + System.arraycopy(keyData, 16, _sipIV, 0, SIP_IV_LENGTH); + if (_log.shouldWarn()) + _log.warn("Recv SipHash keys: " + _sipk1 + ' ' + _sipk2 + ' ' + Base64.encode(_sipIV)); + } + + public void receive(ByteBuffer buf) { + if (_terminated) + return; + while (buf.hasRemaining()) { + if (_received == -2) { + _recvLen[0] = buf.get(); + _received++; + } + if (_received == -1 && buf.hasRemaining()) { + _recvLen[1] = buf.get(); + _received++; + long sipIV = SipHashInline.hash24(_sipk1, _sipk2, _sipIV); + //if (_log.shouldDebug()) + // _log.debug("Got Encrypted frame length: " + DataHelper.fromLong(_recvLen, 0, 2) + + // " byte 1: " + (_recvLen[0] & 0xff) + " byte 2: " + (_recvLen[1] & 0xff) + + // " decrypting with keys " + _sipk1 + ' ' + _sipk2 + ' ' + Base64.encode(_sipIV) + ' ' + sipIV); + _recvLen[0] ^= (byte) (sipIV >> 8); + _recvLen[1] ^= (byte) sipIV; + toLong8LE(_sipIV, 0, sipIV); + _framelen = (int) DataHelper.fromLong(_recvLen, 0, 2); + if (_framelen < OutboundNTCP2State.MAC_SIZE) { + if (_log.shouldWarn()) + _log.warn("Short frame length: " + _framelen); + // set a random length, then close + delayedClose(buf, _frameCount); + return; + } + //if (_log.shouldDebug()) + // _log.debug("Next frame length: " + _framelen); + } + int remaining = buf.remaining(); + if (remaining <= 0) + return; + if (_received == 0 && remaining >= _framelen) { + // shortcut, zero copy, decrypt directly to the ByteBuffer, + // overwriting the encrypted data + byte[] data = buf.array(); + int pos = buf.position(); + boolean ok = decryptAndProcess(data, pos); + buf.position(pos + _framelen); + if (!ok) { + delayedClose(buf, _frameCount); + return; + } + continue; + } + + // allocate ByteArray, + // unless we have one already and it's big enough + if (_received == 0 && (_dataBuf == null || _dataBuf.getData().length < _framelen)) { + if (_dataBuf != null && _dataBuf.getData().length == BUFFER_SIZE) + releaseReadBuf(_dataBuf); + if (_framelen > BUFFER_SIZE) { + if (_log.shouldWarn()) + _log.warn("Allocating big ByteArray: " + _framelen); + byte[] data = new byte[_framelen]; + _dataBuf = new ByteArray(data); + } else { + _dataBuf = acquireReadBuf(); + } + } + + // We now have a ByteArray in _dataBuf, + // copy from ByteBuffer to ByteArray + int toGet = Math.min(buf.remaining(), _framelen - _received); + byte[] data = _dataBuf.getData(); + buf.get(data, _received, toGet); + _received += toGet; + if (_received < _framelen) + return; + // decrypt to the ByteArray, overwriting the encrypted data + boolean ok = decryptAndProcess(data, 0); + // release buf only if we're not going around again + if (!ok || buf.remaining() < 2) { + if (!ok) + delayedClose(buf, _frameCount); + // delayedClose() may have zeroed out _databuf + if (_dataBuf != null) { + if (_dataBuf.getData().length == BUFFER_SIZE) + releaseReadBuf(_dataBuf); + _dataBuf = null; + } + if (!ok) + return; + } + // go around again + } + } + + /** + * Decrypts in place. + * Length is _framelen + * Side effects: Sets _received = -2, increments _frameCount and _blockCount if valid + * + * Does not call close() on failure. Caller MUST call delayedClose() if this returns false. + * + * @return success, false for fatal error (AEAD) only + */ + private boolean decryptAndProcess(byte[] data, int off) { + if (_log.shouldWarn()) + _log.warn("Decrypting frame " + _frameCount + " with " + _framelen + " bytes"); + try { + _rcvr.decryptWithAd(null, data, off, data, off, _framelen); + } catch (GeneralSecurityException gse) { + // TODO set a random length, then close + if (_log.shouldWarn()) + _log.warn("Bad AEAD data phase frame " + _frameCount + " on " + NTCPConnection.this, gse); + return false; + } + try { + int blocks = NTCP2Payload.processPayload(_context, this, data, off, + _framelen - OutboundNTCP2State.MAC_SIZE, false); + if (_log.shouldWarn()) + _log.warn("Processed " + blocks + " blocks in frame"); + _blockCount += blocks; + } catch (IOException ioe) { + if (_log.shouldWarn()) + _log.warn("Fail payload " + NTCPConnection.this, ioe); + } catch (DataFormatException dfe) { + if (_log.shouldWarn()) + _log.warn("Fail payload " + NTCPConnection.this, dfe); + } catch (I2NPMessageException ime) { + if (_log.shouldWarn()) + _log.warn("Error parsing I2NP message on " + NTCPConnection.this, ime); + _context.statManager().addRateData("ntcp.corruptI2NPIME", 1); + } + _received = -2; + _frameCount++; + return !_terminated; + } + + public void destroy() { + if (_dataBuf != null && _dataBuf.getData().length == BUFFER_SIZE) + releaseReadBuf(_dataBuf); + _dataBuf = null; + _rcvr.destroy(); + _terminated = true; + } + + //// PayloadCallbacks + + public void gotRI(RouterInfo ri, boolean isHandshake, boolean flood) throws DataFormatException { + if (_log.shouldWarn()) + _log.warn("Got updated RI"); + _messagesRead.incrementAndGet(); + try { + Hash h = ri.getHash(); + RouterInfo old = _context.netDb().store(h, ri); + if (flood && !ri.equals(old)) { + FloodfillNetworkDatabaseFacade fndf = (FloodfillNetworkDatabaseFacade) _context.netDb(); + if (fndf.floodConditional(ri)) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Flooded the RI: " + h); + } else { + if (_log.shouldLog(Log.WARN)) + _log.warn("Flood request but we didn't: " + h); + } + } + } catch (IllegalArgumentException iae) { + throw new DataFormatException("RI store fail", iae); + } + } + + public void gotDateTime(long time) { + if (_log.shouldWarn()) + _log.warn("Got updated datetime block"); + receiveTimestamp((time + 500) / 1000); + // update skew + } + + public void gotI2NP(I2NPMessage msg) { + if (_log.shouldWarn()) + _log.warn("Got I2NP msg: " + msg); + long timeToRecv = 0; // _context.clock().now() - _stateBegin; + int size = 100; // FIXME + _transport.messageReceived(msg, _remotePeer, null, timeToRecv, size); + _lastReceiveTime = _context.clock().now(); + _messagesRead.incrementAndGet(); + // TEST send back. null RI for target, not necesary + //if (_context.getBooleanProperty("i2np.ntcp2.loopback")) + // send(new OutNetMessage(_context, msg, _context.clock().now() + 10*1000, OutNetMessage.PRIORITY_MY_DATA, null)); + } + + public void gotOptions(byte[] options, boolean isHandshake) { + if (options.length < 12) { + if (_log.shouldWarn()) + _log.warn("Got options length " + options.length + " on: " + this); + return; + } + float tmin = (options[0] & 0xff) / 16.0f; + float tmax = (options[1] & 0xff) / 16.0f; + float rmin = (options[2] & 0xff) / 16.0f; + float rmax = (options[3] & 0xff) / 16.0f; + int tdummy = (int) DataHelper.fromLong(options, 4, 2); + int rdummy = (int) DataHelper.fromLong(options, 6, 2); + int tdelay = (int) DataHelper.fromLong(options, 8, 2); + int rdelay = (int) DataHelper.fromLong(options, 10, 2); + NTCP2Options hisPadding = new NTCP2Options(tmin, tmax, rmin, rmax, + tdummy, rdummy, tdelay, rdelay); + _paddingConfig = OUR_PADDING.merge(hisPadding); + if (_log.shouldWarn()) + _log.warn("Got padding options:" + + "\nhis padding options: " + hisPadding + + "\nour padding options: " + OUR_PADDING + + "\nmerged config is: " + _paddingConfig); + } + + public void gotTermination(int reason, long lastReceived) { + if (_log.shouldWarn()) + _log.warn("Got Termination: " + reason + " total rcvd: " + lastReceived); + _terminated = true; + close(); + } + + public void gotUnknown(int type, int len) { + if (_log.shouldWarn()) + _log.warn("Got unknown block type " + type + " length " + len); + } + + public void gotPadding(int paddingLength, int frameLength) { + if (_log.shouldWarn()) + _log.warn("Got " + paddingLength + + " bytes padding in " + frameLength + + " byte frame; ratio: " + (((float) paddingLength) / ((float) frameLength)) + + " configured min: " + _paddingConfig.getRecvMin() + + " configured max: " + _paddingConfig.getRecvMax()); + } + } + + /** + * After an AEAD failure, read a random number of bytes, + * with a brief timeout, and then fail. + * This replaces _curReadState, so no more messages will be received. + * + * @param buf possibly with data remaining + * @param validFramesRcvd to be sent in termination message + * @since 0.9.36 + */ + private void delayedClose(ByteBuffer buf, int validFramesRcvd) { + int toRead = 18 + _context.random().nextInt(NTCP2_FAIL_READ); + int remaining = toRead - buf.remaining(); + if (remaining > 0) { + if (_log.shouldWarn()) + _log.warn("delayed close after AEAD failure, to read: " + toRead); + _curReadState = new NTCP2FailState(toRead, validFramesRcvd); + _curReadState.receive(buf); + } else { + if (_log.shouldWarn()) + _log.warn("immediate close after AEAD failure and reading " + toRead); + sendTermination(REASON_AEAD, validFramesRcvd); + try { Thread.sleep(NTCP2_TERMINATION_CLOSE_DELAY); } catch (InterruptedException ie) {} + close(); + } + } + + /** + * After an AEAD failure, read a random number of bytes, + * with a brief timeout, and then fail. + * + * Call all methods from synchronized parent method. + * + * @since 0.9.36 + */ + private class NTCP2FailState extends SimpleTimer2.TimedEvent implements ReadState { + private final int _toRead; + private final int _validFramesRcvd; + private int _read; + + /** + * @param toRead how much left to read + * @param validFramesRcvd to be sent in termination message + */ + public NTCP2FailState(int toRead, int validFramesRcvd) { + super(_context.simpleTimer2()); + _toRead = toRead; + _validFramesRcvd = validFramesRcvd; + schedule(NTCP2_FAIL_TIMEOUT); + } + + public void receive(ByteBuffer buf) { + _read += buf.remaining(); + if (_read >= _toRead) { + cancel(); + if (_log.shouldWarn()) + _log.warn("close after AEAD failure and reading " + _toRead); + sendTermination(REASON_AEAD, _validFramesRcvd); + try { Thread.sleep(NTCP2_TERMINATION_CLOSE_DELAY); } catch (InterruptedException ie) {} + close(); + } + } + + public void destroy() { + cancel(); + } + + public void timeReached() { + if (_log.shouldWarn()) + _log.warn("timeout after AEAD failure waiting for more data"); + sendTermination(REASON_AEAD, _validFramesRcvd); + try { Thread.sleep(NTCP2_TERMINATION_CLOSE_DELAY); } catch (InterruptedException ie) {} + close(); + } + } + + //// Utils + + /** + * XOR a into b. Modifies b. a is unmodified. + * @param a 16 bytes + * @param b 16 bytes + * @since 0.9.36 + */ + private static void xor16(byte[] a, byte[] b) { + for (int i = 0; i < BLOCK_SIZE; i++) { + b[i] ^= a[i]; + } + } + + /** + * Little endian. + * Same as DataHelper.fromlongLE(src, offset, 8) but allows negative result + * + * @throws ArrayIndexOutOfBoundsException + * @since 0.9.36 + */ + private static long fromLong8LE(byte src[], int offset) { + long rv = 0; + for (int i = offset + 7; i >= offset; i--) { + rv <<= 8; + rv |= src[i] & 0xFF; + } + return rv; + } + + /** + * Little endian. + * Same as DataHelper.fromlongLE(target, offset, 8, value) but allows negative value + * + */ + private static void toLong8LE(byte target[], int offset, long value) { + int limit = offset + 8; + for (int i = offset; i < limit; i++) { + target[i] = (byte) value; + value >>= 8; + } + } + @Override public String toString() { - return "NTCP conn " + + return "NTCP" + _version + " conn " + _connID + - (_isInbound ? " from " : " to ") + + (_isInbound ? (" from " + _chan.socket().getInetAddress() + " port " + _chan.socket().getPort() + ' ') + : (" to " + _remAddr.getHost() + " port " + _remAddr.getPort() + ' ')) + (_remotePeer == null ? "unknown" : _remotePeer.calculateHash().toBase64().substring(0,6)) + (isEstablished() ? "" : " not established") + " created " + DataHelper.formatDuration(getTimeSinceCreated()) + " ago," + " last send " + DataHelper.formatDuration(getTimeSinceSend()) + " ago," + " last recv " + DataHelper.formatDuration(getTimeSinceReceive()) + " ago," + - " sent " + _messagesWritten + "," + + " sent " + _messagesWritten + ',' + " rcvd " + _messagesRead; } } diff --git a/router/java/src/net/i2p/router/transport/ntcp/NTCPTransport.java b/router/java/src/net/i2p/router/transport/ntcp/NTCPTransport.java index 2f5b21d69..e106e64be 100644 --- a/router/java/src/net/i2p/router/transport/ntcp/NTCPTransport.java +++ b/router/java/src/net/i2p/router/transport/ntcp/NTCPTransport.java @@ -8,6 +8,7 @@ import java.net.Inet6Address; import java.net.UnknownHostException; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; +import java.security.KeyPair; import java.text.DecimalFormat; import java.text.NumberFormat; import java.util.ArrayList; @@ -45,6 +46,9 @@ import net.i2p.router.transport.TransportImpl; import net.i2p.router.transport.TransportUtil; import static net.i2p.router.transport.TransportUtil.IPv6Config.*; import net.i2p.router.transport.crypto.DHSessionKeyBuilder; +import net.i2p.router.transport.crypto.X25519KeyFactory; +import net.i2p.router.transport.crypto.X25519PublicKey; +import net.i2p.router.transport.crypto.X25519PrivateKey; import net.i2p.router.util.DecayingHashSet; import net.i2p.router.util.DecayingBloomFilter; import net.i2p.util.Addresses; @@ -97,13 +101,15 @@ public class NTCPTransport extends TransportImpl { public final static String PROP_I2NP_NTCP_AUTO_PORT = "i2np.ntcp.autoport"; public final static String PROP_I2NP_NTCP_AUTO_IP = "i2np.ntcp.autoip"; private static final String PROP_ADVANCED = "routerconsole.advanced"; - public static final int DEFAULT_COST = 10; + private static final int DEFAULT_COST = 10; + private static final int NTCP2_OUTBOUND_COST = 14; /** this is rarely if ever used, default is to bind to wildcard address */ public static final String PROP_BIND_INTERFACE = "i2np.ntcp.bindInterface"; private final NTCPSendFinisher _finisher; private final DHSessionKeyBuilder.Factory _dhFactory; + private final X25519KeyFactory _xdhFactory; private long _lastBadSkew; private static final long[] RATES = { 10*60*1000 }; @@ -114,28 +120,32 @@ public class NTCPTransport extends TransportImpl { // NTCP2 stuff public static final String STYLE = "NTCP"; - private static final String STYLE2 = "NTCP2"; - private static final String PROP_NTCP2_ENABLE = "i2np.ntcp2.enable"; - private static final boolean DEFAULT_NTCP2_ENABLE = false; - private boolean _enableNTCP2; - private static final String NTCP2_PROTO_SHORT = "NXK2CS"; - private static final String OPT_NTCP2_SK = 'N' + NTCP2_PROTO_SHORT + "2s"; + public static final String STYLE2 = "NTCP2"; static final int NTCP2_INT_VERSION = 2; - private static final String NTCP2_VERSION = Integer.toString(NTCP2_INT_VERSION); + /** "2" */ + static final String NTCP2_VERSION = Integer.toString(NTCP2_INT_VERSION); + /** "2," */ + static final String NTCP2_VERSION_ALT = NTCP2_VERSION + ','; /** b64 static private key */ - private static final String PROP_NTCP2_SP = "i2np.ntcp2.sp"; + public static final String PROP_NTCP2_SP = "i2np.ntcp2.sp"; /** b64 static IV */ - private static final String PROP_NTCP2_IV = "i2np.ntcp2.iv"; - private static final int NTCP2_IV_LEN = 16; - private static final int NTCP2_KEY_LEN = 32; + public static final String PROP_NTCP2_IV = "i2np.ntcp2.iv"; + private static final int NTCP2_IV_LEN = OutboundNTCP2State.IV_SIZE; + private static final int NTCP2_KEY_LEN = OutboundNTCP2State.KEY_SIZE; + private final boolean _enableNTCP2; + private final byte[] _ntcp2StaticPubkey; private final byte[] _ntcp2StaticPrivkey; private final byte[] _ntcp2StaticIV; private final String _b64Ntcp2StaticPubkey; private final String _b64Ntcp2StaticIV; - public NTCPTransport(RouterContext ctx, DHSessionKeyBuilder.Factory dh) { + /** + * @param xdh null to disable NTCP2 + */ + public NTCPTransport(RouterContext ctx, DHSessionKeyBuilder.Factory dh, X25519KeyFactory xdh) { super(ctx); _dhFactory = dh; + _xdhFactory = xdh; _log = ctx.logManager().getLog(getClass()); _context.statManager().createRateStat("ntcp.sendTime", "Total message lifetime when sent completely", "ntcp", RATES); @@ -222,27 +232,31 @@ public class NTCPTransport extends TransportImpl { _nearCapacityCostBid = new SharedBid(105); _transientFail = new SharedBid(TransportBid.TRANSIENT_FAIL); - //_enableNTCP2 = ctx.getProperty(PROP_NTCP2_ENABLE, DEFAULT_NTCP2_ENABLE); - _enableNTCP2 = false; + _enableNTCP2 = xdh != null; if (_enableNTCP2) { boolean shouldSave = false; byte[] priv = null; byte[] iv = null; - String b64Pub = null; String b64IV = null; String s = ctx.getProperty(PROP_NTCP2_SP); if (s != null) { priv = Base64.decode(s); } if (priv == null || priv.length != NTCP2_KEY_LEN) { - priv = new byte[NTCP2_KEY_LEN]; - ctx.random().nextBytes(priv); + KeyPair keys = xdh.getKeys(); + _ntcp2StaticPrivkey = keys.getPrivate().getEncoded(); + _ntcp2StaticPubkey = keys.getPublic().getEncoded(); shouldSave = true; + } else { + _ntcp2StaticPrivkey = priv; + _ntcp2StaticPubkey = (new X25519PrivateKey(priv)).toPublic().getEncoded(); } - s = ctx.getProperty(PROP_NTCP2_IV); - if (s != null) { - iv = Base64.decode(s); - b64IV = s; + if (!shouldSave) { + s = ctx.getProperty(PROP_NTCP2_IV); + if (s != null) { + iv = Base64.decode(s); + b64IV = s; + } } if (iv == null || iv.length != NTCP2_IV_LEN) { iv = new byte[NTCP2_IV_LEN]; @@ -251,17 +265,17 @@ public class NTCPTransport extends TransportImpl { } if (shouldSave) { Map changes = new HashMap(2); - String b64Priv = Base64.encode(priv); + String b64Priv = Base64.encode(_ntcp2StaticPrivkey); b64IV = Base64.encode(iv); changes.put(PROP_NTCP2_SP, b64Priv); changes.put(PROP_NTCP2_IV, b64IV); ctx.router().saveConfig(changes, null); } - _ntcp2StaticPrivkey = priv; _ntcp2StaticIV = iv; - _b64Ntcp2StaticPubkey = "TODO"; // priv->pub + _b64Ntcp2StaticPubkey = Base64.encode(_ntcp2StaticPubkey); _b64Ntcp2StaticIV = b64IV; } else { + _ntcp2StaticPubkey = null; _ntcp2StaticPrivkey = null; _ntcp2StaticIV = null; _b64Ntcp2StaticPubkey = null; @@ -299,17 +313,17 @@ public class NTCPTransport extends TransportImpl { RouterIdentity ident = target.getIdentity(); Hash ih = ident.calculateHash(); NTCPConnection con = null; - boolean isNew = false; + int newVersion = 0; boolean fail = false; synchronized (_conLock) { con = _conByIdent.get(ih); if (con == null) { - isNew = true; RouterAddress addr = getTargetAddress(target); if (addr != null) { - int ver = getNTCPVersion(addr); - if (ver != 0) { - con = new NTCPConnection(_context, this, ident, addr, ver); + newVersion = getNTCPVersion(addr); + if (newVersion != 0) { + con = new NTCPConnection(_context, this, ident, addr, newVersion); + establishing(con); //if (_log.shouldLog(Log.DEBUG)) // _log.debug("Send on a new con: " + con + " at " + addr + " for " + ih); // Note that outbound conns go in the map BEFORE establishment @@ -331,9 +345,7 @@ public class NTCPTransport extends TransportImpl { afterSend(msg, false); return; } - if (isNew) { - // doesn't do anything yet, just enqueues it - con.send(msg); + if (newVersion != 0) { // As of 0.9.12, don't send our info if the first message is // doing the same (common when connecting to a floodfill). // Also, put the info message after whatever we are trying to send @@ -341,16 +353,27 @@ public class NTCPTransport extends TransportImpl { // Prior to 0.9.12, Bob would not send his RI unless he had ours, // but that's fixed in 0.9.12. boolean shouldSkipInfo = false; + boolean shouldFlood = false; I2NPMessage m = msg.getMessage(); if (m.getType() == DatabaseStoreMessage.MESSAGE_TYPE) { DatabaseStoreMessage dsm = (DatabaseStoreMessage) m; if (dsm.getKey().equals(_context.routerHash())) { shouldSkipInfo = true; + shouldFlood = dsm.getReplyToken() != 0; + // TODO tell the NTCP2 con to flood in the handshake and mark success when sent } } if (!shouldSkipInfo) { + // Queue the message, and our RI + // doesn't do anything yet, just enqueues it + con.send(msg); con.enqueueInfoMessage(); + } else if (shouldFlood || newVersion == 1) { + // Queue the message, which is a DSM of our RI + con.send(msg); } else if (_log.shouldLog(Log.INFO)) { + // Send nothing, the handshake has the RI + // version == 2 && shouldSkipInfo && !shouldFlood _log.info("SKIPPING INFO message: " + con); } @@ -365,6 +388,10 @@ public class NTCPTransport extends TransportImpl { _log.error("Error opening a channel", ioe); _context.statManager().addRateData("ntcp.outboundFailedIOEImmediate", 1); con.close(); + afterSend(msg, false); + } catch (IllegalStateException ise) { + _log.error("Failed opening a channel", ise); + afterSend(msg, false); } } else { con.send(msg); @@ -677,6 +704,7 @@ public class NTCPTransport extends TransportImpl { long tooOld = _context.clock().now() - 10*60*1000; for (NTCPConnection con : _conByIdent.values()) { + // TODO skip isEstablished() check? if (con.isEstablished() && con.getCreated() > tooOld) skews.addElement(Long.valueOf(con.getClockSkew())); } @@ -696,7 +724,7 @@ public class NTCPTransport extends TransportImpl { * As there is no timestamp in the first message, we can't detect * something long-delayed. To be fixed in next version of NTCP. * - * @param hxhi 32 bytes + * @param hxhi using first 8 bytes only * @return valid * @since 0.9.12 */ @@ -740,19 +768,37 @@ public class NTCPTransport extends TransportImpl { replaceAddress(addr); } else if (port > 0) { // all detected interfaces - for (InetAddress ia : getSavedLocalAddresses()) { - OrderedProperties props = new OrderedProperties(); - props.setProperty(RouterAddress.PROP_HOST, ia.getHostAddress()); - props.setProperty(RouterAddress.PROP_PORT, Integer.toString(port)); - addNTCP2Options(props); - int cost = getDefaultCost(ia instanceof Inet6Address); - myAddress = new RouterAddress(STYLE, props, cost); - replaceAddress(myAddress); + Collection addrs = getSavedLocalAddresses(); + if (!addrs.isEmpty()) { + for (InetAddress ia : addrs) { + OrderedProperties props = new OrderedProperties(); + props.setProperty(RouterAddress.PROP_HOST, ia.getHostAddress()); + props.setProperty(RouterAddress.PROP_PORT, Integer.toString(port)); + addNTCP2Options(props); + int cost = getDefaultCost(ia instanceof Inet6Address); + myAddress = new RouterAddress(STYLE, props, cost); + replaceAddress(myAddress); + } + } else if (_enableNTCP2) { + setOutboundNTCP2Address(); } + } else if (_enableNTCP2) { + setOutboundNTCP2Address(); } // TransportManager.startListening() calls router.rebuildRouterInfo() } + /** + * Outbound only, NTCP2 with "s" and "v" only + * @since 0.9.36 + */ + private void setOutboundNTCP2Address() { + OrderedProperties props = new OrderedProperties(); + addNTCP2Options(props); + RouterAddress myAddress = new RouterAddress(STYLE2, props, NTCP2_OUTBOUND_COST); + replaceAddress(myAddress); + } + /** * Only called by externalAddressReceived(). * Calls replaceAddress() or removeAddress(). @@ -966,6 +1012,14 @@ public class NTCPTransport extends TransportImpl { return _dhFactory.getBuilder(); } + /** + * @return null if not configured for NTCP2 + * @since 0.9.36 + */ + X25519KeyFactory getXDHFactory() { + return _xdhFactory; + } + /** * Return an unused DH key builder * to be put back onto the queue for reuse. @@ -1071,15 +1125,17 @@ public class NTCPTransport extends TransportImpl { } /** - * Add the required options to the properties for a NTCP2 address + * Add the required options to the properties for a NTCP2 address. + * Host/port must already be set in props if they are going to be. * * @since 0.9.35 */ private void addNTCP2Options(Properties props) { if (!_enableNTCP2) return; - props.setProperty("i", _b64Ntcp2StaticIV); - props.setProperty("n", NTCP2_PROTO_SHORT); + // only set i if we are not firewalled + if (props.containsKey("host")) + props.setProperty("i", _b64Ntcp2StaticIV); props.setProperty("s", _b64Ntcp2StaticPubkey); props.setProperty("v", NTCP2_VERSION); } @@ -1091,6 +1147,15 @@ public class NTCPTransport extends TransportImpl { */ boolean isNTCP2Enabled() { return _enableNTCP2; } + /** + * The static priv key + * + * @since 0.9.36 + */ + byte[] getNTCP2StaticPubkey() { + return _ntcp2StaticPubkey; + } + /** * The static priv key * @@ -1101,7 +1166,17 @@ public class NTCPTransport extends TransportImpl { } /** - * Get the valid NTCP version of this NTCP address. + * The static IV + * + * @since 0.9.36 + */ + byte[] getNTCP2StaticIV() { + return _ntcp2StaticIV; + } + + /** + * Get the valid NTCP version of Bob's NTCP address + * for our outbound connections as Alice. * * @return the valid version 1 or 2, or 0 if unusable * @since 0.9.35 @@ -1116,18 +1191,21 @@ public class NTCPTransport extends TransportImpl { } else if (style.equals(STYLE2)) { if (!_enableNTCP2) return 0; - rv = 2; + rv = NTCP2_INT_VERSION; } else { return 0; } - if (addr.getOption("s") == null || + // check version == "2" || version starts with "2," + // and static key, and iv + String v = addr.getOption("v"); + if (v == null || addr.getOption("i") == null || - !NTCP2_VERSION.equals(addr.getOption("v")) || - !NTCP2_PROTO_SHORT.equals(addr.getOption("n"))) { + addr.getOption("s") == null || + (!v.equals(NTCP2_VERSION) && !v.startsWith(NTCP2_VERSION_ALT))) { return (rv == 1) ? 1 : 0; } // todo validate s/i b64, or just catch it later? - return rv; + return NTCP2_INT_VERSION; } /** @@ -1314,7 +1392,6 @@ public class NTCPTransport extends TransportImpl { int cost; if (oldAddr == null) { cost = getDefaultCost(isIPv6); - addNTCP2Options(newProps); } else { cost = oldAddr.getCost(); newProps.putAll(oldAddr.getOptionsMap()); @@ -1436,6 +1513,7 @@ public class NTCPTransport extends TransportImpl { return; } } + addNTCP2Options(newProps); // stopListening stops the pumper, readers, and writers, so required even if // oldAddr == null since startListening starts them all again diff --git a/router/java/src/net/i2p/router/transport/ntcp/OutboundEstablishState.java b/router/java/src/net/i2p/router/transport/ntcp/OutboundEstablishState.java index e7af82c42..c40573803 100644 --- a/router/java/src/net/i2p/router/transport/ntcp/OutboundEstablishState.java +++ b/router/java/src/net/i2p/router/transport/ntcp/OutboundEstablishState.java @@ -3,6 +3,7 @@ package net.i2p.router.transport.ntcp; import java.io.IOException; import java.net.InetAddress; import java.nio.ByteBuffer; +import java.util.Arrays; import net.i2p.crypto.SigType; import net.i2p.data.DataFormatException; @@ -33,13 +34,13 @@ class OutboundEstablishState extends EstablishBase { } /** - * 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) + * Parse the contents of the buffer as part of the handshake. * * All data must be copied out of the buffer as Reader.processRead() * will return it to the pool. + * + * If there are additional data in the buffer after the handshake is complete, + * the EstablishState is responsible for passing it to NTCPConnection. */ @Override public synchronized void receive(ByteBuffer src) { @@ -65,124 +66,129 @@ class OutboundEstablishState extends EstablishBase { * * 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; - } + if (_state == State.OB_SENT_X && src.hasRemaining()) { + int toGet = Math.min(src.remaining(), XY_SIZE - _received); + src.get(_Y, _received, toGet); + _received += toGet; + if (_received < XY_SIZE) + return; + + 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); + _received = 0; + } catch (DHSessionKeyBuilder.InvalidPublicParameterException e) { + _context.statManager().addRateData("ntcp.invalidDH", 1); + fail("Invalid X", e); + return; + } catch (IllegalStateException ise) { + // setPeerPublicValue() + fail("reused keys?", ise); + 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; - } + if (_state == State.OB_GOT_Y && src.hasRemaining()) { + int toGet = Math.min(src.remaining(), HXY_TSB_PAD_SIZE - _received); + src.get(_e_hXY_tsB, _received, toGet); + _received += toGet; + if (_received < HXY_TSB_PAD_SIZE) + return; + + 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); - 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); + _context.statManager().addRateData("ntcp.invalidHXY", 1); + fail("Invalid H(X+Y) - mitm attack attempted?"); + return; } + SimpleByteCache.release(h); + changeState(State.OB_GOT_HXY); + _received = 0; + // 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 @@ -205,7 +211,7 @@ class OutboundEstablishState extends EstablishBase { _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; + off = _received; 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 + ")"); @@ -223,6 +229,7 @@ class OutboundEstablishState extends EstablishBase { // handle variable signature size SigType type = _con.getRemotePeer().getSigningPublicKey().getType(); int siglen = type.getSigLen(); + // we don't need to do this if no padding! byte bobSigData[] = new byte[siglen]; System.arraycopy(bobSig, 0, bobSigData, 0, siglen); Signature sig = new Signature(type, bobSigData); @@ -242,23 +249,36 @@ class OutboundEstablishState extends EstablishBase { } 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 + // skew in seconds + _con.finishOutboundEstablishment(_dh.getSessionKey(), _peerSkew, nextWriteIV, _e_bobSig); + changeState(State.VERIFIED); + if (src.hasRemaining()) { + // process "extra" data + // This is fairly common for outbound, where Bob may send his updated RI + if (_log.shouldInfo()) + _log.info("extra data " + src.remaining() + " on " + this); + _con.recvEncryptedI2NP(src); + } 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; } } } + + // check for remaining data + if ((_state == State.VERIFIED || _state == State.CORRUPT) && src.hasRemaining()) { + if (_log.shouldWarn()) + _log.warn("Received unexpected " + src.remaining() + " on " + this, new Exception()); + } } /** @@ -266,13 +286,12 @@ class OutboundEstablishState extends EstablishBase { * 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. + * + * @throws IllegalStateException */ + @Override public synchronized void prepareOutbound() { - boolean shouldSend; - synchronized(_stateLock) { - shouldSend = _state == State.OB_INIT; - } - if (shouldSend) { + if (_state == State.OB_INIT) { if (_log.shouldLog(Log.DEBUG)) _log.debug(prefix() + "send X"); byte toWrite[] = new byte[XY_SIZE + _hX_xor_bobIdentHash.length]; @@ -281,8 +300,7 @@ class OutboundEstablishState extends EstablishBase { changeState(State.OB_SENT_X); _transport.getPumper().wantsWrite(_con, toWrite); } else { - if (_log.shouldLog(Log.WARN)) - _log.warn(prefix() + "unexpected prepareOutbound()"); + throw new IllegalStateException(prefix() + "unexpected prepareOutbound()"); } } @@ -293,6 +311,7 @@ class OutboundEstablishState extends EstablishBase { @Override protected void releaseBufs(boolean isVerified) { super.releaseBufs(isVerified); + Arrays.fill(_Y, (byte) 0); SimpleByteCache.release(_Y); } } diff --git a/router/java/src/net/i2p/router/transport/ntcp/OutboundNTCP2State.java b/router/java/src/net/i2p/router/transport/ntcp/OutboundNTCP2State.java new file mode 100644 index 000000000..688824f4a --- /dev/null +++ b/router/java/src/net/i2p/router/transport/ntcp/OutboundNTCP2State.java @@ -0,0 +1,520 @@ +package net.i2p.router.transport.ntcp; + +import java.io.IOException; +import java.net.InetAddress; +import java.nio.ByteBuffer; +import java.security.GeneralSecurityException; +import java.util.Arrays; +import java.util.ArrayList; +import java.util.List; + +import com.southernstorm.noise.protocol.CipherState; +import com.southernstorm.noise.protocol.CipherStatePair; +import com.southernstorm.noise.protocol.HandshakeState; + +import net.i2p.data.Base64; +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.data.Hash; +import net.i2p.data.SessionKey; +import net.i2p.data.router.RouterIdentity; +import net.i2p.data.router.RouterInfo; +import net.i2p.router.RouterContext; +import net.i2p.router.transport.ntcp.NTCP2Payload.Block; +import net.i2p.util.Log; + +/** + * + * NTCP 2 only. We are Alice. + * + * Also contains static constants and methods used by InboundEstablishState for NTCP2. + * Does not extend EstablishBase. + * + * @since 0.9.35 + */ +class OutboundNTCP2State implements EstablishState { + + private final RouterContext _context; + private final Log _log; + private final NTCPTransport _transport; + private final NTCPConnection _con; + private final byte[] _tmp; + /** bytes received so far */ + private int _received; + private long _peerSkew; + + public static final int KEY_SIZE = 32; + public static final int MAC_SIZE = 16; + public static final int IV_SIZE = 16; + public static final int OPTIONS1_SIZE = 16; + /** 64 */ + public static final int MSG1_SIZE = KEY_SIZE + OPTIONS1_SIZE + MAC_SIZE; + /** one less than 288 byte NTCP1 msg 1 */ + public static final int TOTAL1_MAX = 287; + private static final int PADDING1_MAX = 64; + private static final int PADDING3_MAX = 64; + public static final int OPTIONS2_SIZE = 16; + public static final int MSG2_SIZE = KEY_SIZE + OPTIONS2_SIZE + MAC_SIZE; + /** 48 */ + public static final int MSG3P1_SIZE = KEY_SIZE + MAC_SIZE; + private static final int OPTIONS3_SIZE = 12; + /** in SECONDS */ + public static final long MAX_SKEW = 60; + // SipHash KDF things + private static final byte[] ZEROLEN = new byte[0]; + private static final byte[] ONE = new byte[] { 1 }; + public static final byte[] ZEROKEY = new byte[KEY_SIZE]; + /** for SipHash keygen */ + private static final byte[] ASK = new byte[] { (byte) 'a', (byte) 's', (byte) 'k', 1 }; + /** for SipHash keygen */ + private static final byte[] SIPHASH = DataHelper.getASCII("siphash"); + + private final Object _stateLock = new Object(); + private State _state; + + private final HandshakeState _handshakeState; + private final RouterInfo _aliceRI; + private final int _aliceRISize; + private int _padlen1; + private int _padlen2; + private final int _padlen3; + private final SessionKey _bobHash; + private final byte[] _bobIV; + + private enum State { + OB_INIT, + /** sent 1 */ + OB_SENT_X, + /** sent 1, got 2 but not padding */ + OB_GOT_HXY, + /** sent 1, got 2 incl. padding */ + OB_GOT_PADDING, + /** sent 1, got 2 incl. padding, sent 3 */ + VERIFIED, + CORRUPT + } + + public OutboundNTCP2State(RouterContext ctx, NTCPTransport transport, NTCPConnection con) { + _context = ctx; + _log = ctx.logManager().getLog(getClass()); + _transport = transport; + _con = con; + _state = State.OB_INIT; + _tmp = new byte[TOTAL1_MAX]; + try { + _handshakeState = new HandshakeState(HandshakeState.INITIATOR, _transport.getXDHFactory()); + } catch (GeneralSecurityException gse) { + throw new IllegalStateException("bad proto", gse); + } + // save because we must know length + _aliceRI = ctx.router().getRouterInfo(); + if (_aliceRI == null) + throw new IllegalStateException("no RI yet"); + _aliceRISize = _aliceRI.toByteArray().length; + _padlen3 = _context.random().nextInt(PADDING3_MAX); + + Hash h = _con.getRemotePeer().calculateHash(); + _bobHash = new SessionKey(h.getData()); + String s = _con.getRemoteAddress().getOption("i"); + if (s == null) + throw new IllegalArgumentException("no NTCP2 IV"); + _bobIV = Base64.decode(s); + if (_bobIV == null || _bobIV.length != IV_SIZE || + DataHelper.eq(_bobIV, 0, ZEROKEY, 0, IV_SIZE)) + throw new IllegalArgumentException("bad NTCP2 IV"); + } + + private void changeState(State state) { + synchronized (_stateLock) { + _state = state; + } + } + + /** + * Parse the contents of the buffer as part of the handshake. + * + * 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) { + if (_state == State.VERIFIED || _state == State.CORRUPT) + throw new IllegalStateException(this + "received unexpected data on " + _con); + if (_log.shouldLog(Log.DEBUG)) + _log.debug(this + "Receiving: " + src.remaining() + " Received: " + _received); + if (!src.hasRemaining()) + return; // nothing to receive + receiveOutbound(src); + } + + /** did the handshake fail for some reason? */ + public boolean isCorrupt() { + synchronized (_stateLock) { + return _state == State.CORRUPT; + } + } + + /** + * Don't synchronize this, deadlocks all over. + * + * @return is the handshake complete and valid? + */ + public boolean isComplete() { + synchronized (_stateLock) { + return _state == State.VERIFIED; + } + } + + /** + * Get the NTCP version + * @return 2 + */ + public int getVersion() { return 2; } + + /** + * We are Alice. + * We are establishing an outbound connection, so prepare ourselves by + * writing the first message in the handshake. + * Encrypt X and write X, the options block, and the padding. + * Save last half of encrypted X as IV for message 2 AES. + * + * @throws IllegalStateException + */ + public synchronized void prepareOutbound() { + if (!(_state == State.OB_INIT)) { + throw new IllegalStateException(this + "unexpected prepareOutbound()"); + } + if (_log.shouldLog(Log.DEBUG)) + _log.debug(this + "send X"); + byte options[] = new byte[OPTIONS1_SIZE]; + options[1] = NTCPTransport.NTCP2_INT_VERSION; + int padlen1 = _context.random().nextInt(PADDING1_MAX); + DataHelper.toLong(options, 2, 2, padlen1); + int msg3p2len = NTCP2Payload.BLOCK_HEADER_SIZE + 1 + _aliceRISize + + NTCP2Payload.BLOCK_HEADER_SIZE + OPTIONS3_SIZE + + NTCP2Payload.BLOCK_HEADER_SIZE + _padlen3 + + MAC_SIZE; + DataHelper.toLong(options, 4, 2, msg3p2len); + long now = (_context.clock().now() + 500) / 1000; + DataHelper.toLong(options, 8, 4, now); + + // set keys + String s = _con.getRemoteAddress().getOption("s"); + if (s == null) { + fail("no NTCP2 S"); + return; + } + byte[] bk = Base64.decode(s); + if (bk == null || bk.length != KEY_SIZE || + DataHelper.eq(bk, 0, ZEROKEY, 0, KEY_SIZE)) { + fail("bad NTCP2 S: " + s); + return; + } + _handshakeState.getRemotePublicKey().setPublicKey(bk, 0); + _handshakeState.getLocalKeyPair().setPublicKey(_transport.getNTCP2StaticPubkey(), 0); + _handshakeState.getLocalKeyPair().setPrivateKey(_transport.getNTCP2StaticPrivkey(), 0); + // output to _tmp + try { + _handshakeState.start(); + if (_log.shouldWarn()) + _log.warn("After start: " + _handshakeState.toString()); + _handshakeState.writeMessage(_tmp, 0, options, 0, OPTIONS1_SIZE); + } catch (GeneralSecurityException gse) { + // buffer length error + if (!_log.shouldWarn()) + _log.error("Bad msg 1 out", gse); + fail("Bad msg 1 out", gse); + return; + } catch (RuntimeException re) { + if (!_log.shouldWarn()) + _log.error("Bad msg 1 out", re); + fail("Bad msg 1 out", re); + return; + } + if (_log.shouldWarn()) + _log.warn("After msg 1: " + _handshakeState.toString()); + + // encrypt key before writing + _context.aes().encrypt(_tmp, 0, _tmp, 0, _bobHash, _bobIV, KEY_SIZE); + // overwrite _bobIV with last 16 encrypted bytes, CBC for message 2 + System.arraycopy(_tmp, KEY_SIZE - IV_SIZE, _bobIV, 0, IV_SIZE); + // add padding + if (padlen1 > 0) { + _context.random().nextBytes(_tmp, MSG1_SIZE, padlen1); + _handshakeState.mixHash(_tmp, MSG1_SIZE, padlen1); + if (_log.shouldWarn()) + _log.warn("After mixhash padding " + padlen1 + " msg 1: " + _handshakeState.toString()); + } + + changeState(State.OB_SENT_X); + // send it all at once + _transport.getPumper().wantsWrite(_con, _tmp, 0, MSG1_SIZE + padlen1); + } + + /** + * We are Alice, so receive these bytes as part of an outbound connection. + * This method receives message 2, and sends message 3. + * + * IV (CBC from msg 1) must be in _bobIV + * + * All data must be copied out of the buffer as Reader.processRead() + * will return it to the pool. + * + * Caller must synch + */ + private void receiveOutbound(ByteBuffer src) { + // Read in message #2 except for the padding + if (_state == State.OB_SENT_X && src.hasRemaining()) { + int toGet = Math.min(src.remaining(), MSG2_SIZE - _received); + src.get(_tmp, _received, toGet); + _received += toGet; + if (_received < MSG2_SIZE) + return; + _context.aes().decrypt(_tmp, 0, _tmp, 0, _bobHash, _bobIV, KEY_SIZE); + if (DataHelper.eqCT(_tmp, 0, ZEROKEY, 0, KEY_SIZE)) { + fail("Bad msg 2, Y = 0"); + return; + } + byte[] options2 = new byte[OPTIONS2_SIZE]; + try { + _handshakeState.readMessage(_tmp, 0, MSG2_SIZE, options2, 0); + } catch (GeneralSecurityException gse) { + fail("Bad msg 2, Y = " + Base64.encode(_tmp, 0, KEY_SIZE), gse); + return; + } catch (RuntimeException re) { + fail("Bad msg 2, Y = " + Base64.encode(_tmp, 0, KEY_SIZE), re); + return; + } + if (_log.shouldWarn()) + _log.warn("After msg 2: " + _handshakeState.toString()); + _padlen2 = (int) DataHelper.fromLong(options2, 2, 2); + long tsB = DataHelper.fromLong(options2, 8, 4); + long now = _context.clock().now(); + // rtt from sending #1 to receiving #2 + long rtt = now - _con.getCreated(); + _peerSkew = (now - (tsB * 1000) - (rtt / 2) + 500) / 1000; + if (_peerSkew > MAX_SKEW || _peerSkew < 0 - MAX_SKEW) { + fail("Clock Skew: " + _peerSkew, null, true); + return; + } + changeState(State.OB_GOT_HXY); + _received = 0; + } + + // Read in the padding for message #2 + if (_state == State.OB_GOT_HXY && src.hasRemaining()) { + int toGet = Math.min(src.remaining(), _padlen2 - _received); + src.get(_tmp, _received, toGet); + _received += toGet; + if (_received < _padlen2) + return; + if (_padlen2 > 0) { + _handshakeState.mixHash(_tmp, 0, _padlen2); + if (_log.shouldWarn()) + _log.warn("After mixhash padding " + _padlen2 + " msg 2: " + _handshakeState.toString()); + } + changeState(State.OB_GOT_PADDING); + if (src.hasRemaining()) { + // Outbound conn can never have extra data after msg 2 + fail("Extra data after msg 2: " + src.remaining()); + return; + } + prepareOutbound3(); + return; + } + + // check for remaining data + if ((_state == State.VERIFIED || _state == State.CORRUPT) && src.hasRemaining()) { + if (_log.shouldWarn()) + _log.warn("Received unexpected " + src.remaining() + " on " + this, new Exception()); + } + } + + /** + * We are Alice. + * Write the 3rd message. + * + * Caller must synch + */ + private void prepareOutbound3() { + // create msg 3 part 2 payload + // payload without MAC + int msg3p2len = NTCP2Payload.BLOCK_HEADER_SIZE + 1 + _aliceRISize + + NTCP2Payload.BLOCK_HEADER_SIZE + OPTIONS3_SIZE + + NTCP2Payload.BLOCK_HEADER_SIZE + _padlen3; + + // total for parts 1 and 2 with mac + byte[] tmp = new byte[MSG3P1_SIZE + msg3p2len + MAC_SIZE]; + List blocks = new ArrayList(3); + Block block = new NTCP2Payload.RIBlock(_aliceRI, false); + blocks.add(block); + byte[] opts = new byte[OPTIONS3_SIZE]; + opts[0] = NTCPConnection.PADDING_MIN_DEFAULT_INT; + opts[1] = NTCPConnection.PADDING_MAX_DEFAULT_INT; + opts[2] = NTCPConnection.PADDING_MIN_DEFAULT_INT; + opts[3] = NTCPConnection.PADDING_MAX_DEFAULT_INT; + DataHelper.toLong(opts, 4, 2, NTCPConnection.DUMMY_DEFAULT); + DataHelper.toLong(opts, 6, 2, NTCPConnection.DUMMY_DEFAULT); + DataHelper.toLong(opts, 8, 2, NTCPConnection.DELAY_DEFAULT); + DataHelper.toLong(opts, 10, 2, NTCPConnection.DELAY_DEFAULT); + block = new NTCP2Payload.OptionsBlock(opts); + blocks.add(block); + // all zeros is fine here + //block = new NTCP2Payload.PaddingBlock(_context, _padlen3); + block = new NTCP2Payload.PaddingBlock(_padlen3); + blocks.add(block); + // we put it at the offset so it doesn't get overwritten by HandshakeState + // when it copies the static key in there + int newoff = NTCP2Payload.writePayload(tmp, MSG3P1_SIZE, blocks); + int expect = MSG3P1_SIZE + msg3p2len; + if (newoff != expect) + throw new IllegalStateException("msg3 size mismatch expected " + expect + " got " + newoff); + try { + _handshakeState.writeMessage(tmp, 0, tmp, MSG3P1_SIZE, msg3p2len); + } catch (GeneralSecurityException gse) { + // buffer length error + if (!_log.shouldWarn()) + _log.error("Bad msg 3 out", gse); + fail("Bad msg 3 out", gse); + return; + } catch (RuntimeException re) { + if (!_log.shouldWarn()) + _log.error("Bad msg 3 out", re); + fail("Bad msg 3 out", re); + return; + } + // send it all at once + if (_log.shouldWarn()) + _log.warn("Sending msg3, part 1 is:\n" + net.i2p.util.HexDump.dump(tmp, 0, MSG3P1_SIZE)); + _transport.getPumper().wantsWrite(_con, tmp); + if (_log.shouldWarn()) + _log.warn("After msg 3: " + _handshakeState.toString()); + setDataPhase(); + } + + /** + * KDF for data phase, + * then calls con.finishOutboundEstablishment(), + * passing over the final keys and states to the con. + * + * Caller must synch + */ + private void setDataPhase() { + // Data phase ChaChaPoly keys + CipherStatePair ckp = _handshakeState.split(); + CipherState rcvr = ckp.getReceiver(); + CipherState sender = ckp.getSender(); + byte[] k_ab = sender.getKey(); + byte[] k_ba = rcvr.getKey(); + + // Data phase SipHash keys + byte[][] sipkeys = generateSipHashKeys(_context, _handshakeState); + byte[] sip_ab = sipkeys[0]; + byte[] sip_ba = sipkeys[1]; + + if (_log.shouldWarn()) { + _log.warn("Finished establishment for " + this + + "\nGenerated ChaCha key for A->B: " + Base64.encode(k_ab) + + "\nGenerated ChaCha key for B->A: " + Base64.encode(k_ba) + + "\nGenerated SipHash key for A->B: " + Base64.encode(sip_ab) + + "\nGenerated SipHash key for B->A: " + Base64.encode(sip_ba)); + } + // skew in seconds + _con.finishOutboundEstablishment(sender, rcvr, sip_ab, sip_ba, _peerSkew); + changeState(State.VERIFIED); + // no extra data possible + releaseBufs(true); + _handshakeState.destroy(); + Arrays.fill(sip_ab, (byte) 0); + Arrays.fill(sip_ba, (byte) 0); + } + + /** + * KDF for SipHash + * + * @return rv[0] is sip_ab, rv[1] is sip_ba + */ + static byte[][] generateSipHashKeys(RouterContext ctx, HandshakeState state) { + // TODO use noise HMAC or HKDF method instead? + // ask_master = HKDF(ck, zerolen, info="ask") + SessionKey tk = new SessionKey(state.getChainingKey()); + byte[] temp_key = doHMAC(ctx, tk, ZEROLEN); + tk = new SessionKey(temp_key); + byte[] ask_master = doHMAC(ctx, tk, ASK); + byte[] tmp = new byte[32 + SIPHASH.length]; + byte[] hash = state.getHash(); + System.arraycopy(hash, 0, tmp, 0, 32); + System.arraycopy(SIPHASH, 0, tmp, 32, SIPHASH.length); + tk = new SessionKey(ask_master); + temp_key = doHMAC(ctx, tk, tmp); + tk = new SessionKey(temp_key); + byte[] sip_master = doHMAC(ctx, tk, ONE); + tk = new SessionKey(sip_master); + temp_key = doHMAC(ctx, tk, ZEROLEN); + tk = new SessionKey(temp_key); + // Output 1 + byte[] sip_ab = doHMAC(ctx, tk, ONE); + // Output 2 + tmp = new byte[KEY_SIZE + 1]; + System.arraycopy(sip_ab, 0, tmp, 0, 32); + tmp[32] = 2; + byte[] sip_ba = doHMAC(ctx, tk, tmp); + Arrays.fill(temp_key, (byte) 0); + Arrays.fill(tmp, (byte) 0); + return new byte[][] { sip_ab, sip_ba }; + } + + private static byte[] doHMAC(RouterContext ctx, SessionKey key, byte data[]) { + byte[] rv = new byte[32]; + ctx.hmac256().calculate(key, data, 0, data.length, rv, 0); + return rv; + } + + /** + * Release resources on timeout. + * @param e may be null + * @since 0.9.16 + */ + public synchronized void close(String reason, Exception e) { + fail(reason, e); + } + + protected void fail(String reason) { fail(reason, null); } + + protected void fail(String reason, Exception e) { fail(reason, e, false); } + + protected synchronized void fail(String reason, Exception e, boolean bySkew) { + if (_state == State.CORRUPT || _state == State.VERIFIED) + return; + changeState(State.CORRUPT); + if (_log.shouldWarn()) { + _log.warn(this + "Failed to establish: " + reason, e); + _log.warn("State at failure: " + _handshakeState.toString()); + } + _handshakeState.destroy(); + if (!bySkew) + _context.statManager().addRateData("ntcp.receiveCorruptEstablishment", 1); + releaseBufs(false); + } + + /** + * Only call once. + * + * Caller must synch + */ + private void releaseBufs(boolean isVerified) { + Arrays.fill(_tmp, (byte) 0); + // TODO + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(64); + buf.append("OBES2 "); + buf.append(System.identityHashCode(this)); + buf.append(' ').append(_state); + if (_con.isEstablished()) buf.append(" established"); + buf.append(": "); + return buf.toString(); + } +} diff --git a/router/java/src/net/i2p/router/transport/ntcp/Reader.java b/router/java/src/net/i2p/router/transport/ntcp/Reader.java index 1e7746d21..98310042d 100644 --- a/router/java/src/net/i2p/router/transport/ntcp/Reader.java +++ b/router/java/src/net/i2p/router/transport/ntcp/Reader.java @@ -149,8 +149,6 @@ class Reader { if ((buf = con.getNextReadBuf()) == null) return; EstablishState est = con.getEstablishState(); - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Processing read buffer as an establishment for " + con + " with [" + est + "]"); if (est.isComplete()) { // why is it complete yet !con.isEstablished? @@ -163,20 +161,13 @@ class Reader { est.receive(buf); EventPumper.releaseBuf(buf); if (est.isCorrupt()) { - if (_log.shouldLog(Log.WARN)) - _log.warn("closing connection on establishment because: " +est.getError(), est.getException()); - if (!est.getFailedBySkew()) - _context.statManager().addRateData("ntcp.receiveCorruptEstablishment", 1); con.close(); return; } - if (est.isComplete() && est.getExtraBytes() != null) - con.recvEncryptedI2NP(ByteBuffer.wrap(est.getExtraBytes())); + // EstablishState is responsible for passing "extra" data to the con } while (!con.isClosed() && (buf = con.getNextReadBuf()) != null) { // decrypt the data and push it into an i2np message - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Processing read buffer as part of an i2np message (" + buf.remaining() + " bytes)"); con.recvEncryptedI2NP(buf); EventPumper.releaseBuf(buf); }