From ec63f41b27c93f0b4bf5b398a720186b493cd6a3 Mon Sep 17 00:00:00 2001 From: zzz <zzz@i2pmail.org> Date: Mon, 28 Feb 2022 09:18:06 -0500 Subject: [PATCH] SSU2: Handle handshake messages Decrypt handshake headers in Packet Handler Pass handshake messages to Establishment Manager SSU 1 and 2: Pass establish state to Establishment Manager so it doesn't have to look it up again Add notes about causes of decrypt failures WIP, untested --- .../transport/udp/EstablishmentManager.java | 147 ++++++------ .../router/transport/udp/PacketHandler.java | 212 ++++++++++++++++-- .../i2p/router/transport/udp/PeerState2.java | 11 + .../i2p/router/transport/udp/SSU2Header.java | 3 +- 4 files changed, 278 insertions(+), 95 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 8bea1842e7..92173a10d5 100644 --- a/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java +++ b/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java @@ -483,8 +483,10 @@ class EstablishmentManager { * Got a SessionRequest (initiates an inbound establishment) * * SSU 1 only. + * + * @param state as looked up in PacketHandler, but probably null unless retransmitted */ - void receiveSessionRequest(RemoteHostId from, UDPPacketReader reader) { + void receiveSessionRequest(RemoteHostId from, InboundEstablishState state, UDPPacketReader reader) { if (!TransportUtil.isValidPort(from.getPort()) || !_transport.isValid(from.getIP())) { if (_log.shouldLog(Log.WARN)) _log.warn("Receive session request from invalid: " + from); @@ -493,7 +495,8 @@ class EstablishmentManager { boolean isNew = false; - InboundEstablishState state = _inboundStates.get(from); + if (state == null) + state = _inboundStates.get(from); if (state == null) { // TODO this is insufficient to prevent DoSing, especially if // IP spoofing is used. For further study. @@ -560,16 +563,17 @@ class EstablishmentManager { * Got a SessionRequest OR a TokenRequest (initiates an inbound establishment) * * SSU 2 only. + * @param state as looked up in PacketHandler, but null unless retransmitted or retry sent + * @param packet header decrypted only * @since 0.9.54 */ - void receiveSessionRequest(RemoteHostId from, UDPPacket packet) { + void receiveSessionOrTokenRequest(RemoteHostId from, InboundEstablishState2 state, 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. @@ -607,9 +611,20 @@ class EstablishmentManager { InboundEstablishState oldState = _inboundStates.putIfAbsent(from, state); isNew = oldState == null; - if (!isNew) + if (!isNew) { // whoops, somebody beat us to it, throw out the state we just created - state = oldState; + if (oldState.getVersion() == 2) + state = (InboundEstablishState2) oldState; + // else don't cast, this is only for printing below + } + } else { + try { + state.receiveSessionRequestAfterRetry(packet); + } catch (GeneralSecurityException gse) { + if (_log.shouldWarn()) + _log.warn("Corrupt Session Request after Retry from: " + state, gse); + return; + } } if (isNew) { @@ -629,10 +644,10 @@ class EstablishmentManager { } ****/ if (_log.shouldInfo()) - _log.info("Received NEW session request " + state); + _log.info("Received NEW session/token request " + state); } else { if (_log.shouldDebug()) - _log.debug("Receive DUP session request from: " + state); + _log.debug("Receive DUP session/token request from: " + state); } notifyActivity(); } @@ -642,9 +657,12 @@ class EstablishmentManager { * establishment) * * SSU 1 only. + * + * @param state as looked up in PacketHandler, if null is probably retransmitted */ - void receiveSessionConfirmed(RemoteHostId from, UDPPacketReader reader) { - InboundEstablishState state = _inboundStates.get(from); + void receiveSessionConfirmed(RemoteHostId from, InboundEstablishState state, UDPPacketReader reader) { + if (state == null) + state = _inboundStates.get(from); if (state != null) { state.receiveSessionConfirmed(reader.getSessionConfirmedReader()); notifyActivity(); @@ -661,40 +679,36 @@ class EstablishmentManager { * establishment) * * SSU 2 only. + * @param state non-null + * @param packet header decrypted 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); + void receiveSessionConfirmed(InboundEstablishState2 state, UDPPacket packet) { + try { + state.receiveSessionConfirmed(packet); + } catch (GeneralSecurityException gse) { + if (_log.shouldWarn()) + _log.warn("Corrupt Session Confirmed on: " + state, gse); + state.fail(); + return; } + // we are done, go right to ps2 + handleCompletelyEstablished(state); + notifyActivity(); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Receive session confirmed from: " + state); } /** * Got a SessionCreated (in response to our outbound SessionRequest) * * SSU 1 only. + * + * @param state as looked up in PacketHandler, if null is probably retransmitted */ - void receiveSessionCreated(RemoteHostId from, UDPPacketReader reader) { - OutboundEstablishState state = _outboundStates.get(from); + void receiveSessionCreated(RemoteHostId from, OutboundEstablishState state, UDPPacketReader reader) { + if (state == null) + state = _outboundStates.get(from); if (state != null) { state.receiveSessionCreated(reader.getSessionCreatedReader()); notifyActivity(); @@ -710,29 +724,23 @@ class EstablishmentManager { * Got a SessionCreated (in response to our outbound SessionRequest) * * SSU 2 only. + * + * @param state non-null + * @param packet header decrypted 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); + void receiveSessionCreated(OutboundEstablishState2 state, UDPPacket packet) { + try { + state.receiveSessionCreated(packet); + } catch (GeneralSecurityException gse) { + if (_log.shouldWarn()) + _log.warn("Corrupt Session Created on: " + state, gse); + state.fail(); + return; } + notifyActivity(); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Receive session created from: " + state); } /** @@ -741,27 +749,18 @@ class EstablishmentManager { * 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); + void receiveRetry(OutboundEstablishState2 state, UDPPacket packet) { + try { + state.receiveSessionCreated(packet); + } catch (GeneralSecurityException gse) { + if (_log.shouldWarn()) + _log.warn("Corrupt Retry from: " + state, gse); + state.fail(); + return; } + notifyActivity(); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Receive retry from: " + state); } /** 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 cc53982d14..0d2f02e924 100644 --- a/router/java/src/net/i2p/router/transport/udp/PacketHandler.java +++ b/router/java/src/net/i2p/router/transport/udp/PacketHandler.java @@ -41,6 +41,7 @@ class PacketHandler { private final BlockingQueue<UDPPacket> _inboundQueue; private static final Object DUMMY = new Object(); private final boolean _enableSSU2; + private final int _networkID; private static final int TYPE_POISON = -99999; private static final int MIN_QUEUE_SIZE = 16; @@ -68,6 +69,7 @@ class PacketHandler { _testManager = testManager; _introManager = introManager; _failCache = new LHMCache<RemoteHostId, Object>(24); + _networkID = ctx.router().getNetworkID(); long maxMemory = SystemVersion.getMaxMemory(); int qsize = (int) Math.max(MIN_QUEUE_SIZE, Math.min(MAX_QUEUE_SIZE, maxMemory / (2*1024*1024))); @@ -241,7 +243,7 @@ class PacketHandler { if (_log.shouldLog(Log.DEBUG)) _log.debug("Packet received IS for an inbound establishment"); if (est.getVersion() == 2) - receiveSSU2Packet(packet, (InboundEstablishState2) est); + receiveSSU2Packet(rem, packet, (InboundEstablishState2) est); else receivePacket(reader, packet, est); } else { @@ -354,6 +356,17 @@ class PacketHandler { alreadyFailed = _failCache.get(remoteHost) != null; } if (!alreadyFailed) { + // For now, try SSU2 Session/Token Request processing here. + // After we've migrated the majority of the network over to SSU2, + // we can try SSU2 first. + if (_enableSSU2 && peerType == PeerType.NEW_PEER) { + boolean handled = receiveSSU2Packet(remoteHost, packet, (InboundEstablishState2) null); + if (handled) + return; + if (_log.shouldDebug()) + _log.debug("Continuing with SSU1 fallback processing, wasn't an SSU2 packet from " + remoteHost); + } + // this is slow, that's why we cache it above. List<PeerState> peers = _transport.getPeerStatesByIP(remoteHost); if (!peers.isEmpty()) { @@ -623,9 +636,6 @@ class PacketHandler { return; } - //InetAddress fromHost = packet.getPacket().getAddress(); - //int fromPort = packet.getPacket().getPort(); - //RemoteHostId from = new RemoteHostId(fromHost.getAddress(), fromPort); RemoteHostId from = packet.getRemoteHost(); switch (type) { @@ -635,7 +645,7 @@ class PacketHandler { _log.warn("Dropping type " + type + " auth " + auth + ": " + packet); break; } - _establisher.receiveSessionRequest(from, reader); + _establisher.receiveSessionRequest(from, inState, reader); break; case UDPPacket.PAYLOAD_TYPE_SESSION_CONFIRMED: if (auth != AuthType.SESSION) { @@ -643,7 +653,7 @@ class PacketHandler { _log.warn("Dropping type " + type + " auth " + auth + ": " + packet); break; } - _establisher.receiveSessionConfirmed(from, reader); + _establisher.receiveSessionConfirmed(from, inState, reader); break; case UDPPacket.PAYLOAD_TYPE_SESSION_CREATED: // this is the only type that allows BOBINTRO @@ -652,7 +662,7 @@ class PacketHandler { _log.warn("Dropping type " + type + " auth " + auth + ": " + packet); break; } - _establisher.receiveSessionCreated(from, reader); + _establisher.receiveSessionCreated(from, outState, reader); break; case UDPPacket.PAYLOAD_TYPE_DATA: if (auth != AuthType.SESSION) { @@ -763,37 +773,199 @@ class PacketHandler { * Packet is decrypted in-place, no fallback * processing is possible. * - * @param state must be version 2 + * @param packet any in-session message + * @param state must be version 2, non-null * @since 0.9.54 */ private void receiveSSU2Packet(UDPPacket packet, PeerState2 state) { + // header and body decryption is done by PeerState2 state.receivePacket(packet); } /** - * Hand off to the state for processing. - * Packet is decrypted in-place, no fallback - * processing is possible. + * Decrypt the header and hand off to the state for processing. + * Packet is trial-decrypted, so fallback + * processing is possible if this returns false. + * + * Possible messages here are Session Request, Token Request, Session Confirmed, or Peer Test. + * Data messages out-of-order from Session Confirmed, or following a + * Session Confirmed that was lost, or in-order but before the Session Confirmed was processed, + * will not be successfully decrypted and will be dropped. * - * @param state must be version 2 + * @param state must be version 2, but will be null for session request unless retransmitted + * @return true if the header was validated as a SSU2 packet, cannot fallback to SSU 1 * @since 0.9.54 */ - private void receiveSSU2Packet(UDPPacket packet, InboundEstablishState2 state) { - + private boolean receiveSSU2Packet(RemoteHostId from, UDPPacket packet, InboundEstablishState2 state) { + // decrypt header + byte[] k1 = _transport.getSSU2StaticIntroKey(); + byte[] k2; + SSU2Header.Header header; + int type; + if (state == null) { + // Session Request, Token Request, or Peer Test + k2 = k1; + header = SSU2Header.trialDecryptHandshakeHeader(packet, k1, k2); + if (header == null || + header.getType() != SSU2Util.SESSION_REQUEST_FLAG_BYTE || + header.getVersion() != 2 || + header.getNetID() != _networkID) { + if (_log.shouldInfo()) + _log.info("Does not decrypt as Session Request, attempt to decrypt as Token Request/Peer Test: " + header); + // The first 32 bytes were fine, but it corrupted the next 32 bytes + // TODO make this more efficient, just take the first 32 bytes + header = SSU2Header.trialDecryptLongHeader(packet, k1, k2); + if (header == null || + header.getVersion() != 2 || + header.getNetID() != _networkID) { + if (_log.shouldWarn()) + _log.warn("Does not decrypt as Session Request, Token Request, or Peer Test: " + header); + return false; + } + type = header.getType(); + } else { + type = SSU2Util.SESSION_REQUEST_FLAG_BYTE; + } + } else { + // Session Request (after Retry) or Session Confirmed + // or retransmitted Session Request or Token Rquest + k2 = state.getRcvHeaderEncryptKey2(); + if (state.getState() == InboundEstablishState.InboundState.IB_STATE_RETRY_SENT) { + // Session Request + header = SSU2Header.trialDecryptHandshakeHeader(packet, k1, k2); + if (header == null || + header.getType() != SSU2Util.SESSION_REQUEST_FLAG_BYTE || + header.getVersion() != 2 || + header.getNetID() != _networkID) { + if (_log.shouldWarn()) + _log.warn("Failed decrypt Session Request after Retry: " + header); + return false; + } + type = SSU2Util.SESSION_REQUEST_FLAG_BYTE; + } else { + // Session Confirmed or retransmitted Session Request or Token Request + header = SSU2Header.trialDecryptShortHeader(packet, k1, k2); + if (header == null || + header.getType() != SSU2Util.SESSION_CONFIRMED_FLAG_BYTE) { + if (_log.shouldWarn()) + _log.warn("Failed decrypt Session Confirmed: " + header); + // TODO either attempt to decrypt as a retransmitted + // Session Request or Token Request, + // or just tell establisher so it can retransmit Session Created or Retry + // Could also be Data messages after (possibly lost or out-of-order) Session Confirmed + return false; + } + type = SSU2Util.SESSION_CONFIRMED_FLAG_BYTE; + } + if (header.getDestConnID() != state.getRcvConnID()) { + if (_log.shouldWarn()) + _log.warn("Bad Dest Conn id " + header); + return false; + } + if (header.getSrcConnID() != state.getSendConnID()) { + if (_log.shouldWarn()) + _log.warn("Bad Source Conn id " + header); + // TODO could be a retransmitted Session Request, + // tell establisher? + return false; + } + } + // all good + SSU2Header.acceptTrialDecrypt(packet, header); + if (type == SSU2Util.SESSION_REQUEST_FLAG_BYTE) { + if (_log.shouldDebug()) + _log.debug("Got a Session Request on " + state); + _establisher.receiveSessionOrTokenRequest(from, state, packet); + } else if (type == SSU2Util.TOKEN_REQUEST_FLAG_BYTE) { + if (_log.shouldDebug()) + _log.debug("Got a Token Request on " + state); + _establisher.receiveSessionOrTokenRequest(from, state, packet); + } else if (type == SSU2Util.SESSION_CONFIRMED_FLAG_BYTE) { + if (_log.shouldDebug()) + _log.debug("Got a Session Confirmed on " + state); + _establisher.receiveSessionConfirmed(state, packet); + } else if (type == SSU2Util.PEER_TEST_FLAG_BYTE) { + if (_log.shouldDebug()) + _log.debug("Got a Peer Test on " + state); + // TODO + } else { + if (_log.shouldWarn()) + _log.warn("Got unknown message " + header + " on " + state); + } + return true; } /** - * Hand off to the state for processing. - * Packet is decrypted in-place, no fallback - * processing is possible. + * Decrypt the header and hand off to the state for processing. + * Packet is trial-decrypted, so fallback + * processing is possible if this returns false. + * But that's probably not necessary. + * + * Possible messages here are Session Created or Retry * - * @param state must be version 2 + * @param state must be version 2, non-null + * @return true if the header was validated as a SSU2 packet, cannot fallback to SSU 1 * @since 0.9.54 */ - private void receiveSSU2Packet(UDPPacket packet, OutboundEstablishState2 state) { - + private boolean receiveSSU2Packet(UDPPacket packet, OutboundEstablishState2 state) { + // decrypt header + byte[] k1 = state.getRcvHeaderEncryptKey1(); + byte[] k2 = state.getRcvHeaderEncryptKey2(); + SSU2Header.Header header = SSU2Header.trialDecryptHandshakeHeader(packet, k1, k2); + if (header != null) { + // dest conn ID decrypts the same for both Session Created + // and Retry, so we can bail out now if it doesn't match + if (header.getDestConnID() != state.getRcvConnID()) { + if (_log.shouldWarn()) + _log.warn("Bad Dest Conn id " + header); + return false; + } + } + int type; + if (header == null || + header.getType() != SSU2Util.SESSION_CREATED_FLAG_BYTE || + header.getVersion() != 2 || + header.getNetID() != _networkID) { + if (_log.shouldInfo()) + _log.info("Does not decrypt as Session Created, attempt to decrypt as Retry: " + header); + k2 = state.getRcvRetryHeaderEncryptKey2(); + header = SSU2Header.trialDecryptLongHeader(packet, k1, k2); + if (header == null || + header.getType() != SSU2Util.RETRY_FLAG_BYTE || + header.getVersion() != 2 || + header.getNetID() != _networkID) { + if (_log.shouldWarn()) + _log.warn("Does not decrypt as Session Created or Retry: " + header); + return false; + } + type = SSU2Util.RETRY_FLAG_BYTE; + } else { + type = SSU2Util.SESSION_CREATED_FLAG_BYTE; + } + if (header.getDestConnID() != state.getRcvConnID()) { + if (_log.shouldWarn()) + _log.warn("Bad Dest Conn id " + header); + return false; + } + if (header.getSrcConnID() != state.getSendConnID()) { + if (_log.shouldWarn()) + _log.warn("Bad Source Conn id " + header); + return false; + } + // all good + SSU2Header.acceptTrialDecrypt(packet, header); + if (type == SSU2Util.SESSION_CREATED_FLAG_BYTE) { + if (_log.shouldDebug()) + _log.debug("Got a Session Created on " + state); + _establisher.receiveSessionCreated(state, packet); + } else { + if (_log.shouldDebug()) + _log.debug("Got a Retry on " + state); + _establisher.receiveRetry(state, packet); + } + return true; } 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 125e444fcf..86c590626f 100644 --- a/router/java/src/net/i2p/router/transport/udp/PeerState2.java +++ b/router/java/src/net/i2p/router/transport/udp/PeerState2.java @@ -138,6 +138,9 @@ public class PeerState2 extends PeerState implements SSU2Payload.PayloadCallback SSU2Bitfield getReceivedMessages() { return _receivedMessages; } SSU2Bitfield getAckedMessages() { return _ackedMessages; } + /** + * @param packet fully encrypted, header and body decryption will be done here + */ void receivePacket(UDPPacket packet) { DatagramPacket dpacket = packet.getPacket(); byte[] data = dpacket.getData(); @@ -163,6 +166,14 @@ public class PeerState2 extends PeerState implements SSU2Payload.PayloadCallback if (header.getType() != DATA_FLAG_BYTE) { if (_log.shouldWarn()) _log.warn("bad data pkt type " + (header.getType() & 0xff) + " on " + this); + // TODO if it's early: + // If inbound, could be a retransmitted Session Confirmed, + // ack it again. + // If outbound, and Session Confirmed is not acked yet, + // could be a retransmitted Session Created, + // retransmit Session Confirmed. + // Alternatively, could be a new Session Request or Token Request, + // we didn't know the session has disconnected yet. return; } long n = header.getPacketNumber(); 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 fe48009ca8..f44441ffd8 100644 --- a/router/java/src/net/i2p/router/transport/udp/SSU2Header.java +++ b/router/java/src/net/i2p/router/transport/udp/SSU2Header.java @@ -25,7 +25,7 @@ final class SSU2Header { * Session Request and Session Created only. 64 bytes. * Packet is unmodified. * - * @param packet must be 56 bytes min + * @param packet must be 88 bytes min * @return 64 byte header, null if data too short */ public static Header trialDecryptHandshakeHeader(UDPPacket packet, byte[] key1, byte[] key2) { @@ -155,6 +155,7 @@ final class SSU2Header { */ public static class Header { public final byte[] data; + public Header(int len) { data = new byte[len]; } /** all headers */ -- GitLab