package net.i2p.client.streaming.impl; import java.net.ConnectException; import java.net.SocketTimeoutException; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import net.i2p.I2PAppContext; import net.i2p.data.Destination; import net.i2p.util.Log; import net.i2p.util.SimpleTimer; import net.i2p.util.SimpleTimer2; /** * Receive new connection attempts * * Use a bounded queue to limit the damage from SYN floods, * router overload, or a slow client * * @author zzz modded to use concurrent and bound queue size */ class ConnectionHandler { private final I2PAppContext _context; private final Log _log; private final ConnectionManager _manager; private final LinkedBlockingQueue<Packet> _synQueue; private final SimpleTimer2 _timer; private volatile boolean _active; private int _acceptTimeout; /** max time after receiveNewSyn() and before the matched accept() */ private static final int DEFAULT_ACCEPT_TIMEOUT = 3*1000; /** * This is both SYNs and subsequent packets, and with an initial window size of 12, * this is a backlog of 5 to 64 Syns, which seems like plenty for now * Don't make this too big because the removal by all the TimeoutSyns is O(n**2) - sortof. */ private static final int MAX_QUEUE_SIZE = 64; /** Creates a new instance of ConnectionHandler */ public ConnectionHandler(I2PAppContext context, ConnectionManager mgr, SimpleTimer2 timer) { _context = context; _log = context.logManager().getLog(ConnectionHandler.class); _manager = mgr; _timer = timer; _synQueue = new LinkedBlockingQueue<Packet>(MAX_QUEUE_SIZE); _acceptTimeout = DEFAULT_ACCEPT_TIMEOUT; } public synchronized void setActive(boolean active) { // FIXME active=false this only kills for one thread in accept() // if there are more, they won't get a poison packet. if (_log.shouldLog(Log.WARN)) _log.warn("setActive(" + active + ") called, previously " + _active, new Exception("I did it")); // if starting, clear any old poison if (active && !_active) _synQueue.clear(); boolean wasActive = _active; _active = active; if (wasActive && !active) { // stopping, clear any pending sockets _synQueue.clear(); _synQueue.offer(new PoisonPacket()); } } public boolean getActive() { return _active; } /** * Non-SYN packets with a zero SendStreamID may also be queued here so * that they don't get thrown away while the SYN packet before it is queued. * * Additional overload protection may be required here... * We don't have a 3-way handshake, so the SYN fully opens a connection. * Does that make us more or less vulnerable to SYN flooding? * */ public void receiveNewSyn(Packet packet) { if (!_active) { if (packet.isFlagSet(Packet.FLAG_SYNCHRONIZE)) { if (_log.shouldLog(Log.WARN)) _log.warn("Dropping new SYN request, as we're not listening"); sendReset(packet); } else { if (_log.shouldLog(Log.WARN)) _log.warn("Dropping non-SYN packet - not listening"); } return; } if (_manager.wasRecentlyClosed(packet.getSendStreamId())) { if (_log.shouldLog(Log.WARN)) _log.warn("Dropping packet for recently closed stream: " + packet); return; } if (_log.shouldLog(Log.INFO)) _log.info("Receive new SYN: " + packet + ": timeout in " + _acceptTimeout); // also check if expiration of the head is long past for overload detection with peek() ? boolean success = _synQueue.offer(packet); // fail immediately if full if (success) { _timer.addEvent(new TimeoutSyn(packet), _acceptTimeout); } else { if (_log.shouldLog(Log.WARN)) _log.warn("Dropping new SYN request, as the queue is full"); if (packet.isFlagSet(Packet.FLAG_SYNCHRONIZE)) sendReset(packet); } } /** * Receive an incoming connection (built from a received SYN) * Non-SYN packets with a zero SendStreamID may also be queued here so * that they don't get thrown away while the SYN packet before it is queued. * * @param timeoutMs max amount of time to wait for a connection (if less * than 1ms, wait indefinitely) * @return connection received. Prior to 0.9.17, or null if there was a timeout or the * handler was shut down. As of 0.9.17, never null. * @throws ConnectException since 0.9.17, returned null before; * if the I2PServerSocket is closed, or if interrupted. * @throws SocketTimeoutException since 0.9.17, returned null before; * if a timeout was previously set with setSoTimeout and the timeout has been reached. */ public Connection accept(long timeoutMs) throws ConnectException, SocketTimeoutException { if (_log.shouldLog(Log.DEBUG)) _log.debug("Accept("+ timeoutMs+") called"); long expiration = timeoutMs + _context.clock().now(); while (true) { if ( (timeoutMs > 0) && (expiration < _context.clock().now()) ) throw new SocketTimeoutException("accept() timed out"); if (!_active) { // fail all the ones we had queued up while(true) { Packet packet = _synQueue.poll(); // fails immediately if empty if (packet == null || packet.getOptionalDelay() == PoisonPacket.POISON_MAX_DELAY_REQUEST) break; sendReset(packet); } throw new ConnectException("ServerSocket closed"); } Packet syn = null; while ( _active && syn == null) { if (_log.shouldLog(Log.DEBUG)) _log.debug("Accept("+ timeoutMs+"): active=" + _active + " queue: " + _synQueue.size()); if (timeoutMs <= 0) { try { syn = _synQueue.take(); // waits forever } catch (InterruptedException ie) { ConnectException ce = new ConnectException("Interrupted accept()"); ce.initCause(ie); throw ce; } } else { long remaining = expiration - _context.clock().now(); // (dont think this applies anymore for LinkedBlockingQueue) // BUGFIX // The specified amount of real time has elapsed, more or less. // If timeout is zero, however, then real time is not taken into consideration // and the thread simply waits until notified. if (remaining < 1) break; try { syn = _synQueue.poll(remaining, TimeUnit.MILLISECONDS); // waits the specified time max } catch (InterruptedException ie) { ConnectException ce = new ConnectException("Interrupted accept()"); ce.initCause(ie); throw ce; } break; } } if (syn != null) { if (syn.getOptionalDelay() == PoisonPacket.POISON_MAX_DELAY_REQUEST) throw new ConnectException("ServerSocket closed"); // deal with forged / invalid syn packets in _manager.receiveConnection() // Handle both SYN and non-SYN packets in the queue if (syn.isFlagSet(Packet.FLAG_SYNCHRONIZE)) { // We are single-threaded here, so this is // a good place to check for dup SYNs and drop them Destination from = syn.getOptionalFrom(); if (from == null) { if (_log.shouldLog(Log.WARN)) _log.warn("Dropping SYN packet with no FROM: " + syn); // drop it continue; } Connection oldcon = _manager.getConnectionByOutboundId(syn.getReceiveStreamId()); if (oldcon != null) { // His ID not guaranteed to be unique to us, but probably is... // only drop it on a destination match too if (from.equals(oldcon.getRemotePeer())) { if (_log.shouldLog(Log.WARN)) _log.warn("Dropping dup SYN: " + syn); continue; } } Connection con = _manager.receiveConnection(syn); if (con != null) return con; } else { reReceivePacket(syn); // ... and keep looping } } // keep looping... } } /** * We found a non-SYN packet that was queued in the syn queue, * check to see if it has a home now, else drop it ... */ private void reReceivePacket(Packet packet) { Connection con = _manager.getConnectionByOutboundId(packet.getReceiveStreamId()); if (con != null) { // Send it through the packet handler again if (_log.shouldLog(Log.WARN)) _log.warn("Found con for queued non-syn packet: " + packet); // false -> don't requeue, fixes a race where a SYN gets dropped // between here and PacketHandler, causing the packet to loop forever.... _manager.getPacketHandler().receivePacketDirect(packet, false); } else { // log it here, just before we kill it - dest will be unknown if (I2PSocketManagerFull.pcapWriter != null && _context.getBooleanProperty(I2PSocketManagerFull.PROP_PCAP)) packet.logTCPDump(null); // goodbye if (_log.shouldLog(Log.WARN)) _log.warn("Did not find con for queued non-syn packet, dropping: " + packet); packet.releasePayload(); } } private void sendReset(Packet packet) { boolean ok = packet.verifySignature(_context, packet.getOptionalFrom(), null); if (!ok) { if (_log.shouldLog(Log.WARN)) _log.warn("Received a spoofed SYN packet: they said they were " + packet.getOptionalFrom()); return; } PacketLocal reply = new PacketLocal(_context, packet.getOptionalFrom(), packet.getSession()); reply.setFlag(Packet.FLAG_RESET); reply.setFlag(Packet.FLAG_SIGNATURE_INCLUDED); reply.setAckThrough(packet.getSequenceNum()); reply.setSendStreamId(packet.getReceiveStreamId()); reply.setReceiveStreamId(0); // TODO remove this someday, as of 0.9.20 we do not require it reply.setOptionalFrom(); if (_log.shouldLog(Log.DEBUG)) _log.debug("Sending RST: " + reply + " because of " + packet); // this just sends the packet - no retries or whatnot _manager.getPacketQueue().enqueue(reply); } private class TimeoutSyn implements SimpleTimer.TimedEvent { private final Packet _synPacket; public TimeoutSyn(Packet packet) { _synPacket = packet; } public void timeReached() { boolean removed = _synQueue.remove(_synPacket); if (removed) { if (_synPacket.isFlagSet(Packet.FLAG_SYNCHRONIZE)) { if (_log.shouldLog(Log.WARN)) _log.warn("Expired on the SYN queue: " + _synPacket); // timeout - send RST sendReset(_synPacket); } else { // non-syn packet got stranded on the syn queue, send it to the con reReceivePacket(_synPacket); } } else { // handled. noop } } } /** * Simple end-of-queue marker. * The standard class limits the delay to POISON_MAX_DELAY_REQUEST so * an evil user can't use this to shut us down */ private static class PoisonPacket extends Packet { public static final int POISON_MAX_DELAY_REQUEST = Packet.MAX_DELAY_REQUEST + 1; public PoisonPacket() { super(null); } @Override public int getOptionalDelay() { return POISON_MAX_DELAY_REQUEST; } @Override public String toString() { return "POISON"; } } }