From 0c08a05bce97b32a2a05275651c01fd7f531e281 Mon Sep 17 00:00:00 2001
From: zzz <zzz@i2pmail.org>
Date: Sun, 27 Feb 2022 12:03:28 -0500
Subject: [PATCH] SSU2: Hook in new classes to EstablishmentManager

Implement handshake retransmissions
Fix up calls to IES2/OES2
split() TODO
not hooked in to PacketHandler yet
WIP, untested
---
 .../transport/udp/EstablishmentManager.java   | 353 +++++++++++++++---
 .../transport/udp/InboundEstablishState2.java |  66 +++-
 .../udp/OutboundEstablishState2.java          | 142 +++++--
 .../router/transport/udp/PacketBuilder2.java  |  14 +-
 .../i2p/router/transport/udp/PeerState.java   |   6 +-
 .../i2p/router/transport/udp/PeerState2.java  |  32 ++
 .../router/transport/udp/UDPTransport.java    |   2 +-
 7 files changed, 532 insertions(+), 83 deletions(-)

diff --git a/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java b/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java
index 44956a05fe..8bea1842e7 100644
--- a/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java
+++ b/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java
@@ -2,6 +2,7 @@ package net.i2p.router.transport.udp;
 
 import java.net.InetAddress;
 import java.net.UnknownHostException;
