diff --git a/router/java/src/net/i2p/router/transport/udp/InboundEstablishState2.java b/router/java/src/net/i2p/router/transport/udp/InboundEstablishState2.java
index 05268468468adab3338cb31677aee7e7c754ed27..5f83fe5497971f225de3575ceee3a4f92addbcd6 100644
--- a/router/java/src/net/i2p/router/transport/udp/InboundEstablishState2.java
+++ b/router/java/src/net/i2p/router/transport/udp/InboundEstablishState2.java
@@ -14,6 +14,7 @@ import com.southernstorm.noise.protocol.CipherState;
 import com.southernstorm.noise.protocol.CipherStatePair;
 import com.southernstorm.noise.protocol.HandshakeState;
 
+import net.i2p.crypto.HKDF;
 import net.i2p.data.Base64;
 import net.i2p.data.DataFormatException;
 import net.i2p.data.DataHelper;
@@ -50,6 +51,7 @@ class InboundEstablishState2 extends InboundEstablishState implements SSU2Payloa
     private byte[] _rcvHeaderEncryptKey2;
     private byte[] _sessCrForReTX;
     private long _timeReceived;
+    private PeerState2 _pstate;
     
     // testing
     private static final boolean ENFORCE_TOKEN = false;
@@ -57,8 +59,6 @@ class InboundEstablishState2 extends InboundEstablishState implements SSU2Payloa
 
 
     /**
-     *  @param localPort Must be our external port, otherwise the signature of the
-     *                   SessionCreated message will be bad if the external port != the internal port.
      *  @param packet with all header encryption removed,
      *                either a SessionRequest OR a TokenRequest.
      */
@@ -351,6 +351,7 @@ class InboundEstablishState2 extends InboundEstablishState implements SSU2Payloa
             _currentState != InboundState.IB_STATE_TOKEN_REQUEST_RECEIVED)
             throw new IllegalStateException("Bad state for Retry Sent: " + _currentState);
         _currentState = InboundState.IB_STATE_RETRY_SENT;
+        _lastSend = _context.clock().now();
     }
 
     /**
@@ -391,7 +392,7 @@ class InboundEstablishState2 extends InboundEstablishState implements SSU2Payloa
         if (_log.shouldDebug())
             _log.debug("State after sess req: " + _handshakeState);
         _timeReceived = 0;
-        processPayload(data, off + LONG_HEADER_SIZE, len - (SHORT_HEADER_SIZE + KEY_LEN + MAC_LEN + MAC_LEN), true);
+        processPayload(data, off + LONG_HEADER_SIZE, len - (LONG_HEADER_SIZE + KEY_LEN + MAC_LEN), true);
         if (_timeReceived == 0)
             throw new GeneralSecurityException("No DateTime block in Session Request");
         long skew = _establishBegin - _timeReceived;
@@ -399,20 +400,17 @@ class InboundEstablishState2 extends InboundEstablishState implements SSU2Payloa
             throw new GeneralSecurityException("Skew exceeded in Session Request: " + skew);
         _sendHeaderEncryptKey2 = SSU2Util.hkdf(_context, _handshakeState.getChainingKey(), "SessCreateHeader");
         _currentState = InboundState.IB_STATE_REQUEST_RECEIVED;
-        
-        if (_createdSentCount == 1) {
-            _rtt = (int) ( _context.clock().now() - _lastSend );
-        }	
+        _rtt = (int) ( _context.clock().now() - _lastSend );
 
         packetReceived();
     }
 
     /**
+     * Receive the last message in the handshake, and create the PeerState.
      *
-     *
-     *
+     * @return the new PeerState2, may also be retrieved from getPeerState()
      */
