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 0cfa41439..bcecd9d40 100644 --- a/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java +++ b/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java @@ -54,6 +54,7 @@ import net.i2p.util.HexDump; import net.i2p.util.I2PThread; import net.i2p.util.LHMCache; import net.i2p.util.Log; +import net.i2p.util.ObjectCounter; import net.i2p.util.SecureFileOutputStream; import net.i2p.util.SystemVersion; import net.i2p.util.VersionComparator; @@ -76,6 +77,7 @@ class EstablishmentManager { private final boolean _enableSSU2; private final Map _outboundTokens; private final Map _inboundTokens; + private final ObjectCounter _terminationCounter; /** map of RemoteHostId to InboundEstablishState */ private final ConcurrentHashMap _inboundStates; @@ -185,6 +187,8 @@ class EstablishmentManager { public static final long IB_TOKEN_EXPIRATION = 60*60*1000L; private static final long MAX_SKEW = 2*60*1000; private static final String TOKEN_FILE = "ssu2tokens.txt"; + // max immediate terminations to send to a peer every FAILSAFE_INTERVAL + private static final int MAX_TERMINATIONS = 2; public EstablishmentManager(RouterContext ctx, UDPTransport transport) { @@ -207,9 +211,11 @@ class EstablishmentManager { int tokenCacheSize = Math.max(MIN_TOKENS, Math.min(MAX_TOKENS, 3 * _transport.getMaxConnections() / 4)); _inboundTokens = new InboundTokens(tokenCacheSize); _outboundTokens = new LHMCache(tokenCacheSize); + _terminationCounter = new ObjectCounter(); } else { _inboundTokens = null; _outboundTokens = null; + _terminationCounter = null; } _activityLock = new Object(); @@ -700,29 +706,38 @@ class EstablishmentManager { if (_log.shouldWarn()) _log.warn("Dropping inbound establish, increase " + PROP_MAX_CONCURRENT_ESTABLISH); _context.statManager().addRateData("udp.establishDropped", 1); - return; // drop the packet + sendTerminationPacket(from, packet, REASON_LIMITS); + return; } if (_context.blocklist().isBlocklisted(from.getIP())) { if (_log.shouldInfo()) _log.info("Receive session request from blocklisted IP: " + from); _context.statManager().addRateData("udp.establishBadIP", 1); - return; // drop the packet + if (!_context.commSystem().isInStrictCountry()) + sendTerminationPacket(from, packet, REASON_BANNED); + // else drop the packet + return; } synchronized (_inboundBans) { Long exp = _inboundBans.get(from); if (exp != null) { if (exp.longValue() >= _context.clock().now()) { + // this is common, finally get a packet after the IES2 timeout if (_log.shouldInfo()) _log.info("SSU 2 session request from temp. blocked peer: " + from); _context.statManager().addRateData("udp.establishBadIP", 1); - return; // drop the packet + // use this code for a temp ban + sendTerminationPacket(from, packet, REASON_MSG1); + return; } // expired _inboundBans.remove(from); } } - if (!_transport.allowConnection()) - return; // drop the packet + if (!_transport.allowConnection()) { + sendTerminationPacket(from, packet, REASON_LIMITS); + return; + } try { state = new InboundEstablishState2(_context, _transport, packet); } catch (GeneralSecurityException gse) { @@ -781,6 +796,50 @@ class EstablishmentManager { } notifyActivity(); } + + /** + * Send a Retry packet with a termination code, for a rejection + * of a session/token request. No InboundEstablishState2 required. + * + * SSU 2 only. + * The inbound packet was superficially validated for type, netID, and version, + * so we have basic probing resistance. + * The Retry packet encryption is low-cost, chacha only. + * + * Rate limited to MAX_TERMINATIONS per peer every FAILSAFE_INTERVAL + * + * @param fromPacket header already decrypted, must be session or token request + * @param terminationCode nonzero + * @since 0.9.57 + */ + private void sendTerminationPacket(RemoteHostId to, UDPPacket fromPacket, int terminationCode) { + int count = _terminationCounter.increment(to); + if (count > MAX_TERMINATIONS) { + // not everybody listens or backs off... + if (_log.shouldWarn()) + _log.warn("Rate limit " + count + " not sending termination to: " + to); + return; + } + // very basic validation that this is probably in response to a good packet. + // we don't bother to decrypt the packet, even if it's only a token request + DatagramPacket pkt = fromPacket.getPacket(); + int off = pkt.getOffset(); + int len = pkt.getLength(); + if (len < MIN_LONG_DATA_LEN) + return; + byte data[] = pkt.getData(); + int type = data[off + TYPE_OFFSET] & 0xff; + if (type == SSU2Util.SESSION_REQUEST_FLAG_BYTE && len < MIN_SESSION_REQUEST_LEN) + return; + long rcvConnID = DataHelper.fromLong8(data, off); + long sendConnID = DataHelper.fromLong8(data, off + SRC_CONN_ID_OFFSET); + if (rcvConnID == 0 || sendConnID == 0 || rcvConnID == sendConnID) + return; + if (_log.shouldWarn()) + _log.warn("Send immediate termination " + terminationCode + " on type " + type + " to: " + to); + UDPPacket packet = _builder2.buildRetryPacket(to, pkt.getSocketAddress(), sendConnID, rcvConnID, terminationCode); + _transport.send(packet); + } /** * got a SessionConfirmed (should only happen as part of an inbound @@ -3155,6 +3214,7 @@ class EstablishmentManager { } if (count > 0 && _log.shouldDebug()) _log.debug("Expired " + count + " outbound tokens"); + _terminationCounter.clear(); } } } 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 1505a3a32..a9e074d53 100644 --- a/router/java/src/net/i2p/router/transport/udp/InboundEstablishState2.java +++ b/router/java/src/net/i2p/router/transport/udp/InboundEstablishState2.java @@ -513,9 +513,11 @@ class InboundEstablishState2 extends InboundEstablishState implements SSU2Payloa throw new IllegalStateException("Bad state for Retry Sent: " + _currentState); _currentState = InboundState.IB_STATE_RETRY_SENT; _lastSend = _context.clock().now(); - // Won't really be retransmitted, they have 9 sec to respond or + // Won't really be retransmitted, they have 5 sec to respond or // EstablishmentManager.handleInbound() will fail the connection - _nextSend = _lastSend + (3 * RETRANSMIT_DELAY); + // Alice will retransmit at 1 and 3 seconds, so wait 5 + // We're not going to wait for the 3rd retx at 7 seconds. + _nextSend = _lastSend + (5 * RETRANSMIT_DELAY); } /** 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 7a6e6aabc..3b132dbdd 100644 --- a/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState2.java +++ b/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState2.java @@ -346,6 +346,13 @@ class OutboundEstablishState2 extends OutboundEstablishState implements SSU2Payl // this sets the state to FAILED fail(); _transport.getEstablisher().receiveSessionDestroy(_remoteHostId, this); + if (reason == REASON_BANNED) { + _context.banlist().banlistRouter(_remotePeer.calculateHash(), "They banned us", null, null, _context.clock().now() + 2*60*60*1000); + } else if (reason == REASON_MSG1) { + // this is like a short ban + _context.banlist().banlistRouter(_remotePeer.calculateHash(), "They banned us", null, null, _context.clock().now() + 20*60*1000); + } + // TODO handle other cases - skew? } public void gotPathChallenge(RemoteHostId from, byte[] data) { @@ -470,9 +477,9 @@ class OutboundEstablishState2 extends OutboundEstablishState implements SSU2Payl if (sid != _sendConnID) throw new GeneralSecurityException("Conn ID mismatch: 1: " + _sendConnID + " 2: " + sid); long token = DataHelper.fromLong8(data, off + TOKEN_OFFSET); - if (token == 0) - throw new GeneralSecurityException("Bad token 0 in retry"); - _token = token; + // continue and decrypt even if token == 0 to get and log termination reason + if (token != 0) + _token = token; _timeReceived = 0; ChaChaPolyCipherState chacha = new ChaChaPolyCipherState(); chacha.initializeKey(_rcvHeaderEncryptKey1, 0); @@ -491,6 +498,9 @@ class OutboundEstablishState2 extends OutboundEstablishState implements SSU2Payl // termination block received return; } + // generally will be with termination, so do this check after + if (token == 0) + throw new GeneralSecurityException("Bad token 0 in retry"); if (_timeReceived == 0) throw new GeneralSecurityException("No DateTime block in Retry"); // _nextSend is now(), from packetReceived() 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 bee0af222..2604f47dc 100644 --- a/router/java/src/net/i2p/router/transport/udp/PacketBuilder2.java +++ b/router/java/src/net/i2p/router/transport/udp/PacketBuilder2.java @@ -2,6 +2,7 @@ package net.i2p.router.transport.udp; import java.net.DatagramPacket; import java.net.InetAddress; +import java.net.SocketAddress; import java.net.UnknownHostException; import java.security.GeneralSecurityException; import java.util.ArrayList; @@ -379,8 +380,8 @@ class PacketBuilder2 { /** * Build a new SessionRequest packet for the given peer, encrypting it * as necessary. - * - * @return ready to send packet, or null if there was a problem + * + * @return ready to send packet, non-null */ public UDPPacket buildTokenRequestPacket(OutboundEstablishState2 state) { long n = _context.random().signedNextInt() & 0xFFFFFFFFL; @@ -400,8 +401,8 @@ class PacketBuilder2 { /** * Build a new SessionRequest packet for the given peer, encrypting it * as necessary. - * - * @return ready to send packet, or null if there was a problem + * + * @return ready to send packet, non-null */ public UDPPacket buildSessionRequestPacket(OutboundEstablishState2 state) { long n = _context.random().signedNextInt() & 0xFFFFFFFFL; @@ -421,8 +422,8 @@ class PacketBuilder2 { /** * Build a new SessionCreated packet for the given peer, encrypting it * as necessary. - * - * @return ready to send packet, or null if there was a problem + * + * @return ready to send packet, non-null */ public UDPPacket buildSessionCreatedPacket(InboundEstablishState2 state) { long n = _context.random().signedNextInt() & 0xFFFFFFFFL; @@ -443,13 +444,13 @@ class PacketBuilder2 { state.createdPacketSent(pkt); return packet; } - + /** * Build a new Retry packet for the given peer, encrypting it * as necessary. - * + * * @param terminationCode 0 normally, nonzero to send termination block - * @return ready to send packet, or null if there was a problem + * @return ready to send packet, non-null */ public UDPPacket buildRetryPacket(InboundEstablishState2 state, int terminationCode) { long n = _context.random().signedNextInt() & 0xFFFFFFFFL; @@ -470,6 +471,28 @@ class PacketBuilder2 { state.retryPacketSent(); return packet; } + + /** + * Build a new Retry packet with a termination code, for a rejection + * direct from the EstablishmentManager. No InboundEstablishState2 required. + * + * @param terminationCode must be greater than zero + * @return ready to send packet, non-null + * @since 0.9.57 + */ + public UDPPacket buildRetryPacket(RemoteHostId to, SocketAddress toAddr, long destID, long srcID, int terminationCode) { + long n = _context.random().signedNextInt() & 0xFFFFFFFFL; + UDPPacket packet = buildLongPacketHeader(destID, n, RETRY_FLAG_BYTE, srcID, 0); + DatagramPacket pkt = packet.getPacket(); + pkt.setLength(LONG_HEADER_SIZE); + byte[] introKey = _transport.getSSU2StaticIntroKey(); + encryptRetry(packet, introKey, n, introKey, introKey, + to.getIP(), to.getPort(), terminationCode); + pkt.setSocketAddress(toAddr); + packet.setMessageType(TYPE_CREAT); + packet.setPriority(PRIORITY_LOW); + return packet; + } /** * Build a new series of SessionConfirmed packets for the given peer, @@ -479,8 +502,8 @@ class PacketBuilder2 { * a single Session Confirmed message. The remaining RI blocks will be passed to * the establish state via confirmedPacketsSent(), and the state will * transmit them via the new PeerState2. - * - * @return ready to send packets, or null if there was a problem + * + * @return ready to send packets, non-null */ public UDPPacket[] buildSessionConfirmedPackets(OutboundEstablishState2 state, RouterInfo ourInfo) { boolean gzip = false; @@ -540,8 +563,8 @@ class PacketBuilder2 { /** * Build a single new SessionConfirmed packet for the given peer, unfragmented. - * - * @return ready to send packet, or null if there was a problem + * + * @return ready to send packet, non-null */ private UDPPacket buildSessionConfirmedPacket(OutboundEstablishState2 state, SSU2Payload.RIBlock block) { UDPPacket packet = buildShortPacketHeader(state.getSendConnID(), 0, SESSION_CONFIRMED_FLAG_BYTE); @@ -657,8 +680,8 @@ class PacketBuilder2 { /** * Build a packet as Alice, to Bob to begin a peer test. * In-session, message 1. - * - * @return ready to send packet, or null if there was a problem + * + * @return ready to send packet, non-null */ public UDPPacket buildPeerTestFromAlice(byte[] signedData, PeerState2 bob) { Block block = new SSU2Payload.PeerTestBlock(1, 0, null, signedData); @@ -670,8 +693,8 @@ class PacketBuilder2 { /** * Build a packet as Alice to Charlie. * Out-of-session, message 6. - * - * @return ready to send packet, or null if there was a problem + * + * @return ready to send packet, non-null */ public UDPPacket buildPeerTestFromAlice(InetAddress toIP, int toPort, SessionKey introKey, long sendID, long rcvID, byte[] signedData) { @@ -694,7 +717,7 @@ class PacketBuilder2 { * In-session, message 4. * * @param charlieHash fake hash (all zeros) if rejected by bob - * @return ready to send packet, or null if there was a problem + * @return ready to send packet, non-null */ public UDPPacket buildPeerTestToAlice(int code, Hash charlieHash, byte[] signedData, PeerState2 alice) { Block block = new SSU2Payload.PeerTestBlock(4, code, charlieHash, signedData); @@ -706,8 +729,8 @@ class PacketBuilder2 { /** * Build a packet as Charlie to Alice. * Out-of-session, messages 5 and 7. - * - * @return ready to send packet, or null if there was a problem + * + * @return ready to send packet, non-null */ public UDPPacket buildPeerTestToAlice(InetAddress aliceIP, int alicePort, SessionKey introKey, boolean firstSend, @@ -729,8 +752,8 @@ class PacketBuilder2 { /** * Build a packet as Bob to Charlie to help test Alice. * In-session, message 2. - * - * @return ready to send packet, or null if there was a problem + * + * @return ready to send packet, non-null */ public UDPPacket buildPeerTestToCharlie(Hash aliceHash, byte[] signedData, PeerState2 charlie) { Block block = new SSU2Payload.PeerTestBlock(2, 0, aliceHash, signedData); @@ -742,8 +765,8 @@ class PacketBuilder2 { /** * Build a packet as Charlie to Bob verifying that we will help test Alice. * In-session, message 3. - * - * @return ready to send packet, or null if there was a problem + * + * @return ready to send packet, non-null */ public UDPPacket buildPeerTestToBob(int code, byte[] signedData, PeerState2 bob) { Block block = new SSU2Payload.PeerTestBlock(3, code, null, signedData); @@ -757,7 +780,7 @@ class PacketBuilder2 { * In-session. * * @param signedData flag + signed data - * @return null on failure + * @return non-null */ UDPPacket buildRelayRequest(byte[] signedData, PeerState2 bob) { Block block = new SSU2Payload.RelayRequestBlock(signedData); @@ -772,7 +795,7 @@ class PacketBuilder2 { * In-session. * * @param signedData flag + alice hash + signed data - * @return null on failure + * @return non-null */ UDPPacket buildRelayIntro(byte[] signedData, PeerState2 charlie) { Block block = new SSU2Payload.RelayIntroBlock(signedData); @@ -787,7 +810,7 @@ class PacketBuilder2 { * * @param signedData flag + response code + signed data + optional token * @param state Alice or Bob - * @return null on failure + * @return non-null */ UDPPacket buildRelayResponse(byte[] signedData, PeerState2 state) { Block block = new SSU2Payload.RelayResponseBlock(signedData); @@ -823,8 +846,8 @@ class PacketBuilder2 { * @return a packet with the first 32 bytes filled in */ private UDPPacket buildLongPacketHeader(long destID, long pktNum, byte type, long srcID, long token) { - if (_log.shouldDebug()) - _log.debug("Building long header destID " + destID + " pkt num " + pktNum + " type " + type + " srcID " + srcID + " token " + token); + //if (_log.shouldDebug()) + // _log.debug("Building long header destID " + destID + " pkt num " + pktNum + " type " + type + " srcID " + srcID + " token " + token); UDPPacket packet = buildShortPacketHeader(destID, pktNum, type); byte data[] = packet.getPacket().getData(); data[13] = PROTOCOL_VERSION; @@ -980,7 +1003,7 @@ class PacketBuilder2 { /** * Also used for hole punch with a relay request block. - * Also used for retry with ptBlock = null + * Also used for retry with (usually) ptBlock = null * * @param packet containing only 32 byte header * @param ptBlock Peer Test or Relay Request block. Null for retry. @@ -1193,8 +1216,4 @@ class PacketBuilder2 { } return new SSU2Payload.PaddingBlock(padlen); } - - private void writePayload(List blocks, byte[] data, int off) { - SSU2Payload.writePayload(data, off, blocks); - } }