+import java.security.GeneralSecurityException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Iterator;
@@ -402,10 +403,20 @@ class EstablishmentManager {
                     // don't ask if they are indirect
                     boolean requestIntroduction = allowExtendedOptions && !isIndirect &&
                                                   _transport.introducersMaybeRequired(TransportUtil.isIPv6(ra));
-                    state = new OutboundEstablishState(_context, maybeTo, to,
+                    int version = _transport.getSSUVersion(ra);
+                    if (version == 1) {
+                        state = new OutboundEstablishState(_context, maybeTo, to,
                                                        toIdentity, allowExtendedOptions,
                                                        requestIntroduction,
                                                        sessionKey, addr, _transport.getDHFactory());
+                    } else if (version == 2) {
+                        state = new OutboundEstablishState2(_context, _transport, maybeTo, to,
+                                                            toIdentity, requestIntroduction, sessionKey, ra, addr);
+                    } else {
+                        // shouldn't happen
+                        _transport.failed(msg, "OB to bad addr? " + ra);
+                        return;
+                    }
                     OutboundEstablishState oldState = _outboundStates.putIfAbsent(to, state);
                     boolean isNew = oldState == null;
                     if (isNew) {
@@ -471,6 +482,7 @@ class EstablishmentManager {
     /**
      * Got a SessionRequest (initiates an inbound establishment)
      *
+     * SSU 1 only.
      */
     void receiveSessionRequest(RemoteHostId from, UDPPacketReader reader) {
         if (!TransportUtil.isValidPort(from.getPort()) || !_transport.isValid(from.getIP())) {
@@ -544,9 +556,92 @@ class EstablishmentManager {
         notifyActivity();
     }
     
+    /**
+     * Got a SessionRequest OR a TokenRequest (initiates an inbound establishment)
+     *
+     * SSU 2 only.
+     * @since 0.9.54
+     */
+    void receiveSessionRequest(RemoteHostId from, UDPPacket packet) {
+        if (!TransportUtil.isValidPort(from.getPort()) || !_transport.isValid(from.getIP())) {
+            if (_log.shouldWarn())
+                _log.warn("Receive session request from invalid: " + from);
+            return;
+        }
+        boolean isNew = false;
+        InboundEstablishState state = _inboundStates.get(from);
+        if (state == null) {
+            // TODO this is insufficient to prevent DoSing, especially if
+            // IP spoofing is used. For further study.
+            if (!shouldAllowInboundEstablishment()) {
+                if (_log.shouldWarn())
+                    _log.warn("Dropping inbound establish, increase " + PROP_MAX_CONCURRENT_ESTABLISH);
+                _context.statManager().addRateData("udp.establishDropped", 1);
+                return; // drop the packet
+            }
+            if (_context.blocklist().isBlocklisted(from.getIP())) {
+                if (_log.shouldWarn())
+                    _log.warn("Receive session request from blocklisted IP: " + from);
+                _context.statManager().addRateData("udp.establishBadIP", 1);
+                return; // drop the packet
+            }
+            if (!_transport.allowConnection())
+                return; // drop the packet
+            try {
+                state = new InboundEstablishState2(_context, _transport, packet);
+            } catch (GeneralSecurityException gse) {
+                if (_log.shouldWarn())
+                    _log.warn("Corrupt Session/Token Request from: " + from, gse);
+                _context.statManager().addRateData("udp.establishDropped", 1);
+                return;
+            }
+
+          /**** TODO
+            if (_replayFilter.add(state.getReceivedX(), 0, 8)) {
+                if (_log.shouldLog(Log.WARN))
+                    _log.warn("Duplicate X in session request from: " + from);
+                _context.statManager().addRateData("udp.dupDHX", 1);
+                return; // drop the packet
+            }
+          ****/
+
+            InboundEstablishState oldState = _inboundStates.putIfAbsent(from, state);
+            isNew = oldState == null;
+            if (!isNew)
+                // whoops, somebody beat us to it, throw out the state we just created
+                state = oldState;
+        }
+
+        if (isNew) {
+          /**** TODO
+            // Don't offer to relay to privileged ports.
+            // Only offer for an IPv4 session.
+            // TODO if already we have their RI, only offer if they need it (no 'C' cap)
+            // if extended options, only if they asked for it
+            if (state.isIntroductionRequested() &&
+                state.getSentPort() >= 1024 &&
+                _transport.canIntroduce(state.getSentIP().length == 16)) {
+                // ensure > 0
+                long tag = 1 + _context.random().nextLong(MAX_TAG_VALUE);
+                state.setSentRelayTag(tag);
+            } else {
+                // we got an IB even though we were firewalled, hidden, not high cap, etc.
+            }
+          ****/
+            if (_log.shouldInfo())
+                _log.info("Received NEW session request " + state);
+        } else {
+            if (_log.shouldDebug())
+                _log.debug("Receive DUP session request from: " + state);
+        }
+        notifyActivity();
+    }
+    
     /** 
      * got a SessionConfirmed (should only happen as part of an inbound 
      * establishment) 
+     *
+     * SSU 1 only.
      */
     void receiveSessionConfirmed(RemoteHostId from, UDPPacketReader reader) {
         InboundEstablishState state = _inboundStates.get(from);
@@ -561,9 +656,42 @@ class EstablishmentManager {
         }
     }
     
+    /** 
+     * got a SessionConfirmed (should only happen as part of an inbound 
+     * establishment) 
+     *
+     * SSU 2 only.
+     * @since 0.9.54
+     */
+    void receiveSessionConfirmed(RemoteHostId from, UDPPacket packet) {
+        InboundEstablishState state = _inboundStates.get(from);
+        if (state != null) {
+            if (state.getVersion() != 2)
+                return;
+            InboundEstablishState2 state2 = (InboundEstablishState2) state;
+            try {
+                state2.receiveSessionConfirmed(packet);
+            } catch (GeneralSecurityException gse) {
+                if (_log.shouldWarn())
+                    _log.warn("Corrupt Session Confirmed from: " + from, gse);
+                state.fail();
+                return;
+            }
+            // we are done, go right to ps2
+            handleCompletelyEstablished(state2);
+            notifyActivity();
+            if (_log.shouldLog(Log.DEBUG))
+                _log.debug("Receive session confirmed from: " + state);
+        } else {
+            if (_log.shouldLog(Log.WARN))
+                _log.warn("Receive (DUP?) session confirmed from: " + from);
+        }
+    }
+    
     /**
      * Got a SessionCreated (in response to our outbound SessionRequest)
      *
+     * SSU 1 only.
      */
     void receiveSessionCreated(RemoteHostId from, UDPPacketReader reader) {
         OutboundEstablishState state = _outboundStates.get(from);
@@ -577,6 +705,64 @@ class EstablishmentManager {
                 _log.warn("Receive (DUP?) session created from: " + from);
         }
     }
+    
+    /**
+     * Got a SessionCreated (in response to our outbound SessionRequest)
+     *
+     * SSU 2 only.
+     * @since 0.9.54
+     */
+    void receiveSessionCreated(RemoteHostId from, UDPPacket packet) {
+        OutboundEstablishState state = _outboundStates.get(from);
+        if (state != null) {
+            if (state.getVersion() != 2)
+                return;
+            OutboundEstablishState2 state2 = (OutboundEstablishState2) state;
+            try {
+                state2.receiveSessionCreated(packet);
+            } catch (GeneralSecurityException gse) {
+                if (_log.shouldWarn())
+                    _log.warn("Corrupt Session Created from: " + from, gse);
+                state.fail();
+                return;
+            }
+            notifyActivity();
+            if (_log.shouldLog(Log.DEBUG))
+                _log.debug("Receive session created from: " + state);
+        } else {
+            if (_log.shouldLog(Log.WARN))
+                _log.warn("Receive (DUP?) session created from: " + from);
+        }
+    }
+    
+    /**
+     * Got a Retry (in response to our outbound SessionRequest or TokenRequest)
+     *
+     * SSU 2 only.
+     * @since 0.9.54
+     */
+    void receiveRetry(RemoteHostId from, UDPPacket packet) {
+        OutboundEstablishState state = _outboundStates.get(from);
+        if (state != null) {
+            if (state.getVersion() != 2)
+                return;
+            OutboundEstablishState2 state2 = (OutboundEstablishState2) state;
+            try {
+                state2.receiveSessionCreated(packet);
+            } catch (GeneralSecurityException gse) {
+                if (_log.shouldWarn())
+                    _log.warn("Corrupt Retry from: " + from, gse);
+                state.fail();
+                return;
+            }
+            notifyActivity();
+            if (_log.shouldLog(Log.DEBUG))
+                _log.debug("Receive retry from: " + state);
+        } else {
+            if (_log.shouldLog(Log.WARN))
+                _log.warn("Receive (DUP?) retry from: " + from);
+        }
+    }
 
     /**
      * Got a SessionDestroy on an established conn
@@ -719,25 +905,35 @@ class EstablishmentManager {
         if (state.isComplete()) return;
         
         RouterIdentity remote = state.getConfirmedIdentity();
-        PeerState peer = new PeerState(_context, _transport,
-                                       state.getSentIP(), state.getSentPort(), remote.calculateHash(), true, state.getRTT());
-        peer.setCurrentCipherKey(state.getCipherKey());
-        peer.setCurrentMACKey(state.getMACKey());
-        peer.setWeRelayToThemAs(state.getSentRelayTag());
-        // Lookup the peer's MTU from the netdb, since it isn't included in the protocol setup (yet)
-        // TODO if we don't have RI then we will get it shortly, but too late.
-        // Perhaps netdb should notify transport when it gets a new RI...
-        RouterInfo info = _context.netDb().lookupRouterInfoLocally(remote.calculateHash());
-        if (info != null) {
-            RouterAddress addr = _transport.getTargetAddress(info);
-            if (addr != null) {
-                String smtu = addr.getOption(UDPAddress.PROP_MTU);
-                if (smtu != null) {
-                    try { 
-                        boolean isIPv6 = state.getSentIP().length == 16;
-                        int mtu = MTU.rectify(isIPv6, Integer.parseInt(smtu));
-                        peer.setHisMTU(mtu);
-                    } catch (NumberFormatException nfe) {}
+        PeerState peer;
+        int version = state.getVersion();
+        if (version == 1) {
+            peer = new PeerState(_context, _transport,
+                                 state.getSentIP(), state.getSentPort(), remote.calculateHash(), true, state.getRTT());
+            peer.setCurrentCipherKey(state.getCipherKey());
+            peer.setCurrentMACKey(state.getMACKey());
+            peer.setWeRelayToThemAs(state.getSentRelayTag());
+        } else {
+            InboundEstablishState2 state2 = (InboundEstablishState2) state;
+            peer = state2.getPeerState();
+        }
+
+        if (version == 1) {
+            // Lookup the peer's MTU from the netdb, since it isn't included in the protocol setup (yet)
+            // TODO if we don't have RI then we will get it shortly, but too late.
+            // Perhaps netdb should notify transport when it gets a new RI...
+            RouterInfo info = _context.netDb().lookupRouterInfoLocally(remote.calculateHash());
+            if (info != null) {
+                RouterAddress addr = _transport.getTargetAddress(info);
+                if (addr != null) {
+                    String smtu = addr.getOption(UDPAddress.PROP_MTU);
+                    if (smtu != null) {
+                        try { 
+                            boolean isIPv6 = state.getSentIP().length == 16;
+                            int mtu = MTU.rectify(isIPv6, Integer.parseInt(smtu));
+                            peer.setHisMTU(mtu);
+                        } catch (NumberFormatException nfe) {}
+                    }
                 }
             }
         }
@@ -838,11 +1034,18 @@ class EstablishmentManager {
         if (claimed != null)
             _outboundByClaimedAddress.remove(claimed, state);
         _outboundByHash.remove(remote.calculateHash(), state);
-        PeerState peer = new PeerState(_context, _transport,
-                                       state.getSentIP(), state.getSentPort(), remote.calculateHash(), false, state.getRTT());
-        peer.setCurrentCipherKey(state.getCipherKey());
-        peer.setCurrentMACKey(state.getMACKey());
-        peer.setTheyRelayToUsAs(state.getReceivedRelayTag());
+        int version = state.getVersion();
+        PeerState peer;
+        if (version == 1) {
+            peer = new PeerState(_context, _transport,
+                                 state.getSentIP(), state.getSentPort(), remote.calculateHash(), false, state.getRTT());
+            peer.setCurrentCipherKey(state.getCipherKey());
+            peer.setCurrentMACKey(state.getMACKey());
+            peer.setTheyRelayToUsAs(state.getReceivedRelayTag());
+        } else {
+            OutboundEstablishState2 state2 = (OutboundEstablishState2) state;
+            peer = state2.getPeerState();
+        }
         int mtu = state.getRemoteAddress().getMTU();
         if (mtu > 0)
             peer.setHisMTU(mtu);
@@ -859,10 +1062,10 @@ class EstablishmentManager {
         
         _context.statManager().addRateData("udp.outboundEstablishTime", state.getLifetime());
         DatabaseStoreMessage dbsm = null;
-        if (!state.isFirstMessageOurDSM()) {
-            dbsm = getOurInfo();
-        } else if (_log.shouldLog(Log.INFO)) {
-            _log.info("Skipping publish: " + state);
+        if (version == 1) {
+            if (!state.isFirstMessageOurDSM()) {
+                dbsm = getOurInfo();
+            }
         }
         
         List<OutNetMessage> msgs = new ArrayList<OutNetMessage>(8);
@@ -912,18 +1115,30 @@ class EstablishmentManager {
         if (_log.shouldLog(Log.DEBUG))
             _log.debug("Send created to: " + state);
         
-        try {
-            state.generateSessionKey();
-        } catch (DHSessionKeyBuilder.InvalidPublicParameterException ippe) {
-            if (_log.shouldLog(Log.WARN))
-                _log.warn("Peer " + state + " sent us an invalid DH parameter", ippe);
-            _inboundStates.remove(state.getRemoteHostId());
-            state.fail();
-            return;
+        int version = state.getVersion();
+        UDPPacket pkt;
+        if (version == 1) {
+            try {
+                state.generateSessionKey();
+            } catch (DHSessionKeyBuilder.InvalidPublicParameterException ippe) {
+                if (_log.shouldLog(Log.WARN))
+                    _log.warn("Peer " + state + " sent us an invalid DH parameter", ippe);
+                _inboundStates.remove(state.getRemoteHostId());
+                state.fail();
+                return;
+            }
+            pkt = _builder.buildSessionCreatedPacket(state,
+                                                     _transport.getExternalPort(state.getSentIP().length == 16),
+                                                     _transport.getIntroKey());
+        } else {
+            // if already sent, get from the state to retx
+            InboundEstablishState2 state2 = (InboundEstablishState2) state;
+            InboundEstablishState.InboundState istate = state2.getState();
+            if (istate == IB_STATE_CREATED_SENT)
+                pkt = state2.getRetransmitSessionCreatedPacket();
+            else
+                pkt = _builder2.buildSessionCreatedPacket((InboundEstablishState2) state);
         }
-        UDPPacket pkt = _builder.buildSessionCreatedPacket(state,
-                                                           _transport.getExternalPort(state.getSentIP().length == 16),
-                                                           _transport.getIntroKey());
         if (pkt == null) {
             if (_log.shouldLog(Log.WARN))
                 _log.warn("Peer " + state + " sent us an invalid IP?");
@@ -932,7 +1147,9 @@ class EstablishmentManager {
             return;
         }
         _transport.send(pkt);
-        state.createdPacketSent();
+        if (version == 1)
+            state.createdPacketSent();
+        // else PacketBuilder2 told the state
     }
 
     /**
@@ -941,14 +1158,28 @@ class EstablishmentManager {
     private void sendRequest(OutboundEstablishState state) {
         if (_log.shouldLog(Log.DEBUG))
             _log.debug("Send SessionRequest to: " + state);
-        UDPPacket packet = _builder.buildSessionRequestPacket(state);
+        int version = state.getVersion();
+        UDPPacket packet;
+        if (version == 1) {
+            packet = _builder.buildSessionRequestPacket(state);
+        } else {
+            // if already sent, get from the state to retx
+            OutboundEstablishState2 state2 = (OutboundEstablishState2) state;
+            OutboundEstablishState.OutboundState ostate = state2.getState();
+            if (ostate == OB_STATE_REQUEST_SENT)
+                packet = state2.getRetransmitSessionRequestPacket();
+            else
+                packet = _builder2.buildSessionRequestPacket(state2);
+        }
         if (packet != null) {
             _transport.send(packet);
         } else {
             if (_log.shouldLog(Log.WARN))
                 _log.warn("Unable to build a session request packet for " + state);
         }
-        state.requestSent();
+        if (version == 1)
+            state.requestSent();
+        // else PacketBuilder2 told the state
     }
     
     /**
@@ -1105,19 +1336,41 @@ class EstablishmentManager {
         // gives us the opportunity to "detect" our external addr
         _transport.externalAddressReceived(state.getRemoteIdentity().calculateHash(), state.getReceivedIP(), state.getReceivedPort());
         
-        // signs if we havent signed yet
-        state.prepareSessionConfirmed();
-        
-        // BUG - handle null return
-        UDPPacket packets[] = _builder.buildSessionConfirmedPackets(state, _context.router().getRouterInfo().getIdentity());
+        int version = state.getVersion();
+        UDPPacket packets[];
+        if (version == 1) {
+            // signs if we havent signed yet
+            state.prepareSessionConfirmed();
+            packets = _builder.buildSessionConfirmedPackets(state, _context.router().getRouterInfo().getIdentity());
+        } else {
+            OutboundEstablishState2 state2 = (OutboundEstablishState2) state;
+            OutboundEstablishState.OutboundState ostate = state2.getState();
+            // shouldn't happen, we go straight to confirmed after sending
+            if (ostate == OB_STATE_CONFIRMED_COMPLETELY)
+                return;
+            packets = _builder2.buildSessionConfirmedPackets(state2, _context.router().getRouterInfo());
+        }
+        if (packets == null) {
+            state.fail();
+            return;
+        }
         
         if (_log.shouldLog(Log.DEBUG))
             _log.debug("Send confirm to: " + state);
         
-        for (int i = 0; i < packets.length; i++)
+        for (int i = 0; i < packets.length; i++) {
             _transport.send(packets[i]);
+        }
         
-        state.confirmedPacketsSent();
+        if (version == 1) {
+            state.confirmedPacketsSent();
+        } else {
+            // save for retx
+            OutboundEstablishState2 state2 = (OutboundEstablishState2) state;
+            state2.confirmedPacketsSent(packets);
+            // we are done, go right to ps2
+            handleCompletelyEstablished(state2);
+        }
     }
     
     /**
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 5317046b78..dca920afe6 100644
--- a/router/java/src/net/i2p/router/transport/udp/InboundEstablishState2.java
+++ b/router/java/src/net/i2p/router/transport/udp/InboundEstablishState2.java
@@ -2,8 +2,10 @@ package net.i2p.router.transport.udp;
 
 import java.io.IOException;
 import java.net.DatagramPacket;
+import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.net.SocketAddress;
+import java.net.UnknownHostException;
 import java.security.GeneralSecurityException;
 import java.util.List;
 
@@ -44,6 +46,7 @@ class InboundEstablishState2 extends InboundEstablishState implements SSU2Payloa
     private final byte[] _rcvHeaderEncryptKey1;
     private byte[] _sendHeaderEncryptKey2;
     private byte[] _rcvHeaderEncryptKey2;
+    private byte[] _sessCrForReTX;
     
     // testing
     private static final boolean ENFORCE_TOKEN = false;
@@ -52,7 +55,8 @@ 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
+     *  @param packet with all header encryption removed,
+     *                either a SessionRequest OR a TokenRequest.
      */
     public InboundEstablishState2(RouterContext ctx, UDPTransport transport,
                                   UDPPacket packet) throws GeneralSecurityException {
@@ -279,6 +283,13 @@ class InboundEstablishState2 extends InboundEstablishState implements SSU2Payloa
     // end payload callbacks
     /////////////////////////////////////////////////////////
     
+    // SSU 1 unsupported things
+
+    @Override
+    public void generateSessionKey() { throw new UnsupportedOperationException(); }
+
+    // SSU 2 things
+
     public long getSendConnID() { return _sendConnID; }
     public long getRcvConnID() { return _rcvConnID; }
     public long getToken() { return _token; }
@@ -406,6 +417,7 @@ class InboundEstablishState2 extends InboundEstablishState implements SSU2Payloa
         if (_log.shouldDebug())
             _log.debug("State after sess conf: " + _handshakeState);
         processPayload(payload, payload.length, false);
+        _sessCrForReTX = null;
 
         // TODO split, calculate keys
 
@@ -426,6 +438,58 @@ class InboundEstablishState2 extends InboundEstablishState implements SSU2Payloa
 
         packetReceived();
     }
+
+    /**
+     * note that we just sent the SessionCreated packet
+     * and save it for retransmission
+     */
+    public synchronized void createdPacketSent(DatagramPacket pkt) {
+        if (_sessCrForReTX == null) {
+            // store pkt for retx
+            byte data[] = pkt.getData();
+            int off = pkt.getOffset();
+            int len = pkt.getLength();
+            _sessCrForReTX = new byte[len];
+            System.arraycopy(data, off, _sessCrForReTX, 0, len);
+        }
+        createdPacketSent();
+    }
+
+    /**
+     * @return null if not sent or already got the session created
+     */
+    public synchronized UDPPacket getRetransmitSessionCreatedPacket() {
+        if (_sessCrForReTX == null)
+            return null;
+        UDPPacket packet = UDPPacket.acquire(_context, false);
+        DatagramPacket pkt = packet.getPacket();
+        byte data[] = pkt.getData();
+        int off = pkt.getOffset();
+        System.arraycopy(_sessCrForReTX, 0, data, off, _sessCrForReTX.length);
+        InetAddress to;
+        try {
+            to = InetAddress.getByAddress(_aliceIP);
+        } catch (UnknownHostException uhe) {
+            if (_log.shouldLog(Log.ERROR))
+                _log.error("How did we think this was a valid IP?  " + _remoteHostId);
+            packet.release();
+            return null;
+        }
+        pkt.setAddress(to);
+        pkt.setPort(_alicePort);
+        packet.setMessageType(PacketBuilder2.TYPE_CONF);
+        packet.setPriority(PacketBuilder2.PRIORITY_HIGH);
+        createdPacketSent();
+        return packet;
+    }
+
+    /**
+     * @return null we have not received the session confirmed
+     */
+    public synchronized PeerState2 getPeerState() {
+        // TODO
+        return null;
+    }
     
     @Override
     public String toString() {            
diff --git a/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState2.java b/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState2.java
index 0628cb63bc..ffe283a75e 100644
--- a/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState2.java
+++ b/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState2.java
@@ -16,6 +16,7 @@ import net.i2p.data.DataFormatException;
 import net.i2p.data.DataHelper;
 import net.i2p.data.SessionKey;
 import net.i2p.data.i2np.I2NPMessage;
+import net.i2p.data.router.RouterAddress;
 import net.i2p.data.router.RouterIdentity;
 import net.i2p.data.router.RouterInfo;
 import net.i2p.router.RouterContext;
@@ -36,6 +37,7 @@ class OutboundEstablishState2 extends OutboundEstablishState implements SSU2Payl
     private final UDPTransport _transport;
     private final long _sendConnID;
     private final long _rcvConnID;
+    private final RouterAddress _routerAddress;
     private long _token;
     private HandshakeState _handshakeState;
     private final byte[] _sendHeaderEncryptKey1;
@@ -44,6 +46,8 @@ class OutboundEstablishState2 extends OutboundEstablishState implements SSU2Payl
     private byte[] _rcvHeaderEncryptKey2;
     private final byte[] _rcvRetryHeaderEncryptKey2;
     private int _mtu;
+    private byte[] _sessReqForReTX;
+    private byte[] _sessConfForReTX;
     private static final boolean SET_TOKEN = false;
 
     /**
@@ -56,10 +60,9 @@ class OutboundEstablishState2 extends OutboundEstablishState implements SSU2Payl
      *  @param addr non-null
      */
     public OutboundEstablishState2(RouterContext ctx, UDPTransport transport, RemoteHostId claimedAddress,
-                                  RemoteHostId remoteHostId, int mtu,
-                                  RouterIdentity remotePeer, byte[] publicKey,
-                                  boolean needIntroduction,
-                                  SessionKey introKey, UDPAddress addr) {
+                                   RemoteHostId remoteHostId, RouterIdentity remotePeer,
+                                   boolean needIntroduction,
+                                   SessionKey introKey, RouterAddress ra, UDPAddress addr) throws IllegalArgumentException {
         super(ctx, claimedAddress, remoteHostId, remotePeer, needIntroduction, introKey, addr);
         _transport = transport;
         if (claimedAddress != null) {
@@ -68,10 +71,8 @@ class OutboundEstablishState2 extends OutboundEstablishState implements SSU2Payl
             } catch (UnknownHostException uhe) {
                 throw new IllegalArgumentException("bad IP", uhe);
             }
-            _mtu = mtu;
-        } else {
-            _mtu = PeerState.MIN_IPV6_MTU;
         }
+        _mtu = addr.getMTU();
         if (addr.getIntroducerCount() > 0) {
             if (_log.shouldLog(Log.DEBUG))
                 _log.debug("new outbound establish to " + remotePeer.calculateHash() + ", with address: " + addr);
@@ -80,20 +81,18 @@ class OutboundEstablishState2 extends OutboundEstablishState implements SSU2Payl
             _currentState = OutboundState.OB_STATE_UNKNOWN;
         }
 
-        // SSU2
-        createNewState(publicKey);
-
         _sendConnID = ctx.random().nextLong();
         // rcid == scid is not allowed
         long rcid;
         do {
             rcid = ctx.random().nextLong();
         } while (_sendConnID == rcid);
-        if (SET_TOKEN) {
-            do {
-                _token = ctx.random().nextLong();
-            } while (_token == 0);
-        }
+
+        _token = _transport.getEstablisher().getOutboundToken(_remotePeer.calculateHash());
+        _routerAddress = ra;
+        if (_token != 0)
+            createNewState(ra);
+
         _rcvConnID = rcid;
         byte[] ik = introKey.getData();
         _sendHeaderEncryptKey1 = ik;
@@ -103,7 +102,15 @@ class OutboundEstablishState2 extends OutboundEstablishState implements SSU2Payl
         _rcvRetryHeaderEncryptKey2 = ik;
     }
 
-    private void createNewState(byte[] publicKey) {
+    private void createNewState(RouterAddress addr) {
+        String ss = addr.getOption("s");
+        if (ss == null)
+            throw new IllegalArgumentException("no SSU2 S");
+        byte[] publicKey = Base64.decode(ss);
+        if (publicKey == null)
+            throw new IllegalArgumentException("bad SSU2 S");
+        if (publicKey.length != 32)
+            throw new IllegalArgumentException("bad SSU2 S len");
         try {
             _handshakeState = new HandshakeState(HandshakeState.PATTERN_ID_XK_SSU2, HandshakeState.INITIATOR, _transport.getXDHFactory());
         } catch (GeneralSecurityException gse) {
@@ -119,8 +126,9 @@ class OutboundEstablishState2 extends OutboundEstablishState implements SSU2Payl
         HandshakeState old = _handshakeState;
         byte[] pub = new byte[32];
         old.getRemotePublicKey().getPublicKey(pub, 0);
-        createNewState(pub);
-        old.destroy();
+        createNewState(_routerAddress);
+        if (old != null)
+            old.destroy();
         //_rcvHeaderEncryptKey2 will be set after the Session Request message is created
         _rcvHeaderEncryptKey2 = null;
     }
@@ -201,6 +209,11 @@ class OutboundEstablishState2 extends OutboundEstablishState implements SSU2Payl
     // end payload callbacks
     /////////////////////////////////////////////////////////
     
+    // SSU 1 unsupported things
+
+
+    // SSU 2 things
+
     @Override
     public int getVersion() { return 2; }
     public long getSendConnID() { return _sendConnID; }
@@ -226,6 +239,12 @@ class OutboundEstablishState2 extends OutboundEstablishState implements SSU2Payl
     /** what is the largest packet we can send to the peer? */
     public int getMTU() { return _mtu; }
 
+    public synchronized void receiveRetry(UDPPacket packet) throws GeneralSecurityException {
+        ////// TODO state check
+        createNewState(_routerAddress);
+        ////// TODO state change
+    }
+
     public synchronized void receiveSessionCreated(UDPPacket packet) throws GeneralSecurityException {
         ////// todo fix state check
         if (_currentState == OutboundState.OB_STATE_VALIDATION_FAILED) {
@@ -256,6 +275,7 @@ class OutboundEstablishState2 extends OutboundEstablishState implements SSU2Payl
         if (_log.shouldDebug())
             _log.debug("State after sess cr: " + _handshakeState);
         processPayload(payload, payload.length, true);
+        _sessReqForReTX = null;
         _sendHeaderEncryptKey2 = SSU2Util.hkdf(_context, _handshakeState.getChainingKey(), "SessionConfirmed");
 
         if (_currentState == OutboundState.OB_STATE_UNKNOWN ||
@@ -270,15 +290,93 @@ class OutboundEstablishState2 extends OutboundEstablishState implements SSU2Payl
         packetReceived();
     }
 
+    /**
+     * note that we just sent the SessionConfirmed packets
+     * and save them for retransmission
+     */
+    public synchronized void tokenRequestSent(DatagramPacket packet) {
+        if (_currentState == OutboundState.OB_STATE_UNKNOWN)
+            _currentState = OutboundState.OB_STATE_TOKEN_REQUEST_SENT;
+        else if (_currentState == OutboundState.OB_STATE_RETRY_RECEIVED)
+            _currentState = OutboundState.OB_STATE_REQUEST_SENT_NEW_TOKEN;
+        // don't bother saving for retx, just make a new one every time
+    }
+
     /**
      * note that we just sent the SessionRequest packet
+     * and save it for retransmission
      */
-    @Override
-    public synchronized void requestSent() {
-        /// TODO store pkt for retx
+    public synchronized void requestSent(DatagramPacket pkt) {
+        if (_sessReqForReTX == null) {
+            // store pkt for retx
+            byte data[] = pkt.getData();
+            int off = pkt.getOffset();
+            int len = pkt.getLength();
+            _sessReqForReTX = new byte[len];
+            System.arraycopy(data, off, _sessReqForReTX, 0, len);
+        }
         if (_rcvHeaderEncryptKey2 == null)
             _rcvHeaderEncryptKey2 = SSU2Util.hkdf(_context, _handshakeState.getChainingKey(), "SessCreateHeader");
-        super.requestSent();
+        requestSent();
+    }
+
+    /**
+     * note that we just sent the SessionConfirmed packets
+     * and save them for retransmission
+     */
+    public synchronized void confirmedPacketsSent(UDPPacket[] packets) {
+        if (_sessConfForReTX == null) {
+            // store pkt for retx
+            // only one supported right now
+            DatagramPacket pkt = packets[0].getPacket();
+            byte data[] = pkt.getData();
+            int off = pkt.getOffset();
+            int len = pkt.getLength();
+            _sessConfForReTX = new byte[len];
+            System.arraycopy(data, off, _sessConfForReTX, 0, len);
+            if (_rcvHeaderEncryptKey2 == null)
+                _rcvHeaderEncryptKey2 = SSU2Util.hkdf(_context, _handshakeState.getChainingKey(), "SessCreateHeader");
+
+            // TODO split(), create PeerState2
+        }
+        confirmedPacketsSent();
+    }
+
+    /**
+     * @return null if not sent or already got the session created
+     */
+    public synchronized UDPPacket getRetransmitSessionRequestPacket() {
+        if (_sessReqForReTX == null)
+            return null;
+        UDPPacket packet = UDPPacket.acquire(_context, false);
+        DatagramPacket pkt = packet.getPacket();
+        byte data[] = pkt.getData();
+        int off = pkt.getOffset();
+        System.arraycopy(_sessReqForReTX, 0, data, off, _sessReqForReTX.length);
+        InetAddress to;
+        try {
+            to = InetAddress.getByAddress(_bobIP);
+        } catch (UnknownHostException uhe) {
+            if (_log.shouldLog(Log.ERROR))
+                _log.error("How did we think this was a valid IP?  " + _remoteHostId);
+            packet.release();
+            return null;
+        }
+        pkt.setAddress(to);
+        pkt.setPort(_bobPort);
+        packet.setMessageType(PacketBuilder2.TYPE_SREQ);
+        packet.setPriority(PacketBuilder2.PRIORITY_HIGH);
+        requestSent();
+        return packet;
+    }
+
+    /**
+     * @return null we have not sent the session confirmed
+     */
+    public synchronized PeerState2 getPeerState() {
+        // TODO
+        // set confirmed pkt data
+        return null;
     }
 
     @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 5b7170d702..19b08c97df 100644
--- a/router/java/src/net/i2p/router/transport/udp/PacketBuilder2.java
+++ b/router/java/src/net/i2p/router/transport/udp/PacketBuilder2.java
@@ -292,7 +292,7 @@ class PacketBuilder2 {
             packet.release();
             return null;
         }
-        InetAddress to = null;
+        InetAddress to;
         try {
             to = InetAddress.getByAddress(toIP);
         } catch (UnknownHostException uhe) {
@@ -309,6 +309,7 @@ class PacketBuilder2 {
         setTo(packet, to, state.getSentPort());
         packet.setMessageType(TYPE_SREQ);
         packet.setPriority(PRIORITY_HIGH);
+        state.tokenRequestSent(pkt);
         return packet;
     }
     
@@ -328,7 +329,7 @@ class PacketBuilder2 {
             packet.release();
             return null;
         }
-        InetAddress to = null;
+        InetAddress to;
         try {
             to = InetAddress.getByAddress(toIP);
         } catch (UnknownHostException uhe) {
@@ -345,6 +346,7 @@ class PacketBuilder2 {
         setTo(packet, to, state.getSentPort());
         packet.setMessageType(TYPE_SREQ);
         packet.setPriority(PRIORITY_HIGH);
+        state.requestSent(pkt);
         return packet;
     }
     
@@ -365,10 +367,10 @@ class PacketBuilder2 {
         encryptSessionCreated(packet, state.getHandshakeState(), state.getSendHeaderEncryptKey1(),
                               state.getSendHeaderEncryptKey2(), state.getSentRelayTag(), state.getNextToken(),
                               sentIP, port);
-        state.createdPacketSent();
         pkt.setSocketAddress(state.getSentAddress());
         packet.setMessageType(TYPE_CREAT);
         packet.setPriority(PRIORITY_HIGH);
+        state.createdPacketSent(pkt);
         return packet;
     }
     
@@ -390,10 +392,10 @@ class PacketBuilder2 {
         encryptRetry(packet, state.getSendHeaderEncryptKey1(), n, state.getSendHeaderEncryptKey1(),
                      state.getSendHeaderEncryptKey2(),
                      sentIP, port);
-        state.retryPacketSent();
         pkt.setSocketAddress(state.getSentAddress());
         packet.setMessageType(TYPE_CREAT);
         packet.setPriority(PRIORITY_HIGH);
+        state.retryPacketSent();
         return packet;
     }
     
@@ -460,7 +462,7 @@ class PacketBuilder2 {
             // TODO numFragments > 1 requires shift to data phase
             throw new IllegalArgumentException("TODO");
         }
-        state.confirmedPacketsSent();
+        state.confirmedPacketsSent(packets);
         return packets;
     }
 
@@ -473,7 +475,7 @@ class PacketBuilder2 {
         UDPPacket packet = buildShortPacketHeader(state.getSendConnID(), 1, SESSION_CONFIRMED_FLAG_BYTE);
         DatagramPacket pkt = packet.getPacket();
 
-        InetAddress to = null;
+        InetAddress to;
         try {
             to = InetAddress.getByAddress(state.getSentIP());
         } catch (UnknownHostException uhe) {
diff --git a/router/java/src/net/i2p/router/transport/udp/PeerState.java b/router/java/src/net/i2p/router/transport/udp/PeerState.java
index 23ba705327..58f933663a 100644
--- a/router/java/src/net/i2p/router/transport/udp/PeerState.java
+++ b/router/java/src/net/i2p/router/transport/udp/PeerState.java
@@ -139,11 +139,11 @@ public class PeerState {
     /** what IP is the peer sending and receiving packets on? */
     protected final byte[] _remoteIP;
     /** cached IP address */
-    private volatile InetAddress _remoteIPAddress;
+    protected volatile InetAddress _remoteIPAddress;
     /** what port is the peer sending and receiving packets on? */
-    private volatile int _remotePort;
+    protected volatile int _remotePort;
     /** cached RemoteHostId, used to find the peerState by remote info */
-    private volatile RemoteHostId _remoteHostId;
+    protected volatile RemoteHostId _remoteHostId;
 
     /** if we need to contact them, do we need to talk to an introducer? */
     //private boolean _remoteRequiresIntroduction;
diff --git a/router/java/src/net/i2p/router/transport/udp/PeerState2.java b/router/java/src/net/i2p/router/transport/udp/PeerState2.java
index d8e7e14673..9db55bb9a5 100644
--- a/router/java/src/net/i2p/router/transport/udp/PeerState2.java
+++ b/router/java/src/net/i2p/router/transport/udp/PeerState2.java
@@ -1,7 +1,9 @@
 package net.i2p.router.transport.udp;
 
 import java.net.DatagramPacket;
+import java.net.InetAddress;
 import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
 import java.security.GeneralSecurityException;
 import java.util.List;
 import java.util.concurrent.atomic.AtomicInteger;
@@ -44,6 +46,7 @@ public class PeerState2 extends PeerState implements SSU2Payload.PayloadCallback
     private final byte[] _rcvHeaderEncryptKey2;
     private final SSU2Bitfield _receivedMessages;
     private final SSU2Bitfield _ackedMessages;
+    private byte[] _sessConfForReTX;
 
     public static final int MIN_MTU = 1280;
 
@@ -333,4 +336,33 @@ public class PeerState2 extends PeerState implements SSU2Payload.PayloadCallback
     void fragmentsSent(long pktNum, List<PacketBuilder.Fragment> fragments) {
 
     }
+
+    /**
+     * note that we just sent the SessionConfirmed packets
+     * and save them for retransmission
+     */
+    public synchronized void confirmedPacketsSent(byte[] data) {
+        if (_sessConfForReTX == null)
+            _sessConfForReTX = data;
+    }
+
+    /**
+     * @return null if not sent or already got the ack
+     */
+    public synchronized UDPPacket[] getRetransmitSessionConfirmedPackets() {
+        if (_sessConfForReTX == null)
+            return null;
+        UDPPacket packet = UDPPacket.acquire(_context, false);
+        UDPPacket[] rv = new UDPPacket[1];
+        rv[0] = packet;
+        DatagramPacket pkt = packet.getPacket();
+        byte data[] = pkt.getData();
+        int off = pkt.getOffset();
+        System.arraycopy(_sessConfForReTX, 0, data, off, _sessConfForReTX.length);
+        pkt.setAddress(_remoteIPAddress);
+        pkt.setPort(_remotePort);
+        packet.setMessageType(PacketBuilder2.TYPE_CONF);
+        packet.setPriority(PacketBuilder2.PRIORITY_HIGH);
+        return rv;
+    }
 }
diff --git a/router/java/src/net/i2p/router/transport/udp/UDPTransport.java b/router/java/src/net/i2p/router/transport/udp/UDPTransport.java
index 73152c85cb..771c3f85d5 100644
--- a/router/java/src/net/i2p/router/transport/udp/UDPTransport.java
+++ b/router/java/src/net/i2p/router/transport/udp/UDPTransport.java
@@ -906,7 +906,7 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
      * @return the valid version 1 or 2, or 0 if unusable
      * @since 0.9.54
      */
-    private int getSSUVersion(RouterAddress addr) {
+    int getSSUVersion(RouterAddress addr) {
         int rv;
         String style = addr.getTransportStyle();
         if (style.equals(STYLE)) {
-- 
GitLab