-    public synchronized void receiveSessionConfirmed(UDPPacket packet) throws GeneralSecurityException {
+    public synchronized PeerState2 receiveSessionConfirmed(UDPPacket packet) throws GeneralSecurityException {
         if (_currentState != InboundState.IB_STATE_CREATED_SENT)
             throw new GeneralSecurityException("Bad state for Session Confirmed: " + _currentState);
         DatagramPacket pkt = packet.getPacket();
@@ -429,9 +427,9 @@ class InboundEstablishState2 extends InboundEstablishState implements SSU2Payloa
         if (_log.shouldDebug())
             _log.debug("State after mixHash 3: " + _handshakeState);
 
-        byte[] payload = new byte[len - 80]; // 16 hdr, 32 static key, 16 MAC, 16 MAC
+        // decrypt in-place
         try {
-            _handshakeState.readMessage(data, off + SHORT_HEADER_SIZE, len - SHORT_HEADER_SIZE, payload, 0);
+            _handshakeState.readMessage(data, off + SHORT_HEADER_SIZE, len - SHORT_HEADER_SIZE, data, off + SHORT_HEADER_SIZE);
         } catch (GeneralSecurityException gse) {
             if (_log.shouldDebug())
                 _log.debug("Session Confirmed error, State at failure: " + _handshakeState + '\n' + net.i2p.util.HexDump.dump(data, off, len), gse);
@@ -439,27 +437,53 @@ class InboundEstablishState2 extends InboundEstablishState implements SSU2Payloa
         }
         if (_log.shouldDebug())
             _log.debug("State after sess conf: " + _handshakeState);
-        processPayload(payload, 0, payload.length, false);
+        processPayload(data, off + SHORT_HEADER_SIZE, len - (SHORT_HEADER_SIZE + KEY_LEN + MAC_LEN + MAC_LEN), false);
         _sessCrForReTX = null;
 
-        // TODO split, calculate keys
-
-        
-        // TODO fix state
-        if ( (_currentState == InboundState.IB_STATE_UNKNOWN) || 
-             (_currentState == InboundState.IB_STATE_REQUEST_RECEIVED) ||
-             (_currentState == InboundState.IB_STATE_CREATED_SENT) ) {
-            if (confirmedFullyReceived())
-                _currentState = InboundState.IB_STATE_CONFIRMED_COMPLETELY;
-            else
-                _currentState = InboundState.IB_STATE_CONFIRMED_PARTIALLY;
-        }
-        
-        if (_createdSentCount == 1) {
+        if (_receivedConfirmedIdentity == null)
+            throw new GeneralSecurityException("No RI in Session Confirmed");
+
+        // split()
+        // The CipherStates are from d_ab/d_ba,
+        // not from k_ab/k_ba, so there's no use for
+        // HandshakeState.split()
+        byte[] ckd = _handshakeState.getChainingKey();
+        byte[] k_ab = new byte[32];
+        byte[] k_ba = new byte[32];
+        HKDF hkdf = new HKDF(_context);
+        hkdf.calculate(ckd, ZEROLEN, k_ab, k_ba, 0);
+        // generate keys
+        byte[] d_ab = new byte[32];
+        byte[] h_ab = new byte[32];
+        byte[] d_ba = new byte[32];
+        byte[] h_ba = new byte[32];
+        hkdf.calculate(k_ab, ZEROLEN, INFO_DATA, d_ab, h_ab, 0);
+        hkdf.calculate(k_ba, ZEROLEN, INFO_DATA, d_ba, h_ba, 0);
+        ChaChaPolyCipherState sender = new ChaChaPolyCipherState();
+        sender.initializeKey(d_ba, 0);
+        ChaChaPolyCipherState rcvr = new ChaChaPolyCipherState();
+        sender.initializeKey(d_ab, 0);
+        if (_log.shouldDebug())
+            _log.debug("Generated Chain key:              " + Base64.encode(ckd) +
+                       "\nGenerated split key for A->B:     " + Base64.encode(k_ab) +
+                       "\nGenerated split key for B->A:     " + Base64.encode(k_ba) +
+                       "\nGenerated encrypt key for A->B:   " + Base64.encode(d_ab) +
+                       "\nGenerated encrypt key for B->A:   " + Base64.encode(d_ba) +
+                       "\nIntro key for Alice:              " + Base64.encode(_sendHeaderEncryptKey1) +
+                       "\nIntro key for Bob:                " + Base64.encode(_rcvHeaderEncryptKey1) +
+                       "\nGenerated header key 2 for A->B:  " + Base64.encode(h_ab) +
+                       "\nGenerated header key 2 for B->A:  " + Base64.encode(h_ba));
+        _handshakeState.destroy();
+        if (_createdSentCount == 1)
             _rtt = (int) ( _context.clock().now() - _lastSend );
-        }	
-
+        _pstate = new PeerState2(_context, _transport, _aliceSocketAddress,
+                                 _receivedConfirmedIdentity.calculateHash(),
+                                 true, _rtt, sender, rcvr,
+                                 _sendConnID, _rcvConnID,
+                                 _sendHeaderEncryptKey1, h_ba, h_ab);
+        _currentState = InboundState.IB_STATE_CONFIRMED_COMPLETELY;
         packetReceived();
+        return _pstate;
     }
 
     /**
@@ -507,11 +531,11 @@ class InboundEstablishState2 extends InboundEstablishState implements SSU2Payloa
     }
 
     /**
-     * @return null we have not received the session confirmed
+     * @return null if we have not received the session confirmed
      */
     public synchronized PeerState2 getPeerState() {
-        // TODO
-        return null;
+        _currentState = InboundState.IB_STATE_COMPLETE;
+        return _pstate;
     }
     
     @Override
diff --git a/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState.java b/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState.java
index dc45e41501cdb9f8bb286db1ba08e666b5d45afc..b77fbff12915bcb7c29389938e0ba5b839c5780a 100644
--- a/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState.java
+++ b/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState.java
@@ -51,7 +51,7 @@ class OutboundEstablishState {
     // general status 
     protected final long _establishBegin;
     //private long _lastReceive;
-    private long _lastSend;
+    protected long _lastSend;
     private long _nextSend;
     protected RemoteHostId _remoteHostId;
     private final RemoteHostId _claimedAddress;
diff --git a/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState2.java b/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState2.java
index 28f54de2ce8e3ab482894889acb000f22e058cef..b1c761c588e0794e85ba8edfd237727c7e3894c4 100644
--- a/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState2.java
+++ b/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState2.java
@@ -12,6 +12,7 @@ import com.southernstorm.noise.protocol.CipherState;
 import com.southernstorm.noise.protocol.CipherStatePair;
 import com.southernstorm.noise.protocol.HandshakeState;
 
+import net.i2p.crypto.HKDF;
 import net.i2p.data.Base64;
 import net.i2p.data.DataFormatException;
 import net.i2p.data.DataHelper;
@@ -51,6 +52,7 @@ class OutboundEstablishState2 extends OutboundEstablishState implements SSU2Payl
     private byte[] _sessReqForReTX;
     private byte[] _sessConfForReTX;
     private long _timeReceived;
+    private PeerState2 _pstate;
 
     private static final boolean SET_TOKEN = false;
     private static final long MAX_SKEW = 2*60*1000L;
@@ -381,8 +383,10 @@ class OutboundEstablishState2 extends OutboundEstablishState implements SSU2Payl
     /**
      * note that we just sent the SessionConfirmed packets
      * and save them for retransmission
+     *
+     * @return the new PeerState2, may also be retrieved from getPeerState()
      */
-    public synchronized void confirmedPacketsSent(UDPPacket[] packets) {
+    public synchronized PeerState2 confirmedPacketsSent(UDPPacket[] packets) {
         if (_sessConfForReTX == null) {
             // store pkt for retx
             // only one supported right now
@@ -395,9 +399,49 @@ class OutboundEstablishState2 extends OutboundEstablishState implements SSU2Payl
             if (_rcvHeaderEncryptKey2 == null)
                 _rcvHeaderEncryptKey2 = SSU2Util.hkdf(_context, _handshakeState.getChainingKey(), "SessCreateHeader");
 
-            // TODO split(), create PeerState2
+            // split()
+            // The CipherStates are from d_ab/d_ba,
+            // not from k_ab/k_ba, so there's no use for
+            // HandshakeState.split()
+            byte[] ckd = _handshakeState.getChainingKey();
+            byte[] k_ab = new byte[32];
+            byte[] k_ba = new byte[32];
+            HKDF hkdf = new HKDF(_context);
+            hkdf.calculate(ckd, ZEROLEN, k_ab, k_ba, 0);
+            // generate keys
+            byte[] d_ab = new byte[32];
+            byte[] h_ab = new byte[32];
+            byte[] d_ba = new byte[32];
+            byte[] h_ba = new byte[32];
+            hkdf.calculate(k_ab, ZEROLEN, INFO_DATA, d_ab, h_ab, 0);
+            hkdf.calculate(k_ba, ZEROLEN, INFO_DATA, d_ba, h_ba, 0);
+            ChaChaPolyCipherState sender = new ChaChaPolyCipherState();
+            sender.initializeKey(d_ab, 0);
+            ChaChaPolyCipherState rcvr = new ChaChaPolyCipherState();
+            sender.initializeKey(d_ba, 0);
+            if (_log.shouldDebug())
+                _log.debug("Generated Chain key:              " + Base64.encode(ckd) +
+                           "\nGenerated split key for A->B:     " + Base64.encode(k_ab) +
+                           "\nGenerated split key for B->A:     " + Base64.encode(k_ba) +
+                           "\nGenerated encrypt key for A->B:   " + Base64.encode(d_ab) +
+                           "\nGenerated encrypt key for B->A:   " + Base64.encode(d_ba) +
+                           "\nIntro key for Alice:              " + Base64.encode(_sendHeaderEncryptKey1) +
+                           "\nIntro key for Bob:                " + Base64.encode(_rcvHeaderEncryptKey1) +
+                           "\nGenerated header key 2 for A->B:  " + Base64.encode(h_ab) +
+                           "\nGenerated header key 2 for B->A:  " + Base64.encode(h_ba));
+            _handshakeState.destroy();
+            if (_requestSentCount == 1)
+                _rtt = (int) ( _context.clock().now() - _lastSend );
+            _pstate = new PeerState2(_context, _transport, _bobSocketAddress,
+                                     _remotePeer.calculateHash(),
+                                     false, _rtt, sender, rcvr,
+                                     _sendConnID, _rcvConnID,
+                                     _sendHeaderEncryptKey1, h_ab, h_ba);
+            _currentState = OutboundState.OB_STATE_CONFIRMED_COMPLETELY;
+            _pstate.confirmedPacketsSent(_sessConfForReTX);
         }
         confirmedPacketsSent();
+        return _pstate;
     }
 
     /**
@@ -429,12 +473,11 @@ class OutboundEstablishState2 extends OutboundEstablishState implements SSU2Payl
     }
 
     /**
-     * @return null we have not sent the session confirmed
+     * @return null if we have not sent the session confirmed
      */
     public synchronized PeerState2 getPeerState() {
-        // TODO
-        // set confirmed pkt data
-        return null;
+        _currentState = OutboundState.OB_STATE_CONFIRMED_COMPLETELY;
+        return _pstate;
     }
 
     @Override
diff --git a/router/java/src/net/i2p/router/transport/udp/PacketBuilder2.java b/router/java/src/net/i2p/router/transport/udp/PacketBuilder2.java
index 2c6263116e482a744f898ec366db21bab68bb830..302d44fa67a8560dcf00d48d2bfe9f467796363d 100644
--- a/router/java/src/net/i2p/router/transport/udp/PacketBuilder2.java
+++ b/router/java/src/net/i2p/router/transport/udp/PacketBuilder2.java
@@ -286,27 +286,11 @@ class PacketBuilder2 {
         UDPPacket packet = buildLongPacketHeader(state.getSendConnID(), n, TOKEN_REQUEST_FLAG_BYTE,
                                                  state.getRcvConnID(), 0);
         DatagramPacket pkt = packet.getPacket();
-
-        byte toIP[] = state.getSentIP();
-        if (!_transport.isValid(toIP)) {
-            packet.release();
-            return null;
-        }
-        InetAddress to;
-        try {
-            to = InetAddress.getByAddress(toIP);
-        } catch (UnknownHostException uhe) {
-            if (_log.shouldLog(Log.ERROR))
-                _log.error("How did we think this was a valid IP?  " + state.getRemoteHostId());
-            packet.release();
-            return null;
-        }
-        
         pkt.setLength(LONG_HEADER_SIZE);
         byte[] introKey = state.getSendHeaderEncryptKey1();
         encryptTokenRequest(packet, introKey, n, introKey, introKey);
         state.requestSent();
-        setTo(packet, to, state.getSentPort());
+        pkt.setSocketAddress(state.getSentAddress());
         packet.setMessageType(TYPE_SREQ);
         packet.setPriority(PRIORITY_HIGH);
         state.tokenRequestSent(pkt);
@@ -324,27 +308,11 @@ class PacketBuilder2 {
         UDPPacket packet = buildLongPacketHeader(state.getSendConnID(), n, SESSION_REQUEST_FLAG_BYTE,
                                                  state.getRcvConnID(), state.getToken());
         DatagramPacket pkt = packet.getPacket();
-
-        byte toIP[] = state.getSentIP();
-        if (!_transport.isValid(toIP)) {
-            packet.release();
-            return null;
-        }
-        InetAddress to;
-        try {
-            to = InetAddress.getByAddress(toIP);
-        } catch (UnknownHostException uhe) {
-            if (_log.shouldLog(Log.ERROR))
-                _log.error("How did we think this was a valid IP?  " + state.getRemoteHostId());
-            packet.release();
-            return null;
-        }
-        
         pkt.setLength(LONG_HEADER_SIZE);
         byte[] introKey = state.getSendHeaderEncryptKey1();
         encryptSessionRequest(packet, state.getHandshakeState(), introKey, introKey, state.needIntroduction());
         state.requestSent();
-        setTo(packet, to, state.getSentPort());
+        pkt.setSocketAddress(state.getSentAddress());
         packet.setMessageType(TYPE_SREQ);
         packet.setPriority(PRIORITY_HIGH);
         state.requestSent(pkt);
@@ -475,23 +443,12 @@ class PacketBuilder2 {
     private UDPPacket buildSessionConfirmedPacket(OutboundEstablishState2 state, int numFragments, byte ourInfo[], int len, boolean gzip) {
         UDPPacket packet = buildShortPacketHeader(state.getSendConnID(), 0, SESSION_CONFIRMED_FLAG_BYTE);
         DatagramPacket pkt = packet.getPacket();
-
-        InetAddress to;
-        try {
-            to = InetAddress.getByAddress(state.getSentIP());
-        } catch (UnknownHostException uhe) {
-            if (_log.shouldLog(Log.ERROR))
-                _log.error("How did we think this was a valid IP?  " + state.getRemoteHostId());
-            packet.release();
-            return null;
-        }
-        
         pkt.setLength(SHORT_HEADER_SIZE);
         SSU2Payload.RIBlock block = new SSU2Payload.RIBlock(ourInfo,  0, len,
                                                             false, gzip, 0, numFragments);
         encryptSessionConfirmed(packet, state.getHandshakeState(), state.getMTU(),
                                 state.getSendHeaderEncryptKey1(), state.getSendHeaderEncryptKey2(), block, state.getNextToken());
-        setTo(packet, to, state.getSentPort());
+        pkt.setSocketAddress(state.getSentAddress());
         packet.setMessageType(TYPE_CONF);
         packet.setPriority(PRIORITY_HIGH);
         return packet;
diff --git a/router/java/src/net/i2p/router/transport/udp/SSU2Header.java b/router/java/src/net/i2p/router/transport/udp/SSU2Header.java
index f44441ffd8c66c5966192c8932e2a777f8421609..d89836983a3a9fb4b219cefb2a63769d7b954044 100644
--- a/router/java/src/net/i2p/router/transport/udp/SSU2Header.java
+++ b/router/java/src/net/i2p/router/transport/udp/SSU2Header.java
@@ -73,7 +73,7 @@ final class SSU2Header {
      *  Decrypt bytes 0-7 in header.
      *  Packet is unmodified.
      *
-     *  @param packet must be 8 bytes min
+     *  @param pkt must be 8 bytes min
      *  @return the destination connection ID
      *  @throws IndexOutOfBoundsException if too short
      */