From 1ab3e9b310f97bf5995d83d3ad06b65357836252 Mon Sep 17 00:00:00 2001
From: zzz <zzz@i2pmail.org>
Date: Wed, 20 Oct 2021 09:37:32 -0400
Subject: [PATCH] SSU: Send Bob-to-Alice Peer Test message in-session

Matches what i2pd does.
More checks to require in-session for
Alice/Bob and Bob/Charlie Peer Test messages.
---
 .../router/transport/udp/PacketBuilder.java   | 19 +++-
 .../router/transport/udp/PacketHandler.java   |  2 +-
 .../router/transport/udp/PeerTestManager.java | 91 +++++++++++++------
 .../router/transport/udp/PeerTestState.java   | 36 +++++++-
 4 files changed, 116 insertions(+), 32 deletions(-)

diff --git a/router/java/src/net/i2p/router/transport/udp/PacketBuilder.java b/router/java/src/net/i2p/router/transport/udp/PacketBuilder.java
index 9a097809b6..b9d39334ad 100644
--- a/router/java/src/net/i2p/router/transport/udp/PacketBuilder.java
+++ b/router/java/src/net/i2p/router/transport/udp/PacketBuilder.java
@@ -1089,11 +1089,26 @@ class PacketBuilder {
 
     /**
      * Build a packet as if we are either Bob or Charlie and we are helping test Alice.
-     * 
+     * Not for use as Bob, as of 0.9.52; use in-session cipher/mac keys instead.
+     *
      * @return ready to send packet, or null if there was a problem
      */
     public UDPPacket buildPeerTestToAlice(InetAddress aliceIP, int alicePort,
                                           SessionKey aliceIntroKey, SessionKey charlieIntroKey, long nonce) {
+        return buildPeerTestToAlice(aliceIP, alicePort, aliceIntroKey, aliceIntroKey, charlieIntroKey, nonce);
+    }
+
+    /**
+     * Build a packet as if we are either Bob or Charlie and we are helping test Alice.
+     * 
+     * @param aliceCipherKey the intro key if we are Charlie
+     * @param aliceMACKey the intro key if we are Charlie
+     * @return ready to send packet, or null if there was a problem
+     * @since 0.9.52
+     */
+    public UDPPacket buildPeerTestToAlice(InetAddress aliceIP, int alicePort,
+                                          SessionKey aliceCipherKey, SessionKey aliceMACKey,
+                                          SessionKey charlieIntroKey, long nonce) {
         UDPPacket packet = buildPacketHeader(PEER_TEST_FLAG_BYTE);
         DatagramPacket pkt = packet.getPacket();
         byte data[] = pkt.getData();
@@ -1117,7 +1132,7 @@ class PacketBuilder {
         off = pad1(data, off);
         off = pad2(data, off);
         pkt.setLength(off);
-        authenticate(packet, aliceIntroKey, aliceIntroKey);
+        authenticate(packet, aliceCipherKey, aliceMACKey);
         setTo(packet, aliceIP, alicePort);
         packet.setMessageType(TYPE_TTA);
         return packet;
diff --git a/router/java/src/net/i2p/router/transport/udp/PacketHandler.java b/router/java/src/net/i2p/router/transport/udp/PacketHandler.java
index ea0908dcda..9ab63895d0 100644
--- a/router/java/src/net/i2p/router/transport/udp/PacketHandler.java
+++ b/router/java/src/net/i2p/router/transport/udp/PacketHandler.java
@@ -792,7 +792,7 @@ class PacketHandler {
                     }
                     if (_log.shouldLog(Log.DEBUG))
                         _log.debug("Received test packet: " + reader + " from " + from);
-                    _testManager.receiveTest(from, reader);
+                    _testManager.receiveTest(from, state, auth == AuthType.SESSION, reader);
                     //_context.statManager().addRateData("udp.receivePacketSize.test", packet.getPacket().getLength(), packet.getLifetime());
                     break;
                 case UDPPacket.PAYLOAD_TYPE_RELAY_REQUEST:
diff --git a/router/java/src/net/i2p/router/transport/udp/PeerTestManager.java b/router/java/src/net/i2p/router/transport/udp/PeerTestManager.java
index 5ee1008906..1897f5c47f 100644
--- a/router/java/src/net/i2p/router/transport/udp/PeerTestManager.java
+++ b/router/java/src/net/i2p/router/transport/udp/PeerTestManager.java
@@ -193,8 +193,7 @@ class PeerTestManager {
                                                _context.clock().now());
         test.setBobIP(bobIP);
         test.setBobPort(bobPort);
-        test.setBobCipherKey(bobCipherKey);
-        test.setBobMACKey(bobMACKey);
+        test.setBobKeys(bobCipherKey, bobMACKey);
         _currentTest = test;
         _currentTestComplete = false;
         
@@ -276,7 +275,7 @@ class PeerTestManager {
                 _log.debug("Sending test to Bob: " + test);
             test.setLastSendTime(_context.clock().now());
             _transport.send(_packetBuilder.buildPeerTestFromAlice(test.getBobIP(), test.getBobPort(),
-                                                                  test.getBobCipherKey(), test.getBobMACKey(), //_bobIntroKey, 
+                                                                  test.getBobCipherKey(), test.getBobMACKey(),
                                                                   test.getNonce(), _transport.getIntroKey()));
         } else {
             _currentTest = null;
@@ -312,8 +311,12 @@ class PeerTestManager {
     /**
      * Receive a PeerTest message which contains the correct nonce for our current 
      * test. We are Alice.
+     *
+     * @param fromPeer non-null if an associated session was found, otherwise null
+     * @param inSession true if authenticated in-session
      */
-    private synchronized void receiveTestReply(RemoteHostId from, UDPPacketReader.PeerTestReader testInfo) {
+    private synchronized void receiveTestReply(RemoteHostId from, PeerState fromPeer, boolean inSession,
+                                               UDPPacketReader.PeerTestReader testInfo) {
         _context.statManager().addRateData("udp.receiveTestReply", 1);
         PeerTestState test = _currentTest;
         if (expired())
@@ -323,6 +326,17 @@ class PeerTestManager {
         if ( (DataHelper.eq(from.getIP(), test.getBobIP().getAddress())) && (from.getPort() == test.getBobPort()) ) {
             // The reply is from Bob
 
+            if (inSession) {
+                // i2pd has sent the Bob->Alice message in-session for a long time
+                // Java I2P switched to in-session in 0.9.52
+                //if (_log.shouldDebug())
+                //    _log.debug("Bob replied to us (Alice) in-session " + fromPeer);
+            } else {
+                // TODO check Bob version, drop if >= 0.9.52
+                if (_log.shouldDebug())
+                    _log.debug("Bob replied to us (Alice) with intro key " + from + ' ' + fromPeer);
+            }
+
             int ipSize = testInfo.readIPSize();
             boolean expectV6 = test.isIPv6();
             if ((!expectV6 && ipSize != 4) ||
@@ -512,8 +526,11 @@ class PeerTestManager {
      * adjusting our test state.
      *
      * We could be Alice, Bob, or Charlie.
+     *
+     * @param fromPeer non-null if an associated session was found, otherwise null
+     * @param inSession true if authenticated in-session
      */
-    public void receiveTest(RemoteHostId from, UDPPacketReader reader) {
+    public void receiveTest(RemoteHostId from, PeerState fromPeer, boolean inSession, UDPPacketReader reader) {
         _context.statManager().addRateData("udp.receiveTest", 1);
         byte[] fromIP = from.getIP();
         int fromPort = from.getPort();
@@ -557,7 +574,7 @@ class PeerTestManager {
         PeerTestState test = _currentTest;
         if ( (test != null) && (test.getNonce() == nonce) ) {
             // we are Alice, we initiated the test
-            receiveTestReply(from, testInfo);
+            receiveTestReply(from, fromPeer, inSession, testInfo);
             return;
         }
 
@@ -606,7 +623,7 @@ class PeerTestManager {
                         _log.warn("Too many active tests, droppping from Alice " + Addresses.toString(fromIP, fromPort));
                     return;
                 }
-                if (_transport.getPeerState(from) == null) {
+                if (!inSession || fromPeer == null) {
                     // Require an existing session to start a test,
                     // as a way of preventing trouble
                     if (_log.shouldLog(Log.WARN))
@@ -615,7 +632,7 @@ class PeerTestManager {
                 }
                 if (_log.shouldLog(Log.DEBUG))
                     _log.debug("test IP/port are blank coming from " + from + ", assuming we are Bob and they are alice");
-                receiveFromAliceAsBob(from, testInfo, nonce, null);
+                receiveFromAliceAsBob(from, fromPeer, testInfo, nonce, null);
             } else {
                 if (_recentTests.contains(lNonce)) {
                     // ignore the packet, as its a holdover from a recently completed locally
@@ -630,7 +647,7 @@ class PeerTestManager {
                         _log.debug("We are charlie, as the testIP/port is " + Addresses.toString(testIP, testPort) + " and the state is unknown for " + nonce);
                     // we are charlie, since alice never sends us her IP and port, only bob does (and,
                     // erm, we're not alice, since it isn't our nonce)
-                    receiveFromBobAsCharlie(from, testInfo, nonce, null);
+                    receiveFromBobAsCharlie(from, fromPeer, inSession, testInfo, nonce, null);
                 }
             }
         } else {
@@ -638,10 +655,16 @@ class PeerTestManager {
             if (state.getOurRole() == BOB) {
                 if (DataHelper.eq(fromIP, state.getAliceIP().getAddress()) && 
                     (fromPort == state.getAlicePort()) ) {
-                    receiveFromAliceAsBob(from, testInfo, nonce, state);
+                    if (!inSession || fromPeer == null) {
+                        // Still should be in-session
+                        if (_log.shouldWarn())
+                            _log.warn("No session, dropping test from Alice " + Addresses.toString(fromIP, fromPort));
+                        return;
+                    }
+                    receiveFromAliceAsBob(from, fromPeer, testInfo, nonce, state);
                 } else if (DataHelper.eq(fromIP, state.getCharlieIP().getAddress()) && 
                            (fromPort == state.getCharliePort()) ) {
-                    receiveFromCharlieAsBob(from, state);
+                    receiveFromCharlieAsBob(from, fromPeer, inSession, state);
                 } else {
                     if (_log.shouldLog(Log.WARN))
                         _log.warn("Received from a fourth party as bob!  alice: " + state.getAliceIP() + ", charlie: " + state.getCharlieIP() + ", dave: " + from);
@@ -650,7 +673,7 @@ class PeerTestManager {
                 if ( (testIP == null) || (testPort <= 0) ) {
                     receiveFromAliceAsCharlie(from, testInfo, nonce, state);
                 } else {
-                    receiveFromBobAsCharlie(from, testInfo, nonce, state);
+                    receiveFromBobAsCharlie(from, fromPeer, inSession, testInfo, nonce, state);
                 }
             }
         }
@@ -662,9 +685,18 @@ class PeerTestManager {
      * The packet's IP/port does not match the IP/port included in the message, 
      * so we must be Charlie receiving a PeerTest from Bob.
      *  
+     * @param bob non-null if received in-session, otherwise null
+     * @param inSession true if authenticated in-session
      * @param state null if new
      */
-    private void receiveFromBobAsCharlie(RemoteHostId from, UDPPacketReader.PeerTestReader testInfo, long nonce, PeerTestState state) {
+    private void receiveFromBobAsCharlie(RemoteHostId from, PeerState bob, boolean inSession,
+                                         UDPPacketReader.PeerTestReader testInfo, long nonce, PeerTestState state) {
+        if (!inSession || bob == null) {
+            if (_log.shouldWarn())
+                _log.warn("Received from bob (" + from + ") as charlie w/o session");
+            return;
+        }
+
         long now = _context.clock().now();
         int sz = testInfo.readIPSize();
         boolean isNew = false;
@@ -701,17 +733,8 @@ class PeerTestManager {
             state.setBobIP(bobIP);
             state.setBobPort(from.getPort());
             state.setReceiveBobTime(now);
+            state.setBobKeys(bob.getCurrentCipherKey(), bob.getCurrentMACKey());
             
-            PeerState bob = _transport.getPeerState(from);
-            if (bob == null) {
-                if (_log.shouldLog(Log.WARN))
-                    _log.warn("Received from bob (" + from + ") who hasn't established a session with us, refusing to help him test " + aliceIP +":" + alicePort);
-                return;
-            } else {
-                state.setBobCipherKey(bob.getCurrentCipherKey());
-                state.setBobMACKey(bob.getCurrentMACKey());
-            }
-
             // we send two packets below, but increment just once
             if (state.incrementPacketsRelayed() > MAX_RELAYED_PER_TEST_CHARLIE) {
                 if (_log.shouldLog(Log.WARN))
@@ -748,9 +771,12 @@ class PeerTestManager {
      * any info in the message), plus we are not acting as Charlie (so we've got to be Bob).
      *
      * testInfo IP/port ignored
+     *
+     * @param alice non-null
      * @param state null if new
      */
-    private void receiveFromAliceAsBob(RemoteHostId from, UDPPacketReader.PeerTestReader testInfo, long nonce, PeerTestState state) {
+    private void receiveFromAliceAsBob(RemoteHostId from, PeerState alice, UDPPacketReader.PeerTestReader testInfo,
+                                       long nonce, PeerTestState state) {
         // we are Bob, so pick a (potentially) Charlie and send Charlie Alice's info
         PeerState charlie;
         RouterInfo charlieInfo = null;
@@ -819,6 +845,7 @@ class PeerTestManager {
             state.setAliceIP(aliceIP);
             state.setAlicePort(from.getPort());
             state.setAliceIntroKey(aliceIntroKey);
+            state.setAliceKeys(alice.getCurrentCipherKey(), alice.getCurrentMACKey());
             state.setCharlieIP(charlie.getRemoteIPAddress());
             state.setCharliePort(charlie.getRemotePort());
             state.setCharlieIntroKey(charlieIntroKey);
@@ -858,9 +885,18 @@ class PeerTestManager {
      * packet verifying participation.
      *
      * testInfo IP/port ignored
+     *
+     * @param fromPeer non-null if an associated session was found, otherwise null
+     * @param inSession true if authenticated in-session
      * @param state non-null
      */
-    private void receiveFromCharlieAsBob(RemoteHostId from, PeerTestState state) {
+    private void receiveFromCharlieAsBob(RemoteHostId from, PeerState charlie, boolean inSession, PeerTestState state) {
+        if (!inSession || charlie == null) {
+            if (_log.shouldWarn())
+                _log.warn("Received from charlie (" + from + ") as bob w/o session");
+            return;
+        }
+
         long now = _context.clock().now();
         if (state.getReceiveCharlieTime() > now - (RESEND_TIMEOUT / 2)) {
             if (_log.shouldLog(Log.WARN))
@@ -876,9 +912,10 @@ class PeerTestManager {
         state.setReceiveCharlieTime(now);
         state.setLastSendTime(now);
         
+        // In-session as of 0.9.52
         UDPPacket packet = _packetBuilder.buildPeerTestToAlice(state.getAliceIP(), state.getAlicePort(),
-                                                               state.getAliceIntroKey(), state.getCharlieIntroKey(), 
-                                                               state.getNonce());
+                                                               state.getAliceCipherKey(), state.getAliceMACKey(),
+                                                               state.getCharlieIntroKey(), state.getNonce());
 
         if (_log.shouldLog(Log.DEBUG))
             _log.debug("Receive from Charlie, sending Alice back the OK: " + state);
diff --git a/router/java/src/net/i2p/router/transport/udp/PeerTestState.java b/router/java/src/net/i2p/router/transport/udp/PeerTestState.java
index d9e883656e..3d03f82609 100644
--- a/router/java/src/net/i2p/router/transport/udp/PeerTestState.java
+++ b/router/java/src/net/i2p/router/transport/udp/PeerTestState.java
@@ -23,6 +23,8 @@ class PeerTestState {
     private InetAddress _aliceIPFromCharlie;
     private int _alicePortFromCharlie;
     private SessionKey _aliceIntroKey;
+    private SessionKey _aliceCipherKey;
+    private SessionKey _aliceMACKey;
     private SessionKey _charlieIntroKey;
     private SessionKey _bobCipherKey;
     private SessionKey _bobMACKey;
@@ -85,12 +87,42 @@ class PeerTestState {
     
     public SessionKey getAliceIntroKey() { return _aliceIntroKey; }
     public void setAliceIntroKey(SessionKey key) { _aliceIntroKey = key; }
+
+    /**
+     *  @since 0.9.52
+     */
+    public SessionKey getAliceCipherKey() { return _aliceCipherKey; }
+
+    /**
+     *  @since 0.9.52
+     */
+    public SessionKey getAliceMACKey() { return _aliceMACKey; }
+
+    /**
+     *  @param ck cipher key
+     *  @param mk MAC key
+     *  @since 0.9.52
+     */
+    public void setAliceKeys(SessionKey ck, SessionKey mk) {
+        _aliceCipherKey = ck;
+        _aliceMACKey = mk;
+    }
+
     public SessionKey getCharlieIntroKey() { return _charlieIntroKey; }
     public void setCharlieIntroKey(SessionKey key) { _charlieIntroKey = key; }
+
     public SessionKey getBobCipherKey() { return _bobCipherKey; }
-    public void setBobCipherKey(SessionKey key) { _bobCipherKey = key; }
     public SessionKey getBobMACKey() { return _bobMACKey; }
-    public void setBobMACKey(SessionKey key) { _bobMACKey = key; }
+
+    /**
+     *  @param ck cipher key
+     *  @param mk MAC key
+     *  @since 0.9.52
+     */
+    public void setBobKeys(SessionKey ck, SessionKey mk) {
+        _bobCipherKey = ck;
+        _bobMACKey = mk;
+    }
     
     /** when did this test begin? */
     public long getBeginTime() { return _beginTime; }
-- 
GitLab