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
This commit is contained in:
zzz
2022-02-28 09:18:06 -05:00
parent c19944384e
commit ec63f41b27
4 changed files with 278 additions and 95 deletions

View File

@ -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);
}
/**

View File

@ -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.
*
* @param state must be version 2
* 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, 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.
*
* @param state must be version 2
* Possible messages here are Session Created or Retry
*
* @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;
}

View File

@ -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();

View File

@ -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 */