package net.i2p.router.transport.udp;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.util.Arrays;
import net.i2p.router.RouterContext;
import net.i2p.router.transport.FIFOBandwidthLimiter;
import net.i2p.util.I2PThread;
import net.i2p.util.Log;
import net.i2p.util.SystemVersion;
/**
* Lowest level component to pull raw UDP datagrams off the wire as fast
* as possible, controlled by both the bandwidth limiter and the router's
* throttle. If the inbound queue gets too large or packets have been
* waiting around too long, they are dropped. Packets should be pulled off
* from the queue ASAP by a {@link PacketHandler}
*
* There is a UDPReceiver for each UDPEndpoint.
* It contains a thread but no queue. Received packets are queued
* in the common PacketHandler queue.
*/
class UDPReceiver {
private final RouterContext _context;
private final Log _log;
private final DatagramSocket _socket;
private String _name;
private volatile boolean _keepRunning;
private final Runner _runner;
private final UDPTransport _transport;
private final PacketHandler _handler;
private final SocketListener _endpoint;
private static final boolean _isAndroid = SystemVersion.isAndroid();
public UDPReceiver(RouterContext ctx, UDPTransport transport, DatagramSocket socket, String name,
SocketListener lsnr) {
_context = ctx;
_log = ctx.logManager().getLog(UDPReceiver.class);
_name = name;
_socket = socket;
_transport = transport;
_endpoint = lsnr;
_handler = transport.getPacketHandler();
if (_handler == null)
throw new IllegalStateException();
_runner = new Runner();
//_context.statManager().createRateStat("udp.receivePacketSize", "How large packets received are", "udp", UDPTransport.RATES);
//_context.statManager().createRateStat("udp.receiveRemaining", "How many packets are left sitting on the receiver's queue", "udp", UDPTransport.RATES);
//_context.statManager().createRateStat("udp.droppedInbound", "How many packet are queued up but not yet received when we drop", "udp", UDPTransport.RATES);
_context.statManager().createRateStat("udp.receiveHolePunch", "How often we receive a NAT hole punch", "udp", UDPTransport.RATES);
_context.statManager().createRateStat("udp.ignorePacketFromDroplist", "Packet lifetime for those dropped on the drop list", "udp", UDPTransport.RATES);
_context.statManager().createRateStat("udp.receiveFailsafe", "limiter stuck?", "udp", new long[] { 24*60*60*1000L });
}
/**
* Cannot be restarted (socket is final)
*/
public synchronized void startup() {
//adjustDropProbability();
_keepRunning = true;
I2PThread t = new I2PThread(_runner, _name, true);
t.start();
}
public synchronized void shutdown() {
_keepRunning = false;
}
/*********
private void adjustDropProbability() {
String p = _context.getProperty("i2np.udp.dropProbability");
if (p != null) {
try {
ARTIFICIAL_DROP_PROBABILITY = Integer.parseInt(p);
} catch (NumberFormatException nfe) {}
if (ARTIFICIAL_DROP_PROBABILITY < 0) ARTIFICIAL_DROP_PROBABILITY = 0;
} else {
//ARTIFICIAL_DROP_PROBABILITY = 0;
}
}
**********/
/**
* Replace the old listen port with the new one, returning the old.
* NOTE: this closes the old socket so that blocking calls unblock!
*
*/
/*********
public DatagramSocket updateListeningPort(DatagramSocket socket, int newPort) {
return _runner.updateListeningPort(socket, newPort);
}
**********/
/** if a packet been sitting in the queue for a full second (meaning the handlers are overwhelmed), drop subsequent packets */
private static final long MAX_QUEUE_PERIOD = 2*1000;
/*********
private static int ARTIFICIAL_DROP_PROBABILITY = 0; // 4
private static final int ARTIFICIAL_DELAY = 0; // 200;
private static final int ARTIFICIAL_DELAY_BASE = 0; //600;
**********/
/** @return zero (was queue size) */
private int receive(UDPPacket packet) {
/*********
//adjustDropProbability();
if (ARTIFICIAL_DROP_PROBABILITY > 0) {
// the first check is to let the compiler optimize away this
// random block on the live system when the probability is == 0
// (not if it isn't final jr)
int v = _context.random().nextInt(100);
if (v <= ARTIFICIAL_DROP_PROBABILITY) {
if (_log.shouldLog(Log.ERROR))
_log.error("Drop with v=" + v + " p=" + ARTIFICIAL_DROP_PROBABILITY + " packet size: " + packet.getPacket().getLength() + ": " + packet);
_context.statManager().addRateData("udp.droppedInboundProbabalistically", 1, 0);
return -1;
} else {
_context.statManager().addRateData("udp.acceptedInboundProbabalistically", 1, 0);
}
}
if ( (ARTIFICIAL_DELAY > 0) || (ARTIFICIAL_DELAY_BASE > 0) ) {
long delay = ARTIFICIAL_DELAY_BASE + _context.random().nextInt(ARTIFICIAL_DELAY);
if (_log.shouldLog(Log.INFO))
_log.info("Delay packet " + packet + " for " + delay);
SimpleTimer2.getInstance().addEvent(new ArtificiallyDelayedReceive(packet), delay);
return -1;
}
**********/
return doReceive(packet);
}
/**
* BLOCKING if queue between here and PacketHandler is full.
*
* @return zero (was queue size)
*/
private final int doReceive(UDPPacket packet) {
if (!_keepRunning)
return 0;
if (_log.shouldLog(Log.INFO))
_log.info("Received: " + packet);
RemoteHostId from = packet.getRemoteHost();
if (_transport.isInDropList(from)) {
if (_log.shouldLog(Log.INFO))
_log.info("Ignoring packet from the drop-listed peer: " + from);
_context.statManager().addRateData("udp.ignorePacketFromDroplist", packet.getLifetime());
packet.release();
return 0;
}
// drop anything apparently from our IP (any port)
if (Arrays.equals(from.getIP(), _transport.getExternalIP()) && !_transport.allowLocal()) {
if (_log.shouldLog(Log.WARN))
_log.warn("Dropping (spoofed?) packet from ourselves");
packet.release();
return 0;
}
/****
packet.enqueue();
boolean rejected = false;
int queueSize = 0;
long headPeriod = 0;
UDPPacket head = _inboundQueue.peek();
if (head != null) {
headPeriod = head.getLifetime();
if (headPeriod > MAX_QUEUE_PERIOD) {
rejected = true;
}
}
if (!rejected) {
****/
try {
_handler.queueReceived(packet);
} catch (InterruptedException ie) {
packet.release();
_keepRunning = false;
}
//return queueSize + 1;
return 0;
/****
}
// rejected
packet.release();
_context.statManager().addRateData("udp.droppedInbound", queueSize, headPeriod);
if (_log.shouldLog(Log.WARN)) {
queueSize = _inboundQueue.size();
StringBuilder msg = new StringBuilder();
msg.append("Dropping inbound packet with ");
msg.append(queueSize);
msg.append(" queued for ");
msg.append(headPeriod);
msg.append(" packet handlers: ").append(_transport.getPacketHandlerStatus());
_log.warn(msg.toString());
}
return queueSize;
****/
}
/****
private class ArtificiallyDelayedReceive implements SimpleTimer.TimedEvent {
private UDPPacket _packet;
public ArtificiallyDelayedReceive(UDPPacket packet) { _packet = packet; }
public void timeReached() { doReceive(_packet); }
}
****/
private class Runner implements Runnable {
//private volatile boolean _socketChanged;
public void run() {
//_socketChanged = false;
while (_keepRunning) {
//if (_socketChanged) {
// Thread.currentThread().setName(_name + "." + _id);
// _socketChanged = false;
//}
UDPPacket packet = UDPPacket.acquire(_context, true);
DatagramPacket dpacket = packet.getPacket();
// Android ICS bug
// http://code.google.com/p/android/issues/detail?id=24748
if (_isAndroid)
dpacket.setLength(UDPPacket.MAX_PACKET_SIZE);
// block before we read...
//if (_log.shouldLog(Log.DEBUG))
// _log.debug("Before throttling receive");
while (!_context.throttle().acceptNetworkMessage())
try { Thread.sleep(10); } catch (InterruptedException ie) {}
try {
//if (_log.shouldLog(Log.INFO))
// _log.info("Before blocking socket.receive on " + System.identityHashCode(packet));
//synchronized (Runner.this) {
_socket.receive(dpacket);
//}
int size = dpacket.getLength();
if (_log.shouldLog(Log.INFO))
_log.info("After blocking socket.receive: packet is " + size + " bytes on " + System.identityHashCode(packet));
packet.resetBegin();
// and block after we know how much we read but before
// we release the packet to the inbound queue
if (size >= UDPPacket.MAX_PACKET_SIZE) {
// DatagramSocket javadocs: If the message is longer than the packet's length, the message is truncated.
throw new IOException("packet too large! truncated and dropped from: " + packet.getRemoteHost());
}
if (_context.commSystem().isDummy()) {
// testing
packet.release();
} else if (size > 0) {
//FIFOBandwidthLimiter.Request req = _context.bandwidthLimiter().requestInbound(size, "UDP receiver");
//_context.bandwidthLimiter().requestInbound(req, size, "UDP receiver");
FIFOBandwidthLimiter.Request req =
_context.bandwidthLimiter().requestInbound(size, "UDP receiver");
// failsafe, don't wait forever
int waitCount = 0;
while (req.getPendingRequested() > 0 && waitCount++ < 5) {
req.waitForNextAllocation();
}
if (waitCount >= 5) {
// tell FBL we didn't receive it, but receive it anyway
req.abort();
_context.statManager().addRateData("udp.receiveFailsafe", 1);
}
receive(packet);
//_context.statManager().addRateData("udp.receivePacketSize", size);
} else {
_context.statManager().addRateData("udp.receiveHolePunch", 1);
// nat hole punch packets are 0 bytes
if (_log.shouldLog(Log.INFO))
_log.info("Received a 0 byte udp packet from " + dpacket.getAddress() + ":" + dpacket.getPort());
_transport.getEstablisher().receiveHolePunch(dpacket.getAddress(), dpacket.getPort());
packet.release();
}
} catch (IOException ioe) {
//if (_socketChanged) {
// if (_log.shouldLog(Log.INFO))
// _log.info("Changing ports...");
//} else {
if (_log.shouldLog(Log.WARN))
_log.warn("Error receiving", ioe);
//}
packet.release();
if (_socket.isClosed()) {
if (_keepRunning) {
_keepRunning = false;
_endpoint.fail();
}
} else if (_keepRunning) {
// TODO count consecutive errors, give up after too many?
try { Thread.sleep(100); } catch (InterruptedException ie) {}
}
}
}
if (_log.shouldLog(Log.WARN))
_log.warn("Stop receiving on " + _endpoint);
}
/******
public DatagramSocket updateListeningPort(DatagramSocket socket, int newPort) {
_name = "UDPReceive on " + newPort;
DatagramSocket old = null;
synchronized (Runner.this) {
old = _socket;
_socket = socket;
}
_socketChanged = true;
// ok, its switched, now lets break any blocking calls
old.close();
return old;
}
*****/
}
}