diff --git a/core/java/src/net/i2p/crypto/DHSessionKeyBuilder.java b/core/java/src/net/i2p/crypto/DHSessionKeyBuilder.java
index b3c5813f1..a96a0846a 100644
--- a/core/java/src/net/i2p/crypto/DHSessionKeyBuilder.java
+++ b/core/java/src/net/i2p/crypto/DHSessionKeyBuilder.java
@@ -247,6 +247,23 @@ public class DHSessionKeyBuilder {
if (_myPublicValue == null) _myPublicValue = generateMyValue();
return _myPublicValue;
}
+ /**
+ * Return a 256 byte representation of our public key, with leading 0s
+ * if necessary.
+ *
+ */
+ public byte[] getMyPublicValueBytes() {
+ BigInteger bi = getMyPublicValue();
+ byte data[] = bi.toByteArray();
+ byte rv[] = new byte[256];
+ if (data.length == 257) // high byte has the sign bit
+ System.arraycopy(data, 1, rv, 0, rv.length);
+ else if (data.length == 256)
+ System.arraycopy(data, 0, rv, 0, rv.length);
+ else
+ System.arraycopy(data, 0, rv, rv.length-data.length, data.length);
+ return rv;
+ }
/**
* Specify the value given by the peer for use in the session key negotiation
@@ -255,6 +272,20 @@ public class DHSessionKeyBuilder {
public void setPeerPublicValue(BigInteger peerVal) {
_peerValue = peerVal;
}
+ public void setPeerPublicValue(byte val[]) {
+ if (val.length != 256)
+ throw new IllegalArgumentException("Peer public value must be exactly 256 bytes");
+
+ if (1 == (val[0] & 0x80)) {
+ // high bit set, need to inject an additional byte to keep 2s complement
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("High bit set");
+ byte val2[] = new byte[257];
+ System.arraycopy(val, 0, val2, 1, 256);
+ val = val2;
+ }
+ _peerValue = new NativeBigInteger(val);
+ }
public BigInteger getPeerPublicValue() {
return _peerValue;
diff --git a/router/java/src/net/i2p/data/i2np/I2NPMessage.java b/router/java/src/net/i2p/data/i2np/I2NPMessage.java
index 59d7fe597..dd4325fb1 100644
--- a/router/java/src/net/i2p/data/i2np/I2NPMessage.java
+++ b/router/java/src/net/i2p/data/i2np/I2NPMessage.java
@@ -61,6 +61,7 @@ public interface I2NPMessage extends DataStructure {
* Replay resistent message Id
*/
public long getUniqueId();
+ public void setUniqueId(long id);
/**
* Date after which the message should be dropped (and the associated uniqueId forgotten)
@@ -72,7 +73,20 @@ public interface I2NPMessage extends DataStructure {
/** How large the message is, including any checksums */
public int getMessageSize();
+ /** How large the raw message is */
+ public int getRawMessageSize();
+
- /** write the message to the buffer, returning the number of bytes written */
+ /**
+ * write the message to the buffer, returning the number of bytes written.
+ * the data is formatted so as to be self contained, with the type, size,
+ * expiration, unique id, as well as a checksum bundled along.
+ */
public int toByteArray(byte buffer[]);
+ /**
+ * write the message to the buffer, returning the number of bytes written.
+ * the data is is not self contained - it does not include the size,
+ * unique id, or any checksum, but does include the type and expiration.
+ */
+ public int toRawByteArray(byte buffer[]);
}
diff --git a/router/java/src/net/i2p/data/i2np/I2NPMessageHandler.java b/router/java/src/net/i2p/data/i2np/I2NPMessageHandler.java
index 1f5d6bdfa..3ef157ec7 100644
--- a/router/java/src/net/i2p/data/i2np/I2NPMessageHandler.java
+++ b/router/java/src/net/i2p/data/i2np/I2NPMessageHandler.java
@@ -49,7 +49,7 @@ public class I2NPMessageHandler {
try {
int type = (int)DataHelper.readLong(in, 1);
_lastReadBegin = System.currentTimeMillis();
- I2NPMessage msg = createMessage(type);
+ I2NPMessage msg = I2NPMessageImpl.createMessage(_context, type);
if (msg == null)
throw new I2NPMessageException("The type "+ type + " is an unknown I2NP message");
try {
@@ -94,7 +94,7 @@ public class I2NPMessageHandler {
int type = (int)DataHelper.fromLong(data, cur, 1);
cur++;
_lastReadBegin = System.currentTimeMillis();
- I2NPMessage msg = createMessage(type);
+ I2NPMessage msg = I2NPMessageImpl.createMessage(_context, type);
if (msg == null)
throw new I2NPMessageException("The type "+ type + " is an unknown I2NP message");
try {
@@ -118,39 +118,6 @@ public class I2NPMessageHandler {
public long getLastReadTime() { return _lastReadEnd - _lastReadBegin; }
public int getLastSize() { return _lastSize; }
- /**
- * Yes, this is fairly ugly, but its the only place it ever happens.
- *
- */
- private I2NPMessage createMessage(int type) throws I2NPMessageException {
- switch (type) {
- case DatabaseStoreMessage.MESSAGE_TYPE:
- return new DatabaseStoreMessage(_context);
- case DatabaseLookupMessage.MESSAGE_TYPE:
- return new DatabaseLookupMessage(_context);
- case DatabaseSearchReplyMessage.MESSAGE_TYPE:
- return new DatabaseSearchReplyMessage(_context);
- case DeliveryStatusMessage.MESSAGE_TYPE:
- return new DeliveryStatusMessage(_context);
- case DateMessage.MESSAGE_TYPE:
- return new DateMessage(_context);
- case GarlicMessage.MESSAGE_TYPE:
- return new GarlicMessage(_context);
- case TunnelDataMessage.MESSAGE_TYPE:
- return new TunnelDataMessage(_context);
- case TunnelGatewayMessage.MESSAGE_TYPE:
- return new TunnelGatewayMessage(_context);
- case DataMessage.MESSAGE_TYPE:
- return new DataMessage(_context);
- case TunnelCreateMessage.MESSAGE_TYPE:
- return new TunnelCreateMessage(_context);
- case TunnelCreateStatusMessage.MESSAGE_TYPE:
- return new TunnelCreateStatusMessage(_context);
- default:
- return null;
- }
- }
-
public static void main(String args[]) {
try {
I2NPMessage msg = new I2NPMessageHandler(I2PAppContext.getGlobalContext()).readMessage(new FileInputStream(args[0]));
diff --git a/router/java/src/net/i2p/data/i2np/I2NPMessageImpl.java b/router/java/src/net/i2p/data/i2np/I2NPMessageImpl.java
index cea2087b9..2e6f09149 100644
--- a/router/java/src/net/i2p/data/i2np/I2NPMessageImpl.java
+++ b/router/java/src/net/i2p/data/i2np/I2NPMessageImpl.java
@@ -35,6 +35,8 @@ public abstract class I2NPMessageImpl extends DataStructureImpl implements I2NPM
public final static long DEFAULT_EXPIRATION_MS = 1*60*1000; // 1 minute by default
public final static int CHECKSUM_LENGTH = 1; //Hash.HASH_LENGTH;
+ private static final boolean RAW_FULL_SIZE = true;
+
public I2NPMessageImpl(I2PAppContext context) {
_context = context;
_log = context.logManager().getLog(I2NPMessageImpl.class);
@@ -165,7 +167,13 @@ public abstract class I2NPMessageImpl extends DataStructureImpl implements I2NPM
public void setMessageExpiration(long exp) { _expiration = exp; }
public synchronized int getMessageSize() {
- return calculateWrittenLength()+15 + CHECKSUM_LENGTH; // 47 bytes in the header
+ return calculateWrittenLength()+15 + CHECKSUM_LENGTH; // 16 bytes in the header
+ }
+ public synchronized int getRawMessageSize() {
+ if (RAW_FULL_SIZE)
+ return getMessageSize();
+ else
+ return calculateWrittenLength()+5;
}
public byte[] toByteArray() {
@@ -248,4 +256,83 @@ public abstract class I2NPMessageImpl extends DataStructureImpl implements I2NPM
return curIndex;
}
*/
+
+
+ public int toRawByteArray(byte buffer[]) {
+ if (RAW_FULL_SIZE)
+ return toByteArray(buffer);
+ try {
+ int off = 0;
+ DataHelper.toLong(buffer, off, 1, getType());
+ off += 1;
+ DataHelper.toLong(buffer, off, 4, _expiration/1000); // seconds
+ off += 4;
+ return writeMessageBody(buffer, off);
+ } catch (I2NPMessageException ime) {
+ _context.logManager().getLog(getClass()).log(Log.CRIT, "Error writing", ime);
+ throw new IllegalStateException("Unable to serialize the message (" + getClass().getName()
+ + "): " + ime.getMessage());
+ }
+ }
+
+ public static I2NPMessage fromRawByteArray(I2PAppContext ctx, byte buffer[], int offset, int len) throws I2NPMessageException {
+ int type = (int)DataHelper.fromLong(buffer, offset, 1);
+ offset++;
+ I2NPMessage msg = createMessage(ctx, type);
+ if (msg == null)
+ throw new I2NPMessageException("Unknown message type: " + type);
+ if (RAW_FULL_SIZE) {
+ try {
+ msg.readBytes(buffer, type, offset);
+ } catch (IOException ioe) {
+ throw new I2NPMessageException("Error reading the " + msg, ioe);
+ }
+ return msg;
+ }
+
+ long expiration = DataHelper.fromLong(buffer, offset, 4) * 1000; // seconds
+ offset += 4;
+ int dataSize = len - 1 - 4;
+ try {
+ msg.readMessage(buffer, offset, dataSize, type);
+ msg.setMessageExpiration(expiration);
+ return msg;
+ } catch (IOException ioe) {
+ throw new I2NPMessageException("IO error reading raw message", ioe);
+ }
+ }
+
+
+ /**
+ * Yes, this is fairly ugly, but its the only place it ever happens.
+ *
+ */
+ public static I2NPMessage createMessage(I2PAppContext context, int type) throws I2NPMessageException {
+ switch (type) {
+ case DatabaseStoreMessage.MESSAGE_TYPE:
+ return new DatabaseStoreMessage(context);
+ case DatabaseLookupMessage.MESSAGE_TYPE:
+ return new DatabaseLookupMessage(context);
+ case DatabaseSearchReplyMessage.MESSAGE_TYPE:
+ return new DatabaseSearchReplyMessage(context);
+ case DeliveryStatusMessage.MESSAGE_TYPE:
+ return new DeliveryStatusMessage(context);
+ case DateMessage.MESSAGE_TYPE:
+ return new DateMessage(context);
+ case GarlicMessage.MESSAGE_TYPE:
+ return new GarlicMessage(context);
+ case TunnelDataMessage.MESSAGE_TYPE:
+ return new TunnelDataMessage(context);
+ case TunnelGatewayMessage.MESSAGE_TYPE:
+ return new TunnelGatewayMessage(context);
+ case DataMessage.MESSAGE_TYPE:
+ return new DataMessage(context);
+ case TunnelCreateMessage.MESSAGE_TYPE:
+ return new TunnelCreateMessage(context);
+ case TunnelCreateStatusMessage.MESSAGE_TYPE:
+ return new TunnelCreateStatusMessage(context);
+ default:
+ return null;
+ }
+ }
}
diff --git a/router/java/src/net/i2p/router/transport/Transport.java b/router/java/src/net/i2p/router/transport/Transport.java
index 344e8427e..9f3ee4e53 100644
--- a/router/java/src/net/i2p/router/transport/Transport.java
+++ b/router/java/src/net/i2p/router/transport/Transport.java
@@ -8,6 +8,8 @@ package net.i2p.router.transport;
*
*/
+import java.io.IOException;
+import java.io.Writer;
import java.util.List;
import java.util.Set;
@@ -38,5 +40,5 @@ public interface Transport {
public int countActivePeers();
public List getMostRecentErrorMessages();
- public String renderStatusHTML();
+ public void renderStatusHTML(Writer out) throws IOException;
}
diff --git a/router/java/src/net/i2p/router/transport/TransportImpl.java b/router/java/src/net/i2p/router/transport/TransportImpl.java
index 147ad38d9..0da951571 100644
--- a/router/java/src/net/i2p/router/transport/TransportImpl.java
+++ b/router/java/src/net/i2p/router/transport/TransportImpl.java
@@ -8,6 +8,8 @@ package net.i2p.router.transport;
*
*/
+import java.io.IOException;
+import java.io.Writer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
@@ -365,7 +367,7 @@ public abstract class TransportImpl implements Transport {
/** Who to notify on message availability */
public void setListener(TransportEventListener listener) { _listener = listener; }
/** Make this stuff pretty (only used in the old console) */
- public String renderStatusHTML() { return null; }
+ public void renderStatusHTML(Writer out) throws IOException {}
public RouterContext getContext() { return _context; }
}
diff --git a/router/java/src/net/i2p/router/transport/TransportManager.java b/router/java/src/net/i2p/router/transport/TransportManager.java
index a03ea9c9e..eea745ae2 100644
--- a/router/java/src/net/i2p/router/transport/TransportManager.java
+++ b/router/java/src/net/i2p/router/transport/TransportManager.java
@@ -21,6 +21,7 @@ import net.i2p.data.i2np.I2NPMessage;
import net.i2p.router.OutNetMessage;
import net.i2p.router.RouterContext;
import net.i2p.router.transport.tcp.TCPTransport;
+import net.i2p.router.transport.udp.UDPTransport;
import net.i2p.util.Log;
public class TransportManager implements TransportEventListener {
@@ -29,6 +30,7 @@ public class TransportManager implements TransportEventListener {
private RouterContext _context;
private final static String PROP_DISABLE_TCP = "i2np.tcp.disable";
+ private static final boolean ENABLE_UDP = false;
public TransportManager(RouterContext context) {
_context = context;
@@ -57,6 +59,11 @@ public class TransportManager implements TransportEventListener {
t.setListener(this);
_transports.add(t);
}
+ if (ENABLE_UDP) {
+ UDPTransport udp = new UDPTransport(_context);
+ udp.setListener(this);
+ _transports.add(udp);
+ }
}
public void startListening() {
@@ -172,13 +179,15 @@ public class TransportManager implements TransportEventListener {
}
}
buf.append("\n");
+ out.write(buf.toString());
for (Iterator iter = _transports.iterator(); iter.hasNext(); ) {
Transport t = (Transport)iter.next();
- String str = t.renderStatusHTML();
- if (str != null)
- buf.append(str);
+ //String str = t.renderStatusHTML();
+ //if (str != null)
+ // buf.append(str);
+ t.renderStatusHTML(out);
}
- out.write(buf.toString());
+ //out.write(buf.toString());
out.flush();
}
}
diff --git a/router/java/src/net/i2p/router/transport/tcp/TCPTransport.java b/router/java/src/net/i2p/router/transport/tcp/TCPTransport.java
index 70130f32c..289312c92 100644
--- a/router/java/src/net/i2p/router/transport/tcp/TCPTransport.java
+++ b/router/java/src/net/i2p/router/transport/tcp/TCPTransport.java
@@ -1,5 +1,7 @@
package net.i2p.router.transport.tcp;
+import java.io.IOException;
+import java.io.Writer;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
@@ -766,7 +768,7 @@ public class TCPTransport extends TransportImpl {
}
/** Make this stuff pretty (only used in the old console) */
- public String renderStatusHTML() {
+ public void renderStatusHTML(Writer out) throws IOException {
StringBuffer buf = new StringBuffer(1024);
synchronized (_connectionLock) {
long offsetTotal = 0;
@@ -813,7 +815,7 @@ public class TCPTransport extends TransportImpl {
}
buf.append("");
- return buf.toString();
+ out.write(buf.toString());
}
/**
diff --git a/router/java/src/net/i2p/router/transport/udp/ACKSender.java b/router/java/src/net/i2p/router/transport/udp/ACKSender.java
new file mode 100644
index 000000000..8ec022e7c
--- /dev/null
+++ b/router/java/src/net/i2p/router/transport/udp/ACKSender.java
@@ -0,0 +1,44 @@
+package net.i2p.router.transport.udp;
+
+import java.util.List;
+
+import net.i2p.router.RouterContext;
+import net.i2p.util.Log;
+
+/**
+ * Blocking thread that pulls peers off the inboundFragment pool and
+ * sends them any outstanding ACKs. The logic of what peers get ACKed when
+ * is determined by the {@link InboundMessageFragments#getNextPeerToACK }
+ *
+ */
+public class ACKSender implements Runnable {
+ private RouterContext _context;
+ private Log _log;
+ private InboundMessageFragments _fragments;
+ private UDPTransport _transport;
+ private PacketBuilder _builder;
+
+ public ACKSender(RouterContext ctx, InboundMessageFragments fragments, UDPTransport transport) {
+ _context = ctx;
+ _log = ctx.logManager().getLog(ACKSender.class);
+ _fragments = fragments;
+ _transport = transport;
+ _builder = new PacketBuilder(_context, _transport);
+ }
+
+ public void run() {
+ while (_fragments.isAlive()) {
+ PeerState peer = _fragments.getNextPeerToACK();
+ if (peer != null) {
+ List acks = peer.retrieveACKs();
+ if ( (acks != null) && (acks.size() > 0) ) {
+ UDPPacket ack = _builder.buildACK(peer, acks);
+ if (_log.shouldLog(Log.INFO))
+ _log.info("Sending ACK for " + acks);
+ _transport.send(ack);
+ }
+ }
+ }
+ }
+
+}
diff --git a/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java b/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java
new file mode 100644
index 000000000..04654f6b2
--- /dev/null
+++ b/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java
@@ -0,0 +1,556 @@
+package net.i2p.router.transport.udp;
+
+import java.net.InetAddress;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import net.i2p.data.RouterAddress;
+import net.i2p.data.RouterIdentity;
+import net.i2p.data.SessionKey;
+import net.i2p.data.Signature;
+import net.i2p.data.i2np.DatabaseStoreMessage;
+import net.i2p.router.OutNetMessage;
+import net.i2p.router.RouterContext;
+import net.i2p.util.I2PThread;
+import net.i2p.util.Log;
+
+/**
+ * Coordinate the establishment of new sessions - both inbound and outbound.
+ * This has its own thread to add packets to the packet queue when necessary,
+ * as well as to drop any failed establishment attempts.
+ *
+ */
+public class EstablishmentManager {
+ private RouterContext _context;
+ private Log _log;
+ private UDPTransport _transport;
+ private PacketBuilder _builder;
+ /** map of host+port (String) to InboundEstablishState */
+ private Map _inboundStates;
+ /** map of host+port (String) to OutboundEstablishState */
+ private Map _outboundStates;
+ private boolean _alive;
+ private Object _activityLock;
+ private int _activity;
+
+ public EstablishmentManager(RouterContext ctx, UDPTransport transport) {
+ _context = ctx;
+ _log = ctx.logManager().getLog(EstablishmentManager.class);
+ _transport = transport;
+ _builder = new PacketBuilder(ctx, _transport);
+ _inboundStates = new HashMap(32);
+ _outboundStates = new HashMap(32);
+ _activityLock = new Object();
+ }
+
+ public void startup() {
+ _alive = true;
+ I2PThread t = new I2PThread(new Establisher(), "UDP Establisher");
+ t.setDaemon(true);
+ t.start();
+ }
+ public void shutdown() {
+ _alive = false;
+ notifyActivity();
+ }
+
+ /**
+ * Grab the active establishing state
+ */
+ InboundEstablishState getInboundState(InetAddress fromHost, int fromPort) {
+ String from = PeerState.calculateRemoteHostString(fromHost.getAddress(), fromPort);
+ synchronized (_inboundStates) {
+ InboundEstablishState state = (InboundEstablishState)_inboundStates.get(from);
+ if ( (state == null) && (_log.shouldLog(Log.DEBUG)) )
+ _log.debug("No inbound states for " + from + ", with remaining: " + _inboundStates);
+ return state;
+ }
+ }
+
+ OutboundEstablishState getOutboundState(InetAddress fromHost, int fromPort) {
+ String from = PeerState.calculateRemoteHostString(fromHost.getAddress(), fromPort);
+ synchronized (_outboundStates) {
+ OutboundEstablishState state = (OutboundEstablishState)_outboundStates.get(from);
+ if ( (state == null) && (_log.shouldLog(Log.DEBUG)) )
+ _log.debug("No outbound states for " + from + ", with remaining: " + _outboundStates);
+ return state;
+ }
+ }
+
+ /**
+ * Send the message to its specified recipient by establishing a connection
+ * with them and sending it off. This call does not block, and on failure,
+ * the message is failed.
+ *
+ */
+ public void establish(OutNetMessage msg) {
+ RouterAddress ra = msg.getTarget().getTargetAddress(_transport.getStyle());
+ if (ra == null) {
+ _transport.failed(msg);
+ return;
+ }
+ UDPAddress addr = new UDPAddress(ra);
+ InetAddress remAddr = addr.getHostAddress();
+ int port = addr.getPort();
+ String to = PeerState.calculateRemoteHostString(remAddr.getAddress(), port);
+
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("Add outobund establish state to: " + to);
+
+ synchronized (_outboundStates) {
+ OutboundEstablishState state = (OutboundEstablishState)_outboundStates.get(to);
+ if (state == null) {
+ state = new OutboundEstablishState(_context, remAddr, port,
+ msg.getTarget().getIdentity(),
+ new SessionKey(addr.getIntroKey()));
+ _outboundStates.put(to, state);
+ }
+ state.addMessage(msg);
+ }
+
+ notifyActivity();
+ }
+
+ /**
+ * Got a SessionRequest (initiates an inbound establishment)
+ *
+ */
+ void receiveSessionRequest(String from, InetAddress host, int port, UDPPacketReader reader) {
+ InboundEstablishState state = null;
+ synchronized (_inboundStates) {
+ state = (InboundEstablishState)_inboundStates.get(from);
+ if (state == null) {
+ state = new InboundEstablishState(_context, host, port, _transport.getLocalPort());
+ _inboundStates.put(from, state);
+ }
+ }
+ state.receiveSessionRequest(reader.getSessionRequestReader());
+
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("Receive session request from: " + state.getRemoteHostInfo());
+
+ notifyActivity();
+ }
+
+ /**
+ * got a SessionConfirmed (should only happen as part of an inbound
+ * establishment)
+ */
+ void receiveSessionConfirmed(String from, UDPPacketReader reader) {
+ InboundEstablishState state = null;
+ synchronized (_inboundStates) {
+ state = (InboundEstablishState)_inboundStates.get(from);
+ }
+ if (state != null) {
+ state.receiveSessionConfirmed(reader.getSessionConfirmedReader());
+ notifyActivity();
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("Receive session confirmed from: " + state.getRemoteHostInfo());
+ }
+ }
+
+ /**
+ * Got a SessionCreated (in response to our outbound SessionRequest)
+ *
+ */
+ void receiveSessionCreated(String from, UDPPacketReader reader) {
+ OutboundEstablishState state = null;
+ synchronized (_outboundStates) {
+ state = (OutboundEstablishState)_outboundStates.get(from);
+ }
+ if (state != null) {
+ state.receiveSessionCreated(reader.getSessionCreatedReader());
+ notifyActivity();
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("Receive session created from: " + state.getRemoteHostInfo());
+ }
+ }
+
+ /**
+ * A data packet arrived on an outbound connection being established, which
+ * means its complete (yay!). This is a blocking call, more than I'd like...
+ *
+ */
+ PeerState receiveData(OutboundEstablishState state) {
+ state.dataReceived();
+ synchronized (_outboundStates) {
+ _outboundStates.remove(state.getRemoteHostInfo());
+ }
+ if (_log.shouldLog(Log.INFO))
+ _log.info("Outbound established completely! yay");
+ PeerState peer = handleCompletelyEstablished(state);
+ notifyActivity();
+ return peer;
+ }
+
+
+ private void notifyActivity() {
+ synchronized (_activityLock) {
+ _activity++;
+ _activityLock.notifyAll();
+ }
+ }
+
+ /** kill any inbound or outbound that takes more than 30s */
+ private static final int MAX_ESTABLISH_TIME = 30*1000;
+
+ /**
+ * ok, fully received, add it to the established cons and queue up a
+ * netDb store to them
+ *
+ */
+ private void handleCompletelyEstablished(InboundEstablishState state) {
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("Handle completely established (inbound): " + state.getRemoteHostInfo());
+ long now = _context.clock().now();
+ RouterIdentity remote = state.getConfirmedIdentity();
+ PeerState peer = new PeerState(_context);
+ peer.setCurrentCipherKey(state.getCipherKey());
+ peer.setCurrentMACKey(state.getMACKey());
+ peer.setCurrentReceiveSecond(now - (now % 1000));
+ peer.setKeyEstablishedTime(now);
+ peer.setLastReceiveTime(now);
+ peer.setLastSendTime(now);
+ peer.setRemoteAddress(state.getSentIP(), state.getSentPort());
+ peer.setRemotePeer(remote.calculateHash());
+ if (true) // for now, only support direct
+ peer.setRemoteRequiresIntroduction(false);
+ peer.setTheyRelayToUsAs(0);
+ peer.setWeRelayToThemAs(state.getSentRelayTag());
+
+ _transport.addRemotePeerState(peer);
+
+ sendOurInfo(peer);
+ }
+
+ /**
+ * ok, fully received, add it to the established cons and send any
+ * queued messages
+ *
+ */
+ private PeerState handleCompletelyEstablished(OutboundEstablishState state) {
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("Handle completely established (outbound): " + state.getRemoteHostInfo());
+ long now = _context.clock().now();
+ RouterIdentity remote = state.getRemoteIdentity();
+ PeerState peer = new PeerState(_context);
+ peer.setCurrentCipherKey(state.getCipherKey());
+ peer.setCurrentMACKey(state.getMACKey());
+ peer.setCurrentReceiveSecond(now - (now % 1000));
+ peer.setKeyEstablishedTime(now);
+ peer.setLastReceiveTime(now);
+ peer.setLastSendTime(now);
+ peer.setRemoteAddress(state.getSentIP(), state.getSentPort());
+ peer.setRemotePeer(remote.calculateHash());
+ if (true) // for now, only support direct
+ peer.setRemoteRequiresIntroduction(false);
+ peer.setTheyRelayToUsAs(state.getReceivedRelayTag());
+ peer.setWeRelayToThemAs(0);
+
+ _transport.addRemotePeerState(peer);
+
+ sendOurInfo(peer);
+
+ while (true) {
+ OutNetMessage msg = state.getNextQueuedMessage();
+ if (msg == null)
+ break;
+ _transport.send(msg);
+ }
+ return peer;
+ }
+
+ private void sendOurInfo(PeerState peer) {
+ if (_log.shouldLog(Log.INFO))
+ _log.info("Publishing to the peer after confirm: " + peer);
+
+ DatabaseStoreMessage m = new DatabaseStoreMessage(_context);
+ m.setKey(_context.routerHash());
+ m.setRouterInfo(_context.router().getRouterInfo());
+ m.setMessageExpiration(_context.clock().now() + 10*1000);
+ _transport.send(m, peer);
+ }
+
+ private void sendCreated(InboundEstablishState state) {
+ long now = _context.clock().now();
+ if (true) // for now, don't offer to relay
+ state.setSentRelayTag(0);
+
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("Send created to: " + state.getRemoteHostInfo());
+
+ state.generateSessionKey();
+ _transport.send(_builder.buildSessionCreatedPacket(state));
+ // if they haven't advanced to sending us confirmed packets in 5s,
+ // repeat
+ state.setNextSendTime(now + 5*1000);
+ }
+
+ private void sendRequest(OutboundEstablishState state) {
+ long now = _context.clock().now();
+ state.prepareSessionRequest();
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("Send request to: " + state.getRemoteHostInfo());
+ _transport.send(_builder.buildSessionRequestPacket(state));
+ state.requestSent();
+ }
+
+ private void sendConfirmation(OutboundEstablishState state) {
+ long now = _context.clock().now();
+ boolean valid = state.validateSessionCreated();
+ if (!valid) // validate clears fields on failure
+ return;
+
+ // gives us the opportunity to "detect" our external addr
+ _transport.externalAddressReceived(state.getReceivedIP(), state.getReceivedPort());
+
+ // signs if we havent signed yet
+ state.prepareSessionConfirmed();
+
+ UDPPacket packets[] = _builder.buildSessionConfirmedPackets(state);
+
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("Send confirm to: " + state.getRemoteHostInfo());
+
+ for (int i = 0; i < packets.length; i++)
+ _transport.send(packets[i]);
+
+ state.confirmedPacketsSent();
+ }
+
+
+ /**
+ * Drive through the inbound establishment states, adjusting one of them
+ * as necessary
+ */
+ private long handleInbound() {
+ long now = _context.clock().now();
+ long nextSendTime = -1;
+ InboundEstablishState inboundState = null;
+ synchronized (_inboundStates) {
+ //if (_log.shouldLog(Log.DEBUG))
+ // _log.debug("# inbound states: " + _inboundStates.size());
+ for (Iterator iter = _inboundStates.values().iterator(); iter.hasNext(); ) {
+ InboundEstablishState cur = (InboundEstablishState)iter.next();
+ if (cur.getState() == InboundEstablishState.STATE_CONFIRMED_COMPLETELY) {
+ // completely received (though the signature may be invalid)
+ iter.remove();
+ inboundState = cur;
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("Removing completely confirmed inbound state");
+ break;
+ } else if (cur.getLifetime() > MAX_ESTABLISH_TIME) {
+ // took too long, fuck 'em
+ iter.remove();
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("Removing expired inbound state");
+ } else {
+ if (cur.getNextSendTime() <= now) {
+ // our turn...
+ inboundState = cur;
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("Processing inbound that wanted activity");
+ break;
+ } else {
+ // nothin to do but wait for them to send us
+ // stuff, so lets move on to the next one being
+ // established
+ long when = -1;
+ if (cur.getNextSendTime() <= 0) {
+ when = cur.getEstablishBeginTime() + MAX_ESTABLISH_TIME;
+ } else {
+ when = cur.getNextSendTime();
+ }
+ if (when < nextSendTime)
+ nextSendTime = when;
+ }
+ }
+ }
+ }
+
+ if (inboundState != null) {
+ //if (_log.shouldLog(Log.DEBUG))
+ // _log.debug("Processing for inbound: " + inboundState);
+ switch (inboundState.getState()) {
+ case InboundEstablishState.STATE_REQUEST_RECEIVED:
+ sendCreated(inboundState);
+ break;
+ case InboundEstablishState.STATE_CREATED_SENT: // fallthrough
+ case InboundEstablishState.STATE_CONFIRMED_PARTIALLY:
+ // if its been 5s since we sent the SessionCreated, resend
+ if (inboundState.getNextSendTime() <= now)
+ sendCreated(inboundState);
+ break;
+ case InboundEstablishState.STATE_CONFIRMED_COMPLETELY:
+ if (inboundState.getConfirmedIdentity() != null) {
+ handleCompletelyEstablished(inboundState);
+ break;
+ } else {
+ if (_log.shouldLog(Log.WARN))
+ _log.warn("why are we confirmed with no identity? " + inboundState);
+ break;
+ }
+ case InboundEstablishState.STATE_UNKNOWN: // fallthrough
+ default:
+ // wtf
+ if (_log.shouldLog(Log.ERROR))
+ _log.error("hrm, state is unknown for " + inboundState);
+ }
+
+ // ok, since there was something to do, we want to loop again
+ nextSendTime = now;
+ }
+
+ return nextSendTime;
+ }
+
+
+ /**
+ * Drive through the outbound establishment states, adjusting one of them
+ * as necessary
+ */
+ private long handleOutbound() {
+ long now = _context.clock().now();
+ long nextSendTime = -1;
+ OutboundEstablishState outboundState = null;
+ synchronized (_outboundStates) {
+ //if (_log.shouldLog(Log.DEBUG))
+ // _log.debug("# outbound states: " + _outboundStates.size());
+ for (Iterator iter = _outboundStates.values().iterator(); iter.hasNext(); ) {
+ OutboundEstablishState cur = (OutboundEstablishState)iter.next();
+ if (cur.getState() == OutboundEstablishState.STATE_CONFIRMED_COMPLETELY) {
+ // completely received
+ iter.remove();
+ outboundState = cur;
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("Removing confirmed outbound: " + cur);
+ break;
+ } else if (cur.getLifetime() > MAX_ESTABLISH_TIME) {
+ // took too long, fuck 'em
+ iter.remove();
+ outboundState = cur;
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("Removing expired outbound: " + cur);
+ break;
+ } else {
+ if (cur.getNextSendTime() <= now) {
+ // our turn...
+ outboundState = cur;
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("Outbound wants activity: " + cur);
+ break;
+ } else {
+ // nothin to do but wait for them to send us
+ // stuff, so lets move on to the next one being
+ // established
+ long when = -1;
+ if (cur.getNextSendTime() <= 0) {
+ when = cur.getEstablishBeginTime() + MAX_ESTABLISH_TIME;
+ } else {
+ when = cur.getNextSendTime();
+ }
+ if ( (nextSendTime <= 0) || (when < nextSendTime) )
+ nextSendTime = when;
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("Outbound doesn't want activity: " + cur + " (next=" + (when-now) + ")");
+ }
+ }
+ }
+ }
+
+ if (outboundState != null) {
+ if (outboundState.getLifetime() > MAX_ESTABLISH_TIME) {
+ if (outboundState.getState() != OutboundEstablishState.STATE_CONFIRMED_COMPLETELY) {
+ while (true) {
+ OutNetMessage msg = outboundState.getNextQueuedMessage();
+ if (msg == null)
+ break;
+ _transport.failed(msg);
+ }
+ _context.shitlist().shitlistRouter(outboundState.getRemoteIdentity().calculateHash(), "Unable to establish");
+ } else {
+ while (true) {
+ OutNetMessage msg = outboundState.getNextQueuedMessage();
+ if (msg == null)
+ break;
+ _transport.send(msg);
+ }
+ }
+ } else {
+ switch (outboundState.getState()) {
+ case OutboundEstablishState.STATE_UNKNOWN:
+ sendRequest(outboundState);
+ break;
+ case OutboundEstablishState.STATE_REQUEST_SENT:
+ // no response yet (or it was invalid), lets retry
+ if (outboundState.getNextSendTime() <= now)
+ sendRequest(outboundState);
+ break;
+ case OutboundEstablishState.STATE_CREATED_RECEIVED: // fallthrough
+ case OutboundEstablishState.STATE_CONFIRMED_PARTIALLY:
+ if (outboundState.getNextSendTime() <= now)
+ sendConfirmation(outboundState);
+ break;
+ case OutboundEstablishState.STATE_CONFIRMED_COMPLETELY:
+ handleCompletelyEstablished(outboundState);
+ break;
+ default:
+ // wtf
+ }
+ }
+
+ //if (_log.shouldLog(Log.DEBUG))
+ // _log.debug("Since something happened outbound, next=now");
+ // ok, since there was something to do, we want to loop again
+ nextSendTime = now;
+ } else {
+ //if (_log.shouldLog(Log.DEBUG))
+ // _log.debug("Nothing happened outbound, next is in " + (nextSendTime-now));
+ }
+
+ return nextSendTime;
+ }
+
+ /**
+ * Driving thread, processing up to one step for an inbound peer and up to
+ * one step for an outbound peer. This is prodded whenever any peer's state
+ * changes as well.
+ *
+ */
+ private class Establisher implements Runnable {
+ public void run() {
+ while (_alive) {
+ _activity = 0;
+ long now = _context.clock().now();
+ long nextSendTime = -1;
+ long nextSendInbound = handleInbound();
+ long nextSendOutbound = handleOutbound();
+ if (nextSendInbound > 0)
+ nextSendTime = nextSendInbound;
+ if ( (nextSendTime < 0) || (nextSendOutbound < nextSendTime) )
+ nextSendTime = nextSendOutbound;
+
+ long delay = nextSendTime - now;
+ if ( (nextSendTime == -1) || (delay > 0) ) {
+ boolean interrupted = false;
+ try {
+ synchronized (_activityLock) {
+ if (_activity > 0)
+ continue;
+ if (nextSendTime == -1)
+ _activityLock.wait();
+ else
+ _activityLock.wait(delay);
+ }
+ } catch (InterruptedException ie) {
+ interrupted = true;
+ }
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("After waiting w/ nextSend=" + nextSendTime
+ + " and delay=" + delay + " and interrupted=" + interrupted);
+ }
+ }
+ }
+ }
+}
diff --git a/router/java/src/net/i2p/router/transport/udp/InboundEstablishState.java b/router/java/src/net/i2p/router/transport/udp/InboundEstablishState.java
new file mode 100644
index 000000000..d24097031
--- /dev/null
+++ b/router/java/src/net/i2p/router/transport/udp/InboundEstablishState.java
@@ -0,0 +1,308 @@
+package net.i2p.router.transport.udp;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.List;
+
+import net.i2p.crypto.DHSessionKeyBuilder;
+import net.i2p.data.Base64;
+import net.i2p.data.ByteArray;
+import net.i2p.data.DataFormatException;
+import net.i2p.data.DataHelper;
+import net.i2p.data.RouterIdentity;
+import net.i2p.data.SessionKey;
+import net.i2p.data.Signature;
+import net.i2p.router.OutNetMessage;
+import net.i2p.router.RouterContext;
+import net.i2p.util.Log;
+
+/**
+ * Data for a new connection being established, where the remote peer has
+ * initiated the connection with us. In other words, they are Alice and
+ * we are Bob.
+ *
+ */
+public class InboundEstablishState {
+ private RouterContext _context;
+ private Log _log;
+ // SessionRequest message
+ private byte _receivedX[];
+ private byte _bobIP[];
+ private int _bobPort;
+ private DHSessionKeyBuilder _keyBuilder;
+ // SessionCreated message
+ private byte _sentY[];
+ private byte _aliceIP[];
+ private int _alicePort;
+ private long _sentRelayTag;
+ private long _sentSignedOnTime;
+ private SessionKey _sessionKey;
+ private SessionKey _macKey;
+ private Signature _sentSignature;
+ // SessionConfirmed messages
+ private byte _receivedIdentity[][];
+ private long _receivedSignedOnTime;
+ private byte _receivedSignature[];
+ private boolean _verificationAttempted;
+ private RouterIdentity _receivedConfirmedIdentity;
+ // general status
+ private long _establishBegin;
+ private long _lastReceive;
+ private long _lastSend;
+ private long _nextSend;
+ private String _remoteHostInfo;
+ private int _currentState;
+
+ /** nothin known yet */
+ public static final int STATE_UNKNOWN = 0;
+ /** we have received an initial request */
+ public static final int STATE_REQUEST_RECEIVED = 1;
+ /** we have sent a signed creation packet */
+ public static final int STATE_CREATED_SENT = 2;
+ /** we have received one or more confirmation packets */
+ public static final int STATE_CONFIRMED_PARTIALLY = 3;
+ /** we have completely received all of the confirmation packets */
+ public static final int STATE_CONFIRMED_COMPLETELY = 4;
+
+ public InboundEstablishState(RouterContext ctx, InetAddress remoteHost, int remotePort, int localPort) {
+ _context = ctx;
+ _log = ctx.logManager().getLog(InboundEstablishState.class);
+ _aliceIP = remoteHost.getAddress();
+ _alicePort = remotePort;
+ _remoteHostInfo = PeerState.calculateRemoteHostString(_aliceIP, _alicePort);
+ _bobPort = localPort;
+ _keyBuilder = null;
+ _verificationAttempted = false;
+ _currentState = STATE_UNKNOWN;
+ _establishBegin = ctx.clock().now();
+ }
+
+ public synchronized int getState() { return _currentState; }
+
+ public synchronized void receiveSessionRequest(UDPPacketReader.SessionRequestReader req) {
+ if (_receivedX == null)
+ _receivedX = new byte[UDPPacketReader.SessionRequestReader.X_LENGTH];
+ req.readX(_receivedX, 0);
+ if (_bobIP == null)
+ _bobIP = new byte[req.readIPSize()];
+ req.readIP(_bobIP, 0);
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("Receive sessionRequest, BobIP = " + Base64.encode(_bobIP));
+ if (_currentState == STATE_UNKNOWN)
+ _currentState = STATE_REQUEST_RECEIVED;
+ packetReceived();
+ }
+
+ public synchronized boolean sessionRequestReceived() { return _receivedX != null; }
+ public synchronized byte[] getReceivedX() { return _receivedX; }
+ public synchronized byte[] getReceivedOurIP() { return _bobIP; }
+
+ public synchronized void generateSessionKey() {
+ if (_sessionKey != null) return;
+ _keyBuilder = new DHSessionKeyBuilder();
+ _keyBuilder.setPeerPublicValue(_receivedX);
+ _sessionKey = _keyBuilder.getSessionKey();
+ ByteArray extra = _keyBuilder.getExtraBytes();
+ _macKey = new SessionKey(new byte[SessionKey.KEYSIZE_BYTES]);
+ System.arraycopy(extra.getData(), 0, _macKey.getData(), 0, SessionKey.KEYSIZE_BYTES);
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("Established inbound keys. cipher: " + Base64.encode(_sessionKey.getData())
+ + " mac: " + Base64.encode(_macKey.getData()));
+ }
+
+ public synchronized SessionKey getCipherKey() { return _sessionKey; }
+ public synchronized SessionKey getMACKey() { return _macKey; }
+
+ /** what IP do they appear to be on? */
+ public synchronized byte[] getSentIP() { return _aliceIP; }
+ /** what port number do they appear to be coming from? */
+ public synchronized int getSentPort() { return _alicePort; }
+
+ public synchronized byte[] getSentY() {
+ if (_sentY == null)
+ _sentY = _keyBuilder.getMyPublicValueBytes();
+ return _sentY;
+ }
+
+ public synchronized long getSentRelayTag() { return _sentRelayTag; }
+ public synchronized void setSentRelayTag(long tag) { _sentRelayTag = tag; }
+ public synchronized long getSentSignedOnTime() { return _sentSignedOnTime; }
+
+ public synchronized void prepareSessionCreated() {
+ if (_sentSignature == null) signSessionCreated();
+ }
+
+ public synchronized Signature getSentSignature() { return _sentSignature; }
+
+ /**
+ * Sign: Alice's IP + Alice's port + Bob's IP + Bob's port + Alice's
+ * new relay tag + Bob's signed on time
+ */
+ private void signSessionCreated() {
+ byte signed[] = new byte[_aliceIP.length + 2
+ + _bobIP.length + 2
+ + 4 // sent relay tag
+ + 4 // signed on time
+ ];
+ _sentSignedOnTime = _context.clock().now() / 1000;
+
+ int off = 0;
+ System.arraycopy(_aliceIP, 0, signed, off, _aliceIP.length);
+ off += _aliceIP.length;
+ DataHelper.toLong(signed, off, 2, _alicePort);
+ off += 2;
+ System.arraycopy(_bobIP, 0, signed, off, _bobIP.length);
+ off += _bobIP.length;
+ DataHelper.toLong(signed, off, 2, _bobPort);
+ off += 2;
+ DataHelper.toLong(signed, off, 4, _sentRelayTag);
+ off += 4;
+ DataHelper.toLong(signed, off, 4, _sentSignedOnTime);
+
+ _sentSignature = _context.dsa().sign(signed, _context.keyManager().getSigningPrivateKey());
+
+ if (_log.shouldLog(Log.DEBUG)) {
+ StringBuffer buf = new StringBuffer(128);
+ buf.append("Signing sessionCreated:");
+ buf.append(" AliceIP: ").append(Base64.encode(_aliceIP));
+ buf.append(" AlicePort: ").append(_alicePort);
+ buf.append(" BobIP: ").append(Base64.encode(_bobIP));
+ buf.append(" BobPort: ").append(_bobPort);
+ buf.append(" RelayTag: ").append(_sentRelayTag);
+ buf.append(" SignedOn: ").append(_sentSignedOnTime);
+ buf.append(" signature: ").append(Base64.encode(_sentSignature.getData()));
+ _log.debug(buf.toString());
+ }
+ }
+
+ /** note that we just sent a SessionCreated packet */
+ public synchronized void createdPacketSent() {
+ _lastSend = _context.clock().now();
+ if ( (_currentState == STATE_UNKNOWN) || (_currentState == STATE_REQUEST_RECEIVED) )
+ _currentState = STATE_CREATED_SENT;
+ }
+
+ /** how long have we been trying to establish this session? */
+ public synchronized long getLifetime() { return _context.clock().now() - _establishBegin; }
+ public synchronized long getEstablishBeginTime() { return _establishBegin; }
+ public synchronized long getNextSendTime() { return _nextSend; }
+ public synchronized void setNextSendTime(long when) { _nextSend = when; }
+
+ /** host+port, uniquely identifies an attempt */
+ public String getRemoteHostInfo() { return _remoteHostInfo; }
+
+ public synchronized void receiveSessionConfirmed(UDPPacketReader.SessionConfirmedReader conf) {
+ if (_receivedIdentity == null)
+ _receivedIdentity = new byte[conf.readTotalFragmentNum()][];
+ int cur = conf.readCurrentFragmentNum();
+ if (_receivedIdentity[cur] == null) {
+ byte fragment[] = new byte[conf.readCurrentFragmentSize()];
+ conf.readFragmentData(fragment, 0);
+ _receivedIdentity[cur] = fragment;
+ }
+
+ if (cur == _receivedIdentity.length-1) {
+ _receivedSignedOnTime = conf.readFinalFragmentSignedOnTime();
+ if (_receivedSignature == null)
+ _receivedSignature = new byte[Signature.SIGNATURE_BYTES];
+ conf.readFinalSignature(_receivedSignature, 0);
+ }
+
+ if ( (_currentState == STATE_UNKNOWN) ||
+ (_currentState == STATE_REQUEST_RECEIVED) ||
+ (_currentState == STATE_CREATED_SENT) ) {
+ if (confirmedFullyReceived())
+ _currentState = STATE_CONFIRMED_COMPLETELY;
+ else
+ _currentState = STATE_CONFIRMED_PARTIALLY;
+ }
+
+ packetReceived();
+ }
+
+ /** have we fully received the SessionConfirmed messages from Alice? */
+ public synchronized boolean confirmedFullyReceived() {
+ if (_receivedIdentity != null) {
+ for (int i = 0; i < _receivedIdentity.length; i++)
+ if (_receivedIdentity[i] == null)
+ return false;
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Who is Alice (null if forged/unknown)
+ */
+ public synchronized RouterIdentity getConfirmedIdentity() {
+ if (!_verificationAttempted) {
+ verifyIdentity();
+ _verificationAttempted = true;
+ }
+ return _receivedConfirmedIdentity;
+ }
+
+ /**
+ * Determine if Alice sent us a valid confirmation packet. The
+ * identity signs: Alice's IP + Alice's port + Bob's IP + Bob's port
+ * + Alice's new relay key + Alice's signed on time
+ */
+ private synchronized void verifyIdentity() {
+ int identSize = 0;
+ for (int i = 0; i < _receivedIdentity.length; i++)
+ identSize += _receivedIdentity[i].length;
+ byte ident[] = new byte[identSize];
+ int off = 0;
+ for (int i = 0; i < _receivedIdentity.length; i++) {
+ int len = _receivedIdentity[i].length;
+ System.arraycopy(_receivedIdentity[i], 0, ident, off, len);
+ off += len;
+ }
+ ByteArrayInputStream in = new ByteArrayInputStream(ident);
+ RouterIdentity peer = new RouterIdentity();
+ try {
+ peer.readBytes(in);
+
+ byte signed[] = new byte[_aliceIP.length + 2
+ + _bobIP.length + 2
+ + 4 // Alice's relay key
+ + 4 // signed on time
+ ];
+
+ off = 0;
+ System.arraycopy(_aliceIP, 0, signed, off, _aliceIP.length);
+ off += _aliceIP.length;
+ DataHelper.toLong(signed, off, 2, _alicePort);
+ off += 2;
+ System.arraycopy(_bobIP, 0, signed, off, _bobIP.length);
+ off += _bobIP.length;
+ DataHelper.toLong(signed, off, 2, _bobPort);
+ off += 2;
+ DataHelper.toLong(signed, off, 4, _sentRelayTag);
+ off += 4;
+ DataHelper.toLong(signed, off, 4, _receivedSignedOnTime);
+ Signature sig = new Signature(_receivedSignature);
+ boolean ok = _context.dsa().verifySignature(sig, signed, peer.getSigningPublicKey());
+ if (ok) {
+ _receivedConfirmedIdentity = peer;
+ } else {
+ if (_log.shouldLog(Log.WARN))
+ _log.warn("Signature failed from " + peer);
+ }
+ } catch (DataFormatException dfe) {
+ if (_log.shouldLog(Log.WARN))
+ _log.warn("Improperly formatted yet fully received ident", dfe);
+ } catch (IOException ioe) {
+ if (_log.shouldLog(Log.WARN))
+ _log.warn("Improperly formatted yet fully received ident", ioe);
+ }
+ }
+
+ private void packetReceived() {
+ _lastReceive = _context.clock().now();
+ _nextSend = _lastReceive;
+ }
+}
diff --git a/router/java/src/net/i2p/router/transport/udp/InboundMessageFragments.java b/router/java/src/net/i2p/router/transport/udp/InboundMessageFragments.java
new file mode 100644
index 000000000..1738e3da9
--- /dev/null
+++ b/router/java/src/net/i2p/router/transport/udp/InboundMessageFragments.java
@@ -0,0 +1,228 @@
+package net.i2p.router.transport.udp;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import net.i2p.router.RouterContext;
+import net.i2p.util.I2PThread;
+import net.i2p.util.Log;
+
+/**
+ * Organize the received data message fragments, allowing its
+ * {@link MessageReceiver} to pull off completed messages and its
+ * {@link ACKSender} to pull off peers who need to receive an ACK for
+ * these messages. In addition, it drops failed fragments and keeps a
+ * minimal list of the most recently completed messages (even though higher
+ * up in the router we have full blown replay detection, its nice to have a
+ * basic line of defense here)
+ *
+ */
+public class InboundMessageFragments {
+ private RouterContext _context;
+ private Log _log;
+ /** Map of peer (Hash) to a Map of messageId (Long) to InboundMessageState objects */
+ private Map _inboundMessages;
+ /** list of peers (PeerState) who we have received data from but not yet ACKed to */
+ private List _unsentACKs;
+ /** list of messages (InboundMessageState) fully received but not interpreted yet */
+ private List _completeMessages;
+ /** list of message IDs (Long) recently received, so we can ignore in flight dups */
+ private List _recentlyCompletedMessages;
+ private OutboundMessageFragments _outbound;
+ private UDPTransport _transport;
+ /** this can be broken down further, but to start, OneBigLock does the trick */
+ private Object _stateLock;
+ private boolean _alive;
+
+ private static final int RECENTLY_COMPLETED_SIZE = 100;
+ /** how frequently do we want to send ACKs to a peer? */
+ private static final int ACK_FREQUENCY = 100;
+
+ public InboundMessageFragments(RouterContext ctx, OutboundMessageFragments outbound, UDPTransport transport) {
+ _context = ctx;
+ _log = ctx.logManager().getLog(InboundMessageFragments.class);
+ _inboundMessages = new HashMap(64);
+ _unsentACKs = new ArrayList(64);
+ _completeMessages = new ArrayList(64);
+ _recentlyCompletedMessages = new ArrayList(RECENTLY_COMPLETED_SIZE);
+ _outbound = outbound;
+ _transport = transport;
+ _context.statManager().createRateStat("udp.receivedCompleteTime", "How long it takes to receive a full message", "udp", new long[] { 60*1000, 10*60*1000, 60*60*1000, 24*60*60*1000 });
+ _context.statManager().createRateStat("udp.receivedCompleteFragments", "How many fragments go in a fully received message", "udp", new long[] { 60*1000, 10*60*1000, 60*60*1000, 24*60*60*1000 });
+ _context.statManager().createRateStat("udp.receivedACKs", "How many messages were ACKed at a time", "udp", new long[] { 60*1000, 10*60*1000, 60*60*1000, 24*60*60*1000 });
+ _context.statManager().createRateStat("udp.ignoreRecentDuplicate", "Take note that we received a packet for a recently completed message", "udp", new long[] { 60*1000, 10*60*1000, 60*60*1000, 24*60*60*1000 });
+ _context.statManager().createRateStat("udp.receiveMessagePeriod", "How long it takes to pull the message fragments out of a packet", "udp", new long[] { 60*1000, 10*60*1000, 60*60*1000, 24*60*60*1000 });
+ _context.statManager().createRateStat("udp.receiveACKPeriod", "How long it takes to pull the ACKs out of a packet", "udp", new long[] { 60*1000, 10*60*1000, 60*60*1000, 24*60*60*1000 });
+ _stateLock = this;
+ }
+
+ public void startup() {
+ _alive = true;
+ I2PThread t = new I2PThread(new ACKSender(_context, this, _transport), "UDP ACK sender");
+ t.setDaemon(true);
+ t.start();
+
+ t = new I2PThread(new MessageReceiver(_context, this, _transport), "UDP message receiver");
+ t.setDaemon(true);
+ t.start();
+ }
+ public void shutdown() {
+ _alive = false;
+ synchronized (_stateLock) {
+ _completeMessages.clear();
+ _unsentACKs.clear();
+ _inboundMessages.clear();
+ _stateLock.notifyAll();
+ }
+ }
+ public boolean isAlive() { return _alive; }
+
+ /**
+ * Pull the fragments and ACKs out of the authenticated data packet
+ */
+ public void receiveData(PeerState from, UDPPacketReader.DataReader data) {
+ long beforeMsgs = _context.clock().now();
+ receiveMessages(from, data);
+ long afterMsgs = _context.clock().now();
+ receiveACKs(from, data);
+ long afterACKs = _context.clock().now();
+
+ _context.statManager().addRateData("udp.receiveMessagePeriod", afterMsgs-beforeMsgs, afterACKs-beforeMsgs);
+ _context.statManager().addRateData("udp.receiveACKPeriod", afterACKs-afterMsgs, afterACKs-beforeMsgs);
+ }
+
+ /**
+ * Pull out all the data fragments and shove them into InboundMessageStates.
+ * Along the way, if any state expires, or a full message arrives, move it
+ * appropriately.
+ *
+ */
+ private void receiveMessages(PeerState from, UDPPacketReader.DataReader data) {
+ int fragments = data.readFragmentCount();
+ if (fragments <= 0) return;
+ synchronized (_stateLock) {
+ Map messages = (Map)_inboundMessages.get(from.getRemotePeer());
+ if (messages == null) {
+ messages = new HashMap(fragments);
+ _inboundMessages.put(from.getRemotePeer(), messages);
+ }
+
+ for (int i = 0; i < fragments; i++) {
+ Long messageId = new Long(data.readMessageId(i));
+
+ if (_recentlyCompletedMessages.contains(messageId)) {
+ _context.statManager().addRateData("udp.ignoreRecentDuplicate", 1, 0);
+ continue;
+ }
+
+ int size = data.readMessageFragmentSize(i);
+ InboundMessageState state = null;
+ boolean messageComplete = false;
+ boolean messageExpired = false;
+ boolean fragmentOK = false;
+ state = (InboundMessageState)messages.get(messageId);
+ if (state == null) {
+ state = new InboundMessageState(_context, messageId.longValue(), from.getRemotePeer());
+ messages.put(messageId, state);
+ }
+ fragmentOK = state.receiveFragment(data, i);
+ if (state.isComplete()) {
+ messageComplete = true;
+ messages.remove(messageId);
+
+ while (_recentlyCompletedMessages.size() >= RECENTLY_COMPLETED_SIZE)
+ _recentlyCompletedMessages.remove(0);
+ _recentlyCompletedMessages.add(messageId);
+
+ _completeMessages.add(state);
+
+ from.messageFullyReceived(messageId);
+ if (!_unsentACKs.contains(from))
+ _unsentACKs.add(from);
+
+ if (_log.shouldLog(Log.INFO))
+ _log.info("Message received completely! " + state);
+
+ _context.statManager().addRateData("udp.receivedCompleteTime", state.getLifetime(), state.getLifetime());
+ _context.statManager().addRateData("udp.receivedCompleteFragments", state.getFragmentCount(), state.getLifetime());
+
+ _stateLock.notifyAll();
+ } else if (state.isExpired()) {
+ messageExpired = true;
+ messages.remove(messageId);
+ if (_log.shouldLog(Log.WARN))
+ _log.warn("Message expired while only being partially read: " + state);
+ state.releaseResources();
+ }
+
+ if (!fragmentOK)
+ break;
+ }
+ }
+ }
+
+ private void receiveACKs(PeerState from, UDPPacketReader.DataReader data) {
+ if (data.readACKsIncluded()) {
+ int fragments = 0;
+ long acks[] = data.readACKs();
+ _context.statManager().addRateData("udp.receivedACKs", acks.length, 0);
+ for (int i = 0; i < acks.length; i++) {
+ if (_log.shouldLog(Log.INFO))
+ _log.info("Full ACK of message " + acks[i] + " received!");
+ fragments += _outbound.acked(acks[i], from.getRemotePeer());
+ }
+ from.messageACKed(fragments * from.getMTU()); // estimated size
+ }
+ if (data.readECN())
+ from.ECNReceived();
+ else
+ from.dataReceived();
+ }
+
+ /**
+ * Blocking call to pull off the next fully received message
+ *
+ */
+ public InboundMessageState receiveNextMessage() {
+ while (_alive) {
+ try {
+ synchronized (_stateLock) {
+ if (_completeMessages.size() > 0)
+ return (InboundMessageState)_completeMessages.remove(0);
+ _stateLock.wait();
+ }
+ } catch (InterruptedException ie) {}
+ }
+ return null;
+ }
+
+ /**
+ * Pull off the peer who we next want to send ACKs/NACKs to.
+ * This call blocks, and only returns null on shutdown.
+ *
+ */
+ public PeerState getNextPeerToACK() {
+ while (_alive) {
+ try {
+ long now = _context.clock().now();
+ synchronized (_stateLock) {
+ for (int i = 0; i < _unsentACKs.size(); i++) {
+ PeerState peer = (PeerState)_unsentACKs.get(i);
+ if (peer.getLastACKSend() + ACK_FREQUENCY <= now) {
+ _unsentACKs.remove(i);
+ peer.setLastACKSend(now);
+ return peer;
+ }
+ }
+ if (_unsentACKs.size() > 0)
+ _stateLock.wait(_context.random().nextInt(100));
+ else
+ _stateLock.wait();
+ }
+ } catch (InterruptedException ie) {}
+ }
+ return null;
+ }
+}
diff --git a/router/java/src/net/i2p/router/transport/udp/InboundMessageState.java b/router/java/src/net/i2p/router/transport/udp/InboundMessageState.java
new file mode 100644
index 000000000..a07d45341
--- /dev/null
+++ b/router/java/src/net/i2p/router/transport/udp/InboundMessageState.java
@@ -0,0 +1,112 @@
+package net.i2p.router.transport.udp;
+
+import net.i2p.data.ByteArray;
+import net.i2p.data.Hash;
+import net.i2p.router.RouterContext;
+import net.i2p.util.ByteCache;
+import net.i2p.util.Log;
+
+/**
+ * Hold the raw data fragments of an inbound message
+ *
+ */
+public class InboundMessageState {
+ private RouterContext _context;
+ private Log _log;
+ private long _messageId;
+ private Hash _from;
+ /**
+ * indexed array of fragments for the message, where not yet
+ * received fragments are null.
+ */
+ private ByteArray _fragments[];
+ /**
+ * what is the last fragment in the message (or -1 if not yet known)
+ */
+ private int _lastFragment;
+ private long _receiveBegin;
+
+ /** expire after 30s */
+ private static final long MAX_RECEIVE_TIME = 30*1000;
+ private static final int MAX_FRAGMENTS = 32;
+
+ private static final ByteCache _fragmentCache = ByteCache.getInstance(64, 2048);
+
+ public InboundMessageState(RouterContext ctx, long messageId, Hash from) {
+ _context = ctx;
+ _log = ctx.logManager().getLog(InboundMessageState.class);
+ _messageId = messageId;
+ _from = from;
+ _fragments = new ByteArray[MAX_FRAGMENTS];
+ _lastFragment = -1;
+ _receiveBegin = ctx.clock().now();
+ }
+
+ /**
+ * Read in the data from the fragment.
+ *
+ * @return true if the data was ok, false if it was corrupt
+ */
+ public synchronized boolean receiveFragment(UDPPacketReader.DataReader data, int dataFragment) {
+ int fragmentNum = data.readMessageFragmentNum(dataFragment);
+ if ( (fragmentNum < 0) || (fragmentNum > _fragments.length)) {
+ _log.log(Log.CRIT, "Invalid fragment " + fragmentNum + ": " + data, new Exception("source"));
+ return false;
+ }
+ if (_fragments[fragmentNum] == null) {
+ // new fragment, read it
+ ByteArray message = _fragmentCache.acquire();
+ data.readMessageFragment(dataFragment, message.getData(), 0);
+ int size = data.readMessageFragmentSize(dataFragment);
+ message.setValid(size);
+ _fragments[fragmentNum] = message;
+ if (data.readMessageIsLast(dataFragment))
+ _lastFragment = fragmentNum;
+ }
+ return true;
+ }
+
+ public synchronized boolean isComplete() {
+ if (_lastFragment < 0) return false;
+ for (int i = 0; i <= _lastFragment; i++)
+ if (_fragments[i] == null)
+ return false;
+ return true;
+ }
+ public synchronized boolean isExpired() {
+ return _context.clock().now() > _receiveBegin + MAX_RECEIVE_TIME;
+ }
+ public long getLifetime() {
+ return _context.clock().now() - _receiveBegin;
+ }
+ public Hash getFrom() { return _from; }
+ public long getMessageId() { return _messageId; }
+ public synchronized int getCompleteSize() {
+ int size = 0;
+ for (int i = 0; i <= _lastFragment; i++)
+ size += _fragments[i].getValid();
+ return size;
+ }
+
+ public void releaseResources() {
+ if (_fragments != null)
+ for (int i = 0; i < _fragments.length; i++)
+ _fragmentCache.release(_fragments[i]);
+ _fragments = null;
+ }
+
+ public ByteArray[] getFragments() {
+ return _fragments;
+ }
+ public int getFragmentCount() { return _lastFragment+1; }
+
+ public String toString() {
+ StringBuffer buf = new StringBuffer(32);
+ buf.append("Message: ").append(_messageId);
+ if (isComplete()) {
+ buf.append(" completely received with ");
+ buf.append(getCompleteSize()).append(" bytes");
+ }
+ return buf.toString();
+ }
+}
diff --git a/router/java/src/net/i2p/router/transport/udp/MessageQueue.java b/router/java/src/net/i2p/router/transport/udp/MessageQueue.java
new file mode 100644
index 000000000..cc38ebbaf
--- /dev/null
+++ b/router/java/src/net/i2p/router/transport/udp/MessageQueue.java
@@ -0,0 +1,20 @@
+package net.i2p.router.transport.udp;
+
+import net.i2p.router.OutNetMessage;
+
+/**
+ * Base queue for messages not yet packetized
+ */
+public interface MessageQueue {
+ /**
+ * Get the next message, blocking until one is found or the expiration
+ * reached.
+ *
+ * @param blockUntil expiration, or -1 if indefinite
+ */
+ public OutNetMessage getNext(long blockUntil);
+ /**
+ * Add on a new message to the queue
+ */
+ public void add(OutNetMessage message);
+}
diff --git a/router/java/src/net/i2p/router/transport/udp/MessageReceiver.java b/router/java/src/net/i2p/router/transport/udp/MessageReceiver.java
new file mode 100644
index 000000000..c13071159
--- /dev/null
+++ b/router/java/src/net/i2p/router/transport/udp/MessageReceiver.java
@@ -0,0 +1,74 @@
+package net.i2p.router.transport.udp;
+
+import net.i2p.data.Base64;
+import net.i2p.data.ByteArray;
+import net.i2p.data.DataFormatException;
+import net.i2p.data.i2np.I2NPMessage;
+import net.i2p.data.i2np.I2NPMessageImpl;
+import net.i2p.data.i2np.I2NPMessageException;
+import net.i2p.router.RouterContext;
+import net.i2p.util.Log;
+
+/**
+ * Pull fully completed fragments off the {@link InboundMessageFragments} queue,
+ * parse 'em into I2NPMessages, and stick them on the
+ * {@link net.i2p.router.InNetMessagePool} by way of the {@link UDPTransport}.
+ */
+public class MessageReceiver implements Runnable {
+ private RouterContext _context;
+ private Log _log;
+ private InboundMessageFragments _fragments;
+ private UDPTransport _transport;
+
+ public MessageReceiver(RouterContext ctx, InboundMessageFragments frag, UDPTransport transport) {
+ _context = ctx;
+ _log = ctx.logManager().getLog(MessageReceiver.class);
+ _fragments = frag;
+ _transport = transport;
+ }
+
+ public void run() {
+ while (_fragments.isAlive()) {
+ InboundMessageState message = _fragments.receiveNextMessage();
+ if (message == null) continue;
+
+ int size = message.getCompleteSize();
+ if (_log.shouldLog(Log.INFO))
+ _log.info("Full message received (" + message.getMessageId() + ") after " + message.getLifetime()
+ + "... todo: parse and plop it onto InNetMessagePool");
+ I2NPMessage msg = readMessage(message);
+ if (msg != null)
+ _transport.messageReceived(msg, null, message.getFrom(), message.getLifetime(), size);
+ }
+ }
+
+ private I2NPMessage readMessage(InboundMessageState state) {
+ try {
+ byte buf[] = new byte[state.getCompleteSize()];
+ ByteArray fragments[] = state.getFragments();
+ int numFragments = state.getFragmentCount();
+ int off = 0;
+ for (int i = 0; i < numFragments; i++) {
+ System.arraycopy(fragments[i].getData(), 0, buf, off, fragments[i].getValid());
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("Raw fragment[" + i + "] for " + state.getMessageId() + ": "
+ + Base64.encode(fragments[i].getData(), 0, fragments[i].getValid()));
+ off += fragments[i].getValid();
+ }
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("Raw byte array for " + state.getMessageId() + ": " + Base64.encode(buf));
+ I2NPMessage m = I2NPMessageImpl.fromRawByteArray(_context, buf, 0, buf.length);
+ m.setUniqueId(state.getMessageId());
+ return m;
+ } catch (I2NPMessageException ime) {
+ if (_log.shouldLog(Log.WARN))
+ _log.warn("Message invalid: " + state, ime);
+ return null;
+ } catch (Exception e) {
+ _log.log(Log.CRIT, "Error dealing with a message: " + state, e);
+ return null;
+ } finally {
+ state.releaseResources();
+ }
+ }
+}
diff --git a/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState.java b/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState.java
new file mode 100644
index 000000000..e359bccdf
--- /dev/null
+++ b/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState.java
@@ -0,0 +1,349 @@
+package net.i2p.router.transport.udp;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.List;
+
+import net.i2p.crypto.DHSessionKeyBuilder;
+import net.i2p.data.Base64;
+import net.i2p.data.ByteArray;
+import net.i2p.data.DataFormatException;
+import net.i2p.data.DataHelper;
+import net.i2p.data.RouterIdentity;
+import net.i2p.data.SessionKey;
+import net.i2p.data.Signature;
+import net.i2p.router.OutNetMessage;
+import net.i2p.router.RouterContext;
+import net.i2p.util.Log;
+
+/**
+ * Data for a new connection being established, where we initiated the
+ * connection with a remote peer. In other words, we are Alice and
+ * they are Bob.
+ *
+ */
+public class OutboundEstablishState {
+ private RouterContext _context;
+ private Log _log;
+ // SessionRequest message
+ private byte _sentX[];
+ private byte _bobIP[];
+ private int _bobPort;
+ private DHSessionKeyBuilder _keyBuilder;
+ // SessionCreated message
+ private byte _receivedY[];
+ private byte _aliceIP[];
+ private int _alicePort;
+ private long _receivedRelayTag;
+ private long _receivedSignedOnTime;
+ private SessionKey _sessionKey;
+ private SessionKey _macKey;
+ private Signature _receivedSignature;
+ private byte[] _receivedEncryptedSignature;
+ private byte[] _receivedIV;
+ // SessionConfirmed messages
+ private long _sentSignedOnTime;
+ private Signature _sentSignature;
+ // general status
+ private long _establishBegin;
+ private long _lastReceive;
+ private long _lastSend;
+ private long _nextSend;
+ private String _remoteHostInfo;
+ private RouterIdentity _remotePeer;
+ private SessionKey _introKey;
+ private List _queuedMessages;
+ private int _currentState;
+
+ /** nothin sent yet */
+ public static final int STATE_UNKNOWN = 0;
+ /** we have sent an initial request */
+ public static final int STATE_REQUEST_SENT = 1;
+ /** we have received a signed creation packet */
+ public static final int STATE_CREATED_RECEIVED = 2;
+ /** we have sent one or more confirmation packets */
+ public static final int STATE_CONFIRMED_PARTIALLY = 3;
+ /** we have received a data packet */
+ public static final int STATE_CONFIRMED_COMPLETELY = 4;
+
+ public OutboundEstablishState(RouterContext ctx, InetAddress remoteHost, int remotePort,
+ RouterIdentity remotePeer, SessionKey introKey) {
+ _context = ctx;
+ _log = ctx.logManager().getLog(OutboundEstablishState.class);
+ _bobIP = remoteHost.getAddress();
+ _bobPort = remotePort;
+ _remoteHostInfo = PeerState.calculateRemoteHostString(_bobIP, _bobPort);
+ _remotePeer = remotePeer;
+ _introKey = introKey;
+ _keyBuilder = null;
+ _queuedMessages = new ArrayList(4);
+ _currentState = STATE_UNKNOWN;
+ _establishBegin = ctx.clock().now();
+ }
+
+ public synchronized int getState() { return _currentState; }
+
+ public void addMessage(OutNetMessage msg) {
+ synchronized (_queuedMessages) {
+ _queuedMessages.add(msg);
+ }
+ }
+ public OutNetMessage getNextQueuedMessage() {
+ synchronized (_queuedMessages) {
+ if (_queuedMessages.size() > 0)
+ return (OutNetMessage)_queuedMessages.remove(0);
+ }
+ return null;
+ }
+
+ public RouterIdentity getRemoteIdentity() { return _remotePeer; }
+ public SessionKey getIntroKey() { return _introKey; }
+
+ public synchronized void prepareSessionRequest() {
+ _keyBuilder = new DHSessionKeyBuilder();
+ byte X[] = _keyBuilder.getMyPublicValue().toByteArray();
+ if (_sentX == null)
+ _sentX = new byte[UDPPacketReader.SessionRequestReader.X_LENGTH];
+ if (X.length == 257)
+ System.arraycopy(X, 1, _sentX, 0, _sentX.length);
+ else if (X.length == 256)
+ System.arraycopy(X, 0, _sentX, 0, _sentX.length);
+ else
+ System.arraycopy(X, 0, _sentX, _sentX.length - X.length, X.length);
+ }
+
+ public synchronized byte[] getSentX() { return _sentX; }
+ public synchronized byte[] getSentIP() { return _bobIP; }
+ public synchronized int getSentPort() { return _bobPort; }
+
+ public synchronized void receiveSessionCreated(UDPPacketReader.SessionCreatedReader reader) {
+ if (_receivedY != null) {
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("Session created already received, ignoring");
+ return; // already received
+ }
+ _receivedY = new byte[UDPPacketReader.SessionCreatedReader.Y_LENGTH];
+ reader.readY(_receivedY, 0);
+ if (_aliceIP == null)
+ _aliceIP = new byte[reader.readIPSize()];
+ reader.readIP(_aliceIP, 0);
+ _alicePort = reader.readPort();
+ _receivedRelayTag = reader.readRelayTag();
+ _receivedSignedOnTime = reader.readSignedOnTime();
+ _receivedEncryptedSignature = new byte[Signature.SIGNATURE_BYTES + 8];
+ reader.readEncryptedSignature(_receivedEncryptedSignature, 0);
+ _receivedIV = new byte[UDPPacket.IV_SIZE];
+ reader.readIV(_receivedIV, 0);
+
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("Receive session created:\neSig: " + Base64.encode(_receivedEncryptedSignature)
+ + "\nreceivedIV: " + Base64.encode(_receivedIV)
+ + "\nAliceIP: " + Base64.encode(_aliceIP)
+ + " RelayTag: " + _receivedRelayTag
+ + " SignedOn: " + _receivedSignedOnTime
+ + "\nthis: " + this.toString());
+
+ if ( (_currentState == STATE_UNKNOWN) || (_currentState == STATE_REQUEST_SENT) )
+ _currentState = STATE_CREATED_RECEIVED;
+ packetReceived();
+ }
+
+ /**
+ * Blocking call (run in the establisher thread) to determine if the
+ * session was created properly. If it wasn't, all the SessionCreated
+ * remnants are dropped (perhaps they were spoofed, etc) so that we can
+ * receive another one
+ */
+ public synchronized boolean validateSessionCreated() {
+ if (_receivedSignature != null) {
+ if (_log.shouldLog(Log.WARN))
+ _log.warn("Session created already validated");
+ return true;
+ }
+
+ generateSessionKey();
+ decryptSignature();
+
+ if (verifySessionCreated()) {
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("Session created passed validation");
+ return true;
+ } else {
+ if (_log.shouldLog(Log.WARN))
+ _log.warn("Session created failed validation, clearing state");
+ _receivedY = null;
+ _aliceIP = null;
+ _receivedRelayTag = 0;
+ _receivedSignedOnTime = -1;
+ _receivedEncryptedSignature = null;
+ _receivedIV = null;
+ _receivedSignature = null;
+
+ if ( (_currentState == STATE_UNKNOWN) ||
+ (_currentState == STATE_REQUEST_SENT) ||
+ (_currentState == STATE_CREATED_RECEIVED) )
+ _currentState = STATE_REQUEST_SENT;
+
+ _nextSend = _context.clock().now();
+ return false;
+ }
+ }
+
+ private void generateSessionKey() {
+ if (_sessionKey != null) return;
+ _keyBuilder.setPeerPublicValue(_receivedY);
+ _sessionKey = _keyBuilder.getSessionKey();
+ ByteArray extra = _keyBuilder.getExtraBytes();
+ _macKey = new SessionKey(new byte[SessionKey.KEYSIZE_BYTES]);
+ System.arraycopy(extra.getData(), 0, _macKey.getData(), 0, SessionKey.KEYSIZE_BYTES);
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("Established outbound keys. cipher: " + Base64.encode(_sessionKey.getData())
+ + " mac: " + Base64.encode(_macKey.getData()));
+ }
+
+ /**
+ * decrypt the signature (and subsequent pad bytes) with the
+ * additional layer of encryption using the negotiated key along side
+ * the packet's IV
+ */
+ private void decryptSignature() {
+ if (_receivedEncryptedSignature == null) throw new NullPointerException("encrypted signature is null! this=" + this.toString());
+ else if (_sessionKey == null) throw new NullPointerException("SessionKey is null!");
+ else if (_receivedIV == null) throw new NullPointerException("IV is null!");
+ _context.aes().decrypt(_receivedEncryptedSignature, 0, _receivedEncryptedSignature, 0,
+ _sessionKey, _receivedIV, _receivedEncryptedSignature.length);
+ byte signatureBytes[] = new byte[Signature.SIGNATURE_BYTES];
+ System.arraycopy(_receivedEncryptedSignature, 0, signatureBytes, 0, Signature.SIGNATURE_BYTES);
+ _receivedSignature = new Signature(signatureBytes);
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("Decrypted received signature: \n" + Base64.encode(signatureBytes));
+ }
+
+ /**
+ * Verify: Alice's IP + Alice's port + Bob's IP + Bob's port + Alice's
+ * new relay tag + Bob's signed on time
+ */
+ private boolean verifySessionCreated() {
+ byte signed[] = new byte[_aliceIP.length + 2
+ + _bobIP.length + 2
+ + 4 // sent relay tag
+ + 4 // signed on time
+ ];
+
+ int off = 0;
+ System.arraycopy(_aliceIP, 0, signed, off, _aliceIP.length);
+ off += _aliceIP.length;
+ DataHelper.toLong(signed, off, 2, _alicePort);
+ off += 2;
+ System.arraycopy(_bobIP, 0, signed, off, _bobIP.length);
+ off += _bobIP.length;
+ DataHelper.toLong(signed, off, 2, _bobPort);
+ off += 2;
+ DataHelper.toLong(signed, off, 4, _receivedRelayTag);
+ off += 4;
+ DataHelper.toLong(signed, off, 4, _receivedSignedOnTime);
+ if (_log.shouldLog(Log.DEBUG)) {
+ StringBuffer buf = new StringBuffer(128);
+ buf.append("Signed sessionCreated:");
+ buf.append(" AliceIP: ").append(Base64.encode(_aliceIP));
+ buf.append(" AlicePort: ").append(_alicePort);
+ buf.append(" BobIP: ").append(Base64.encode(_bobIP));
+ buf.append(" BobPort: ").append(_bobPort);
+ buf.append(" RelayTag: ").append(_receivedRelayTag);
+ buf.append(" SignedOn: ").append(_receivedSignedOnTime);
+ buf.append(" signature: ").append(Base64.encode(_receivedSignature.getData()));
+ _log.debug(buf.toString());
+ }
+ return _context.dsa().verifySignature(_receivedSignature, signed, _remotePeer.getSigningPublicKey());
+ }
+
+ public synchronized SessionKey getCipherKey() { return _sessionKey; }
+ public synchronized SessionKey getMACKey() { return _macKey; }
+
+ public synchronized long getReceivedRelayTag() { return _receivedRelayTag; }
+ public synchronized long getSentSignedOnTime() { return _sentSignedOnTime; }
+ public synchronized long getReceivedSignedOnTime() { return _receivedSignedOnTime; }
+ public synchronized byte[] getReceivedIP() { return _aliceIP; }
+ public synchronized int getReceivedPort() { return _alicePort; }
+
+ /**
+ * Lets sign everything so we can fragment properly
+ *
+ */
+ public synchronized void prepareSessionConfirmed() {
+ if (_sentSignedOnTime > 0)
+ return;
+ byte signed[] = new byte[_aliceIP.length + 2
+ + _bobIP.length + 2
+ + 4 // Alice's relay key
+ + 4 // signed on time
+ ];
+
+ _sentSignedOnTime = _context.clock().now() / 1000;
+
+ int off = 0;
+ System.arraycopy(_aliceIP, 0, signed, off, _aliceIP.length);
+ off += _aliceIP.length;
+ DataHelper.toLong(signed, off, 2, _alicePort);
+ off += 2;
+ System.arraycopy(_bobIP, 0, signed, off, _bobIP.length);
+ off += _bobIP.length;
+ DataHelper.toLong(signed, off, 2, _bobPort);
+ off += 2;
+ DataHelper.toLong(signed, off, 4, _receivedRelayTag);
+ off += 4;
+ DataHelper.toLong(signed, off, 4, _sentSignedOnTime);
+ _sentSignature = _context.dsa().sign(signed, _context.keyManager().getSigningPrivateKey());
+ }
+
+ public synchronized Signature getSentSignature() { return _sentSignature; }
+
+ /** note that we just sent the SessionConfirmed packet */
+ public synchronized void confirmedPacketsSent() {
+ _lastSend = _context.clock().now();
+ _nextSend = _lastSend + 5*1000;
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("Send confirm packets, nextSend = 5s");
+ if ( (_currentState == STATE_UNKNOWN) ||
+ (_currentState == STATE_REQUEST_SENT) ||
+ (_currentState == STATE_CREATED_RECEIVED) )
+ _currentState = STATE_CONFIRMED_PARTIALLY;
+ }
+ /** note that we just sent the SessionRequest packet */
+ public synchronized void requestSent() {
+ _lastSend = _context.clock().now();
+ _nextSend = _lastSend + 5*1000;
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("Send a request packet, nextSend = 5s");
+ if (_currentState == STATE_UNKNOWN)
+ _currentState = STATE_REQUEST_SENT;
+ }
+
+ /** how long have we been trying to establish this session? */
+ public synchronized long getLifetime() { return _context.clock().now() - _establishBegin; }
+ public synchronized long getEstablishBeginTime() { return _establishBegin; }
+ public synchronized long getNextSendTime() { return _nextSend; }
+ public synchronized void setNextSendTime(long when) {
+ _nextSend = when;
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("Explicit nextSend=" + (_nextSend-_context.clock().now()), new Exception("Set by"));
+ }
+
+ /** host+port, uniquely identifies an attempt */
+ public String getRemoteHostInfo() { return _remoteHostInfo; }
+
+ /** we have received a real data packet, so we're done establishing */
+ public synchronized void dataReceived() {
+ packetReceived();
+ _currentState = STATE_CONFIRMED_COMPLETELY;
+ }
+
+ private void packetReceived() {
+ _lastReceive = _context.clock().now();
+ _nextSend = _lastReceive;
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("Got a packet, nextSend == now");
+ }
+}
diff --git a/router/java/src/net/i2p/router/transport/udp/OutboundMessageFragments.java b/router/java/src/net/i2p/router/transport/udp/OutboundMessageFragments.java
new file mode 100644
index 000000000..60c2503a2
--- /dev/null
+++ b/router/java/src/net/i2p/router/transport/udp/OutboundMessageFragments.java
@@ -0,0 +1,358 @@
+package net.i2p.router.transport.udp;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import net.i2p.data.Hash;
+import net.i2p.router.OutNetMessage;
+import net.i2p.router.RouterContext;
+import net.i2p.util.I2PThread;
+import net.i2p.util.Log;
+
+/**
+ * Coordinate the outbound fragments and select the next one to be built.
+ * This pool contains messages we are actively trying to send, essentially
+ * doing a round robin across each message to send one fragment, as implemented
+ * in {@link #getNextPacket()}. This also honors per-peer throttling, taking
+ * note of each peer's allocations. If a message has each of its fragments
+ * sent more than a certain number of times, it is failed out. In addition,
+ * this instance also receives notification of message ACKs from the
+ * {@link InboundMessageFragments}, signaling that we can stop sending a
+ * message.
+ *
+ */
+public class OutboundMessageFragments {
+ private RouterContext _context;
+ private Log _log;
+ private UDPTransport _transport;
+ /** OutboundMessageState for messages being sent */
+ private List _activeMessages;
+ private boolean _alive;
+ /** which message should we build the next packet out of? */
+ private int _nextPacketMessage;
+ private PacketBuilder _builder;
+
+ private static final int MAX_ACTIVE = 64;
+ // don't send a packet more than 10 times
+ private static final int MAX_VOLLEYS = 10;
+
+ public OutboundMessageFragments(RouterContext ctx, UDPTransport transport) {
+ _context = ctx;
+ _log = ctx.logManager().getLog(OutboundMessageFragments.class);
+ _transport = transport;
+ _activeMessages = new ArrayList(MAX_ACTIVE);
+ _nextPacketMessage = 0;
+ _builder = new PacketBuilder(ctx, _transport);
+ _alive = true;
+ _context.statManager().createRateStat("udp.sendVolleyTime", "Long it takes to send a full volley", "udp", new long[] { 60*1000, 10*60*1000, 60*60*1000, 24*60*60*1000 });
+ _context.statManager().createRateStat("udp.sendConfirmTime", "How long it takes to send a message and get the ACK", "udp", new long[] { 60*1000, 10*60*1000, 60*60*1000, 24*60*60*1000 });
+ _context.statManager().createRateStat("udp.sendConfirmFragments", "How many fragments are included in a fully ACKed message", "udp", new long[] { 60*1000, 10*60*1000, 60*60*1000, 24*60*60*1000 });
+ _context.statManager().createRateStat("udp.sendConfirmVolley", "How many times did fragments need to be sent before ACK", "udp", new long[] { 60*1000, 10*60*1000, 60*60*1000, 24*60*60*1000 });
+ _context.statManager().createRateStat("udp.sendFailed", "How many fragments were in a message that couldn't be delivered", "udp", new long[] { 60*1000, 10*60*1000, 60*60*1000, 24*60*60*1000 });
+ _context.statManager().createRateStat("udp.sendAggressiveFailed", "How many volleys was a packet sent before we gave up", "udp", new long[] { 60*1000, 10*60*1000, 60*60*1000, 24*60*60*1000 });
+ }
+
+ public void startup() { _alive = true; }
+ public void shutdown() {
+ _alive = false;
+ synchronized (_activeMessages) {
+ _activeMessages.notifyAll();
+ }
+ }
+
+ /**
+ * Block until we allow more messages to be admitted to the active
+ * pool. This is called by the {@link OutboundRefiller}
+ *
+ * @return true if more messages are allowed
+ */
+ public boolean waitForMoreAllowed() {
+ while (_alive) {
+ finishMessages();
+ try {
+ synchronized (_activeMessages) {
+ if (!_alive)
+ return false;
+ else if (_activeMessages.size() < MAX_ACTIVE)
+ return true;
+ else
+ _activeMessages.wait();
+ }
+ } catch (InterruptedException ie) {}
+ }
+ return false;
+ }
+
+ /**
+ * Add a new message to the active pool
+ *
+ */
+ public void add(OutNetMessage msg) {
+ OutboundMessageState state = new OutboundMessageState(_context);
+ state.initialize(msg);
+ finishMessages();
+ synchronized (_activeMessages) {
+ _activeMessages.add(state);
+ _activeMessages.notifyAll();
+ }
+ }
+
+ /**
+ * short circuit the OutNetMessage, letting us send the establish
+ * complete message reliably
+ */
+ public void add(OutboundMessageState state) {
+ synchronized (_activeMessages) {
+ _activeMessages.add(state);
+ _activeMessages.notifyAll();
+ }
+ }
+
+ /**
+ * Remove any expired or complete messages
+ */
+ private void finishMessages() {
+ synchronized (_activeMessages) {
+ for (int i = 0; i < _activeMessages.size(); i++) {
+ OutboundMessageState state = (OutboundMessageState)_activeMessages.get(i);
+ if (state.isComplete()) {
+ _activeMessages.remove(i);
+ _transport.succeeded(state.getMessage());
+ i--;
+ } else if (state.isExpired()) {
+ _activeMessages.remove(i);
+ _context.statManager().addRateData("udp.sendFailed", state.getFragmentCount(), state.getLifetime());
+
+ if (state.getMessage() != null) {
+ _transport.failed(state.getMessage());
+ } else {
+ // it can not have an OutNetMessage if the source is the
+ // final after establishment message
+ if (_log.shouldLog(Log.WARN))
+ _log.warn("Unable to send a direct message: " + state);
+ }
+ i--;
+ } else if (state.getPushCount() > MAX_VOLLEYS) {
+ _activeMessages.remove(i);
+ _context.statManager().addRateData("udp.sendAggressiveFailed", state.getPushCount(), state.getLifetime());
+ if (state.getPeer() != null)
+ state.getPeer().congestionOccurred();
+
+ if (state.getMessage() != null) {
+ _transport.failed(state.getMessage());
+ } else {
+ // it can not have an OutNetMessage if the source is the
+ // final after establishment message
+ if (_log.shouldLog(Log.WARN))
+ _log.warn("Unable to send a direct message: " + state);
+ }
+ i--;
+
+ }
+ }
+ }
+ }
+
+ /**
+ * Grab the next packet that we want to send, blocking until one is ready.
+ * This is the main driver for the packet scheduler
+ *
+ */
+ public UDPPacket getNextPacket() {
+ PeerState peer = null;
+ OutboundMessageState state = null;
+ int currentFragment = -1;
+ while (_alive && (currentFragment < 0) ) {
+ long now = _context.clock().now();
+ long nextSend = -1;
+ finishMessages();
+ synchronized (_activeMessages) {
+ for (int i = 0; i < _activeMessages.size(); i++) {
+ int cur = (i + _nextPacketMessage) % _activeMessages.size();
+ state = (OutboundMessageState)_activeMessages.get(cur);
+ if (state.getNextSendTime() <= now) {
+ peer = state.getPeer(); // known if this is immediately after establish
+ if (peer == null)
+ peer = _transport.getPeerState(state.getMessage().getTarget().getIdentity().calculateHash());
+
+ if (peer == null) {
+ // peer disconnected (whatever that means)
+ _activeMessages.remove(cur);
+ _transport.failed(state.getMessage());
+ if (_log.shouldLog(Log.WARN))
+ _log.warn("Peer disconnected for " + state);
+ i--;
+ } else {
+ if (!state.isFragmented()) {
+ state.fragment(fragmentSize(peer.getMTU()));
+
+ if (_log.shouldLog(Log.INFO))
+ _log.info("Fragmenting " + state);
+ }
+
+ int oldVolley = state.getPushCount();
+ // pickNextFragment increments the pushCount every
+ // time we cycle through all of the packets
+ currentFragment = state.pickNextFragment();
+
+ int fragmentSize = state.fragmentSize(currentFragment);
+ if (peer.allocateSendingBytes(fragmentSize)) {
+ if (_log.shouldLog(Log.INFO))
+ _log.info("Allocation of " + fragmentSize + " allowed");
+
+ // for fairness, we move on in a round robin
+ _nextPacketMessage = i + 1;
+
+ if (state.getPushCount() != oldVolley) {
+ _context.statManager().addRateData("udp.sendVolleyTime", state.getLifetime(), state.getFragmentCount());
+ state.setNextSendTime(now + 500);
+ } else {
+ if (peer.getSendWindowBytesRemaining() > 0)
+ state.setNextSendTime(now);
+ else
+ state.setNextSendTime(now + 50 );
+ }
+ break;
+ } else {
+ if (_log.shouldLog(Log.WARN))
+ _log.warn("Allocation of " + fragmentSize + " rejected");
+ state.setNextSendTime(now + _context.random().nextInt(500));
+ currentFragment = -1;
+ }
+ }
+ }
+ long time = state.getNextSendTime();
+ if ( (nextSend < 0) || (time < nextSend) )
+ nextSend = time;
+ }
+
+ if (currentFragment < 0) {
+ if (nextSend <= 0) {
+ try {
+ _activeMessages.wait(100);
+ } catch (InterruptedException ie) {}
+ } else {
+ // none of the packets were eligible for sending
+ long delay = nextSend - now;
+ if (delay <= 0)
+ delay = 10;
+ if (delay > 500)
+ delay = 500;
+ try {
+ _activeMessages.wait(delay);
+ } catch (InterruptedException ie) {}
+ }
+ }
+ }
+ }
+
+ if (currentFragment >= 0) {
+ if (_log.shouldLog(Log.INFO))
+ _log.info("Building packet for fragment " + currentFragment
+ + " of " + state + " to " + peer);
+ UDPPacket rv = _builder.buildPacket(state, currentFragment, peer);
+ return rv;
+ } else {
+ // !alive
+ return null;
+ }
+ }
+
+ private static final int SSU_HEADER_SIZE = 46;
+ private static final int UDP_HEADER_SIZE = 8;
+ private static final int IP_HEADER_SIZE = 20;
+ /** how much payload data can we shove in there? */
+ private static final int fragmentSize(int mtu) {
+ return mtu - SSU_HEADER_SIZE - UDP_HEADER_SIZE - IP_HEADER_SIZE;
+ }
+
+ /**
+ * We received an ACK of the given messageId from the given peer, so if it
+ * is still unacked, mark it as complete.
+ *
+ * @return fragments acked
+ */
+ public int acked(long messageId, Hash ackedBy) {
+ OutboundMessageState state = null;
+ synchronized (_activeMessages) {
+ // linear search, since its tiny
+ for (int i = 0; i < _activeMessages.size(); i++) {
+ state = (OutboundMessageState)_activeMessages.get(i);
+ if (state.getMessageId() == messageId) {
+ OutNetMessage msg = state.getMessage();
+ if (msg != null) {
+ Hash expectedBy = msg.getTarget().getIdentity().getHash();
+ if (!expectedBy.equals(ackedBy)) {
+ state = null;
+ return 0;
+ }
+ }
+ // either the message was a short circuit after establishment,
+ // or it was received from who we sent it to. yay!
+ _activeMessages.remove(i);
+ _activeMessages.notifyAll();
+ break;
+ } else {
+ state = null;
+ }
+ }
+ }
+
+ if (state != null) {
+ if (_log.shouldLog(Log.INFO))
+ _log.info("Received ack of " + messageId + " by " + ackedBy.toBase64()
+ + " after " + state.getLifetime());
+ _context.statManager().addRateData("udp.sendConfirmTime", state.getLifetime(), state.getLifetime());
+ _context.statManager().addRateData("udp.sendConfirmFragments", state.getFragmentCount(), state.getLifetime());
+ int numSends = state.getMaxSends();
+ _context.statManager().addRateData("udp.sendConfirmVolley", numSends, state.getFragmentCount());
+ if ( (numSends > 1) && (state.getPeer() != null) )
+ state.getPeer().congestionOccurred();
+ _transport.succeeded(state.getMessage());
+ return state.getFragmentCount();
+ } else {
+ if (_log.shouldLog(Log.ERROR))
+ _log.error("Received an ACK for a message not pending: " + messageId);
+ return 0;
+ }
+ }
+
+ /**
+ * Receive a set of fragment ACKs for a given messageId from the
+ * specified peer
+ *
+ */
+ public void acked(long messageId, int ackedFragments[], Hash ackedBy) {
+ if (_log.shouldLog(Log.INFO))
+ _log.info("Received partial ack of " + messageId + " by " + ackedBy.toBase64());
+ OutboundMessageState state = null;
+ synchronized (_activeMessages) {
+ // linear search, since its tiny
+ for (int i = 0; i < _activeMessages.size(); i++) {
+ state = (OutboundMessageState)_activeMessages.get(i);
+ if (state.getMessage().getMessageId() == messageId) {
+ Hash expectedBy = state.getMessage().getTarget().getIdentity().calculateHash();
+ if (!expectedBy.equals(ackedBy)) {
+ return;
+ } else {
+ state.acked(ackedFragments);
+ if (state.isComplete()) {
+ _activeMessages.remove(i);
+ _activeMessages.notifyAll();
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ if ( (state != null) && (state.isComplete()) ) {
+ if (_log.shouldLog(Log.INFO))
+ _log.info("Received ack of " + messageId + " by " + ackedBy.toBase64()
+ + " after " + state.getLifetime());
+ _context.statManager().addRateData("udp.sendConfirmTime", state.getLifetime(), state.getLifetime());
+ _context.statManager().addRateData("udp.sendConfirmFragments", state.getFragmentCount(), state.getLifetime());
+ _transport.succeeded(state.getMessage());
+ }
+ }
+}
diff --git a/router/java/src/net/i2p/router/transport/udp/OutboundMessageState.java b/router/java/src/net/i2p/router/transport/udp/OutboundMessageState.java
new file mode 100644
index 000000000..cff837dac
--- /dev/null
+++ b/router/java/src/net/i2p/router/transport/udp/OutboundMessageState.java
@@ -0,0 +1,232 @@
+package net.i2p.router.transport.udp;
+
+import java.util.Arrays;
+import net.i2p.data.Base64;
+import net.i2p.data.ByteArray;
+import net.i2p.data.i2np.I2NPMessage;
+import net.i2p.router.RouterContext;
+import net.i2p.router.OutNetMessage;
+import net.i2p.util.ByteCache;
+import net.i2p.util.Log;
+
+/**
+ * Maintain the outbound fragmentation for resending
+ *
+ */
+public class OutboundMessageState {
+ private RouterContext _context;
+ private Log _log;
+ /** may be null if we are part of the establishment */
+ private OutNetMessage _message;
+ private long _messageId;
+ /** will be null, unless we are part of the establishment */
+ private PeerState _peer;
+ private long _expiration;
+ private ByteArray _messageBuf;
+ /** fixed fragment size across the message */
+ private int _fragmentSize;
+ /** sends[i] is how many times the fragment has been sent, or -1 if ACKed */
+ private short _fragmentSends[];
+ private long _startedOn;
+ private long _nextSendTime;
+ private int _pushCount;
+ private short _maxSends;
+
+ public static final int MAX_FRAGMENTS = 32;
+ private static final ByteCache _cache = ByteCache.getInstance(64, MAX_FRAGMENTS*1024);
+
+ public OutboundMessageState(RouterContext context) {
+ _context = context;
+ _log = _context.logManager().getLog(OutboundMessageState.class);
+ _pushCount = 0;
+ _maxSends = 0;
+ }
+
+ public synchronized void initialize(OutNetMessage msg) {
+ initialize(msg, msg.getMessage(), null);
+ }
+
+ public void initialize(I2NPMessage msg, PeerState peer) {
+ initialize(null, msg, peer);
+ }
+
+ private void initialize(OutNetMessage m, I2NPMessage msg, PeerState peer) {
+ _message = m;
+ _peer = peer;
+ if (_messageBuf != null) {
+ _cache.release(_messageBuf);
+ _messageBuf = null;
+ }
+
+ _messageBuf = _cache.acquire();
+ int size = msg.getRawMessageSize();
+ if (size > _messageBuf.getData().length)
+ throw new IllegalArgumentException("Size too large! " + size + " in " + msg);
+ int len = msg.toRawByteArray(_messageBuf.getData());
+ _messageBuf.setValid(len);
+ _messageId = msg.getUniqueId();
+
+ _startedOn = _context.clock().now();
+ _nextSendTime = _startedOn;
+ _expiration = _startedOn + 10*1000;
+ //_expiration = msg.getExpiration();
+
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("Raw byte array for " + _messageId + ": " + Base64.encode(_messageBuf.getData(), 0, len));
+ }
+
+ public OutNetMessage getMessage() { return _message; }
+ public long getMessageId() { return _messageId; }
+ public PeerState getPeer() { return _peer; }
+ public boolean isExpired() {
+ return _expiration < _context.clock().now();
+ }
+ public boolean isComplete() {
+ if (_fragmentSends == null) return false;
+ for (int i = 0; i < _fragmentSends.length; i++)
+ if (_fragmentSends[i] >= 0)
+ return false;
+ // nothing else pending ack
+ return true;
+ }
+ public long getLifetime() { return _context.clock().now() - _startedOn; }
+
+ /**
+ * Ack all the fragments in the ack list
+ */
+ public void acked(int ackedFragments[]) {
+ // stupid brute force, but the cardinality should be trivial
+ for (int i = 0; i < ackedFragments.length; i++) {
+ if ( (ackedFragments[i] < 0) || (ackedFragments[i] >= _fragmentSends.length) )
+ continue;
+ _fragmentSends[ackedFragments[i]] = -1;
+ }
+ }
+
+ public long getNextSendTime() { return _nextSendTime; }
+ public void setNextSendTime(long when) { _nextSendTime = when; }
+ public int getMaxSends() { return _maxSends; }
+ public int getPushCount() { return _pushCount; }
+ /** note that we have pushed the message fragments */
+ public void push() { _pushCount++; }
+ public boolean isFragmented() { return _fragmentSends != null; }
+ /**
+ * Prepare the message for fragmented delivery, using no more than
+ * fragmentSize bytes per fragment.
+ *
+ */
+ public void fragment(int fragmentSize) {
+ int totalSize = _messageBuf.getValid();
+ int numFragments = totalSize / fragmentSize;
+ if (numFragments * fragmentSize != totalSize)
+ numFragments++;
+
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("Fragmenting a " + totalSize + " message into " + numFragments + " fragments");
+
+ //_fragmentEnd = new int[numFragments];
+ _fragmentSends = new short[numFragments];
+ //Arrays.fill(_fragmentEnd, -1);
+ Arrays.fill(_fragmentSends, (short)0);
+
+ _fragmentSize = fragmentSize;
+ }
+ /** how many fragments in the message */
+ public int getFragmentCount() {
+ if (_fragmentSends == null)
+ return -1;
+ else
+ return _fragmentSends.length;
+ }
+ /** should we continue sending this fragment? */
+ public boolean shouldSend(int fragmentNum) { return _fragmentSends[fragmentNum] >= (short)0; }
+ public int fragmentSize(int fragmentNum) {
+ if (fragmentNum + 1 == _fragmentSends.length)
+ return _messageBuf.getValid() % _fragmentSize;
+ else
+ return _fragmentSize;
+ }
+
+ /**
+ * Pick a fragment that we still need to send. Current implementation
+ * picks the fragment which has been sent the least (randomly choosing
+ * among equals), incrementing the # sends of the winner in the process.
+ *
+ * @return fragment index, or -1 if all of the fragments were acked
+ */
+ public int pickNextFragment() {
+ short minValue = -1;
+ int minIndex = -1;
+ int startOffset = _context.random().nextInt(_fragmentSends.length);
+ for (int i = 0; i < _fragmentSends.length; i++) {
+ int cur = (i + startOffset) % _fragmentSends.length;
+ if (_fragmentSends[cur] < (short)0)
+ continue;
+ else if ( (minValue < (short)0) || (_fragmentSends[cur] < minValue) ) {
+ minValue = _fragmentSends[cur];
+ minIndex = cur;
+ }
+ }
+ if (minIndex >= 0) {
+ _fragmentSends[minIndex]++;
+ if (_fragmentSends[minIndex] > _maxSends)
+ _maxSends = _fragmentSends[minIndex];
+ }
+
+ // if all fragments have now been sent an equal number of times,
+ // lets give pause for an ACK
+ boolean endOfVolley = true;
+ for (int i = 0; i < _fragmentSends.length; i++) {
+ if (_fragmentSends[i] < (short)0)
+ continue;
+ if (_fragmentSends[i] != (short)_pushCount+1) {
+ endOfVolley = false;
+ break;
+ }
+ }
+ if (endOfVolley)
+ _pushCount++;
+
+
+ if (_log.shouldLog(Log.DEBUG)) {
+ StringBuffer buf = new StringBuffer(64);
+ buf.append("Next fragment is ").append(minIndex);
+ if (minIndex >= 0) {
+ buf.append(" (#sends: ").append(_fragmentSends[minIndex]-1);
+ buf.append(" #fragments: ").append(_fragmentSends.length);
+ buf.append(")");
+ }
+ _log.debug(buf.toString());
+ }
+ return minIndex;
+ }
+
+ /**
+ * Write a part of the the message onto the specified buffer.
+ *
+ * @param out target to write
+ * @param outOffset into outOffset to begin writing
+ * @param fragmentNum fragment to write (0 indexed)
+ * @return bytesWritten
+ */
+ public synchronized int writeFragment(byte out[], int outOffset, int fragmentNum) {
+ int start = _fragmentSize * fragmentNum;
+ int end = start + _fragmentSize;
+ if (end > _messageBuf.getValid())
+ end = _messageBuf.getValid();
+ int toSend = end - start;
+ System.arraycopy(_messageBuf.getData(), start, out, outOffset, toSend);
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("Raw fragment[" + fragmentNum + "] for " + _messageId + ": "
+ + Base64.encode(_messageBuf.getData(), start, toSend));
+ return toSend;
+ }
+
+ public String toString() {
+ StringBuffer buf = new StringBuffer(64);
+ buf.append("Message ").append(_messageId);
+ if (_fragmentSends != null)
+ buf.append(" with ").append(_fragmentSends.length).append(" fragments");
+ return buf.toString();
+ }
+}
diff --git a/router/java/src/net/i2p/router/transport/udp/OutboundRefiller.java b/router/java/src/net/i2p/router/transport/udp/OutboundRefiller.java
new file mode 100644
index 000000000..ee41ad8c0
--- /dev/null
+++ b/router/java/src/net/i2p/router/transport/udp/OutboundRefiller.java
@@ -0,0 +1,62 @@
+package net.i2p.router.transport.udp;
+
+import net.i2p.router.OutNetMessage;
+import net.i2p.router.RouterContext;
+import net.i2p.util.I2PThread;
+import net.i2p.util.Log;
+
+/**
+ * Blocking thread to grab new messages off the outbound queue and
+ * plopping them into our active pool.
+ *
+ */
+public class OutboundRefiller implements Runnable {
+ private RouterContext _context;
+ private Log _log;
+ private OutboundMessageFragments _fragments;
+ private MessageQueue _messages;
+ private boolean _alive;
+ private Object _refillLock;
+
+ public OutboundRefiller(RouterContext ctx, OutboundMessageFragments fragments, MessageQueue messages) {
+ _context = ctx;
+ _log = ctx.logManager().getLog(OutboundRefiller.class);
+ _fragments = fragments;
+ _messages = messages;
+ _refillLock = this;
+ _context.statManager().createRateStat("udp.timeToActive", "Message lifetime until it reaches the outbound fragment queue", "udp", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
+ }
+
+ public void startup() {
+ _alive = true;
+ I2PThread t = new I2PThread(this, "UDP outbound refiller");
+ t.setDaemon(true);
+ t.start();
+ }
+ public void shutdown() { _alive = false; }
+
+ public void run() {
+ while (_alive) {
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("Check the fragments to see if we can add more...");
+ boolean wantMore = _fragments.waitForMoreAllowed();
+ if (wantMore) {
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("Want more fragments...");
+ OutNetMessage msg = _messages.getNext(-1);
+ if (msg != null) {
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("New message found to fragments: " + msg);
+ _context.statManager().addRateData("udp.timeToActive", msg.getLifetime(), msg.getLifetime());
+ _fragments.add(msg);
+ } else {
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("No message found to fragment");
+ }
+ } else {
+ if (_log.shouldLog(Log.WARN))
+ _log.warn("No more fragments allowed, looping");
+ }
+ }
+ }
+}
diff --git a/router/java/src/net/i2p/router/transport/udp/PacketBuilder.java b/router/java/src/net/i2p/router/transport/udp/PacketBuilder.java
new file mode 100644
index 000000000..cdeb89169
--- /dev/null
+++ b/router/java/src/net/i2p/router/transport/udp/PacketBuilder.java
@@ -0,0 +1,445 @@
+package net.i2p.router.transport.udp;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+
+import net.i2p.data.Base64;
+import net.i2p.data.ByteArray;
+import net.i2p.data.DataHelper;
+import net.i2p.data.Hash;
+import net.i2p.data.SessionKey;
+import net.i2p.data.Signature;
+import net.i2p.router.RouterContext;
+import net.i2p.util.ByteCache;
+import net.i2p.util.Log;
+
+/**
+ * Big ol' class to do all our packet formatting. The UDPPackets generated are
+ * fully authenticated, encrypted, and configured for delivery to the peer.
+ *
+ */
+public class PacketBuilder {
+ private RouterContext _context;
+ private Log _log;
+ private UDPTransport _transport;
+
+ private static final ByteCache _ivCache = ByteCache.getInstance(64, UDPPacket.IV_SIZE);
+
+ public PacketBuilder(RouterContext ctx, UDPTransport transport) {
+ _context = ctx;
+ _log = ctx.logManager().getLog(PacketBuilder.class);
+ _transport = transport;
+ }
+
+ public UDPPacket buildPacket(OutboundMessageState state, int fragment, PeerState peer) {
+ UDPPacket packet = UDPPacket.acquire(_context);
+
+ byte data[] = packet.getPacket().getData();
+ Arrays.fill(data, 0, data.length, (byte)0x0);
+ int off = UDPPacket.MAC_SIZE + UDPPacket.IV_SIZE;
+
+ // header
+ data[off] |= (UDPPacket.PAYLOAD_TYPE_DATA << 4);
+ // todo: add support for rekeying and extended options
+ off++;
+ long now = _context.clock().now() / 1000;
+ DataHelper.toLong(data, off, 4, now);
+ off += 4;
+
+ // ok, now for the body...
+
+ // just always ask for an ACK for now...
+ data[off] |= UDPPacket.DATA_FLAG_WANT_REPLY;
+ off++;
+
+ DataHelper.toLong(data, off, 1, 1); // only one fragment in this message
+ off++;
+
+ DataHelper.toLong(data, off, 4, state.getMessageId());
+ off += 4;
+
+ data[off] |= fragment << 3;
+ if (fragment == state.getFragmentCount() - 1)
+ data[off] |= 1 << 2; // isLast
+ off++;
+
+ DataHelper.toLong(data, off, 2, state.fragmentSize(fragment));
+ off += 2;
+
+ off += state.writeFragment(data, off, fragment);
+
+ // we can pad here if we want, maybe randomized?
+
+ // pad up so we're on the encryption boundary
+ if ( (off % 16) != 0)
+ off += 16 - (off % 16);
+ packet.getPacket().setLength(off);
+ authenticate(packet, peer.getCurrentCipherKey(), peer.getCurrentMACKey());
+ setTo(packet, peer.getRemoteIP(), peer.getRemotePort());
+ return packet;
+ }
+
+ public UDPPacket buildACK(PeerState peer, List ackedMessageIds) {
+ UDPPacket packet = UDPPacket.acquire(_context);
+
+ byte data[] = packet.getPacket().getData();
+ Arrays.fill(data, 0, data.length, (byte)0x0);
+ int off = UDPPacket.MAC_SIZE + UDPPacket.IV_SIZE;
+
+ // header
+ data[off] |= (UDPPacket.PAYLOAD_TYPE_DATA << 4);
+ // todo: add support for rekeying and extended options
+ off++;
+ long now = _context.clock().now() / 1000;
+ DataHelper.toLong(data, off, 4, now);
+ off += 4;
+
+ // ok, now for the body...
+ data[off] |= UDPPacket.DATA_FLAG_EXPLICIT_ACK;
+ // add ECN if (peer.getSomethingOrOther())
+ off++;
+
+ DataHelper.toLong(data, off, 1, ackedMessageIds.size());
+ off++;
+ for (int i = 0; i < ackedMessageIds.size(); i++) {
+ Long id = (Long)ackedMessageIds.get(i);
+ DataHelper.toLong(data, off, 4, id.longValue());
+ off += 4;
+ }
+
+ DataHelper.toLong(data, off, 1, 0); // no fragments in this message
+ off++;
+
+ // we can pad here if we want, maybe randomized?
+
+ // pad up so we're on the encryption boundary
+ if ( (off % 16) != 0)
+ off += 16 - (off % 16);
+ packet.getPacket().setLength(off);
+ authenticate(packet, peer.getCurrentCipherKey(), peer.getCurrentMACKey());
+ setTo(packet, peer.getRemoteIP(), peer.getRemotePort());
+ return packet;
+ }
+
+ /**
+ * full flag info for a sessionCreated message. this can be fixed,
+ * since we never rekey on startup, and don't need any extended options
+ */
+ private static final byte SESSION_CREATED_FLAG_BYTE = (UDPPacket.PAYLOAD_TYPE_SESSION_CREATED << 4);
+
+ /**
+ * 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
+ */
+ public UDPPacket buildSessionCreatedPacket(InboundEstablishState state) {
+ UDPPacket packet = UDPPacket.acquire(_context);
+ try {
+ packet.getPacket().setAddress(InetAddress.getByAddress(state.getSentIP()));
+ } catch (UnknownHostException uhe) {
+ if (_log.shouldLog(Log.ERROR))
+ _log.error("How did we think this was a valid IP? " + state.getRemoteHostInfo());
+ return null;
+ }
+
+ state.prepareSessionCreated();
+
+ byte data[] = packet.getPacket().getData();
+ Arrays.fill(data, 0, data.length, (byte)0x0);
+ int off = UDPPacket.MAC_SIZE + UDPPacket.IV_SIZE;
+
+ // header
+ data[off] = SESSION_CREATED_FLAG_BYTE;
+ off++;
+ long now = _context.clock().now() / 1000;
+ DataHelper.toLong(data, off, 4, now);
+ off += 4;
+
+ // now for the body
+ System.arraycopy(state.getSentY(), 0, data, off, state.getSentY().length);
+ off += state.getSentY().length;
+ DataHelper.toLong(data, off, 1, state.getSentIP().length);
+ off += 1;
+ System.arraycopy(state.getSentIP(), 0, data, off, state.getSentIP().length);
+ off += state.getSentIP().length;
+ DataHelper.toLong(data, off, 2, state.getSentPort());
+ off += 2;
+ DataHelper.toLong(data, off, 4, state.getSentRelayTag());
+ off += 4;
+ DataHelper.toLong(data, off, 4, state.getSentSignedOnTime());
+ off += 4;
+ System.arraycopy(state.getSentSignature().getData(), 0, data, off, Signature.SIGNATURE_BYTES);
+ off += Signature.SIGNATURE_BYTES;
+ // ok, we need another 8 bytes of random padding
+ // (ok, this only gives us 63 bits, not 64)
+ long l = _context.random().nextLong();
+ if (l < 0) l = 0 - l;
+ DataHelper.toLong(data, off, 8, l);
+ off += 8;
+
+ if (_log.shouldLog(Log.DEBUG)) {
+ StringBuffer buf = new StringBuffer(128);
+ buf.append("Sending sessionCreated:");
+ buf.append(" AliceIP: ").append(Base64.encode(state.getSentIP()));
+ buf.append(" AlicePort: ").append(state.getSentPort());
+ buf.append(" BobIP: ").append(Base64.encode(state.getReceivedOurIP()));
+ buf.append(" BobPort: ").append(_transport.getExternalPort());
+ buf.append(" RelayTag: ").append(state.getSentRelayTag());
+ buf.append(" SignedOn: ").append(state.getSentSignedOnTime());
+ buf.append(" signature: ").append(Base64.encode(state.getSentSignature().getData()));
+ buf.append("\nRawCreated: ").append(Base64.encode(data, 0, off));
+ buf.append("\nsignedTime: ").append(Base64.encode(data, off-8-Signature.SIGNATURE_BYTES-4, 4));
+ _log.debug(buf.toString());
+ }
+
+ // ok, now the full data is in there, but we also need to encrypt
+ // the signature, which means we need the IV
+ ByteArray iv = _ivCache.acquire();
+ _context.random().nextBytes(iv.getData());
+
+ int encrWrite = Signature.SIGNATURE_BYTES + 8;
+ int sigBegin = off - encrWrite;
+ _context.aes().encrypt(data, sigBegin, data, sigBegin, state.getCipherKey(), iv.getData(), encrWrite);
+
+ // pad up so we're on the encryption boundary
+ if ( (off % 16) != 0)
+ off += 16 - (off % 16);
+ packet.getPacket().setLength(off);
+ authenticate(packet, _transport.getIntroKey(), _transport.getIntroKey(), iv);
+ setTo(packet, state.getSentIP(), state.getSentPort());
+ _ivCache.release(iv);
+ return packet;
+ }
+
+ /**
+ * full flag info for a sessionRequest message. this can be fixed,
+ * since we never rekey on startup, and don't need any extended options
+ */
+ private static final byte SESSION_REQUEST_FLAG_BYTE = (UDPPacket.PAYLOAD_TYPE_SESSION_REQUEST << 4);
+
+ /**
+ * 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
+ */
+ public UDPPacket buildSessionRequestPacket(OutboundEstablishState state) {
+ UDPPacket packet = UDPPacket.acquire(_context);
+ try {
+ packet.getPacket().setAddress(InetAddress.getByAddress(state.getSentIP()));
+ } catch (UnknownHostException uhe) {
+ if (_log.shouldLog(Log.ERROR))
+ _log.error("How did we think this was a valid IP? " + state.getRemoteHostInfo());
+ return null;
+ }
+
+ byte data[] = packet.getPacket().getData();
+ Arrays.fill(data, 0, data.length, (byte)0x0);
+ int off = UDPPacket.MAC_SIZE + UDPPacket.IV_SIZE;
+
+ // header
+ data[off] = SESSION_REQUEST_FLAG_BYTE;
+ off++;
+ long now = _context.clock().now() / 1000;
+ DataHelper.toLong(data, off, 4, now);
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("Sending request with time = " + new Date(now*1000));
+ off += 4;
+
+ // now for the body
+ System.arraycopy(state.getSentX(), 0, data, off, state.getSentX().length);
+ off += state.getSentX().length;
+ DataHelper.toLong(data, off, 1, state.getSentIP().length);
+ off += 1;
+ System.arraycopy(state.getSentIP(), 0, data, off, state.getSentIP().length);
+ off += state.getSentIP().length;
+ DataHelper.toLong(data, off, 2, state.getSentPort());
+ off += 2;
+
+ // we can pad here if we want, maybe randomized?
+
+ // pad up so we're on the encryption boundary
+ if ( (off % 16) != 0)
+ off += 16 - (off % 16);
+ packet.getPacket().setLength(off);
+ authenticate(packet, state.getIntroKey(), state.getIntroKey());
+ setTo(packet, state.getSentIP(), state.getSentPort());
+ return packet;
+ }
+
+ private static final int MAX_IDENTITY_FRAGMENT_SIZE = 512;
+
+ /**
+ * Build a new series of SessionConfirmed packets for the given peer,
+ * encrypting it as necessary.
+ *
+ * @return ready to send packets, or null if there was a problem
+ */
+ public UDPPacket[] buildSessionConfirmedPackets(OutboundEstablishState state) {
+ byte identity[] = _context.router().getRouterInfo().getIdentity().toByteArray();
+ int numFragments = identity.length / MAX_IDENTITY_FRAGMENT_SIZE;
+ if (numFragments * MAX_IDENTITY_FRAGMENT_SIZE != identity.length)
+ numFragments++;
+ UDPPacket packets[] = new UDPPacket[numFragments];
+ for (int i = 0; i < numFragments; i++)
+ packets[i] = buildSessionConfirmedPacket(state, i, numFragments, identity);
+ return packets;
+ }
+
+
+ /**
+ * full flag info for a sessionConfirmed message. this can be fixed,
+ * since we never rekey on startup, and don't need any extended options
+ */
+ private static final byte SESSION_CONFIRMED_FLAG_BYTE = (UDPPacket.PAYLOAD_TYPE_SESSION_CONFIRMED << 4);
+
+ /**
+ * Build a new SessionConfirmed packet for the given peer
+ *
+ * @return ready to send packets, or null if there was a problem
+ */
+ public UDPPacket buildSessionConfirmedPacket(OutboundEstablishState state, int fragmentNum, int numFragments, byte identity[]) {
+ UDPPacket packet = UDPPacket.acquire(_context);
+ try {
+ packet.getPacket().setAddress(InetAddress.getByAddress(state.getSentIP()));
+ } catch (UnknownHostException uhe) {
+ if (_log.shouldLog(Log.ERROR))
+ _log.error("How did we think this was a valid IP? " + state.getRemoteHostInfo());
+ return null;
+ }
+
+ byte data[] = packet.getPacket().getData();
+ Arrays.fill(data, 0, data.length, (byte)0x0);
+ int off = UDPPacket.MAC_SIZE + UDPPacket.IV_SIZE;
+
+ // header
+ data[off] = SESSION_CONFIRMED_FLAG_BYTE;
+ off++;
+ long now = _context.clock().now() / 1000;
+ DataHelper.toLong(data, off, 4, now);
+ off += 4;
+
+ // now for the body
+ data[off] |= fragmentNum << 4;
+ data[off] |= (numFragments & 0xF);
+ off++;
+
+ int curFragSize = MAX_IDENTITY_FRAGMENT_SIZE;
+ if (fragmentNum == numFragments-1) {
+ if (identity.length % MAX_IDENTITY_FRAGMENT_SIZE != 0)
+ curFragSize = identity.length % MAX_IDENTITY_FRAGMENT_SIZE;
+ }
+
+ DataHelper.toLong(data, off, 2, curFragSize);
+ off += 2;
+
+ int curFragOffset = fragmentNum * MAX_IDENTITY_FRAGMENT_SIZE;
+ System.arraycopy(identity, curFragOffset, data, off, curFragSize);
+ off += curFragSize;
+
+ if (fragmentNum == numFragments - 1) {
+ DataHelper.toLong(data, off, 4, state.getSentSignedOnTime());
+ off += 4;
+
+ int paddingRequired = 0;
+ // we need to pad this so we're at the encryption boundary
+ if ( (off + Signature.SIGNATURE_BYTES) % 16 != 0)
+ paddingRequired += 16 - ((off + Signature.SIGNATURE_BYTES) % 16);
+
+ // add an arbitrary number of 16byte pad blocks too...
+
+ for (int i = 0; i < paddingRequired; i++) {
+ data[off] = (byte)_context.random().nextInt(255);
+ off++;
+ }
+
+ System.arraycopy(state.getSentSignature().getData(), 0, data, off, Signature.SIGNATURE_BYTES);
+ packet.getPacket().setLength(off + Signature.SIGNATURE_BYTES);
+ authenticate(packet, state.getCipherKey(), state.getMACKey());
+ } else {
+ // nothing more to add beyond the identity fragment, though we can
+ // pad here if we want. maybe randomized?
+
+ // pad up so we're on the encryption boundary
+ if ( (off % 16) != 0)
+ off += 16 - (off % 16);
+ packet.getPacket().setLength(off);
+ authenticate(packet, state.getIntroKey(), state.getIntroKey());
+ }
+
+ setTo(packet, state.getSentIP(), state.getSentPort());
+ return packet;
+ }
+
+ private void setTo(UDPPacket packet, byte ip[], int port) {
+ try {
+ InetAddress to = InetAddress.getByAddress(ip);
+ packet.getPacket().setAddress(to);
+ packet.getPacket().setPort(port);
+ } catch (UnknownHostException uhe) {
+ if (_log.shouldLog(Log.ERROR))
+ _log.error("Invalid IP? ", uhe);
+ }
+ }
+
+ /**
+ * Encrypt the packet with the cipher key and a new random IV, generate a
+ * MAC for that encrypted data and IV, and store the result in the packet.
+ *
+ * @param packet prepared packet with the first 32 bytes empty and a length
+ * whose size is mod 16
+ * @param cipherKey key to encrypt the payload
+ * @param macKey key to generate the, er, MAC
+ */
+ private void authenticate(UDPPacket packet, SessionKey cipherKey, SessionKey macKey) {
+ ByteArray iv = _ivCache.acquire();
+ _context.random().nextBytes(iv.getData());
+ authenticate(packet, cipherKey, macKey, iv);
+ _ivCache.release(iv);
+ }
+
+ /**
+ * Encrypt the packet with the cipher key and the given IV, generate a
+ * MAC for that encrypted data and IV, and store the result in the packet.
+ * The MAC used is:
+ * HMAC-SHA256(payload || IV || payloadLength, macKey)[0:15]
+ *
+ * @param packet prepared packet with the first 32 bytes empty and a length
+ * whose size is mod 16
+ * @param cipherKey key to encrypt the payload
+ * @param macKey key to generate the, er, MAC
+ * @param iv IV to deliver
+ */
+ private void authenticate(UDPPacket packet, SessionKey cipherKey, SessionKey macKey, ByteArray iv) {
+ int encryptOffset = packet.getPacket().getOffset() + UDPPacket.IV_SIZE + UDPPacket.MAC_SIZE;
+ int encryptSize = packet.getPacket().getLength() - UDPPacket.IV_SIZE - UDPPacket.MAC_SIZE - packet.getPacket().getOffset();
+ byte data[] = packet.getPacket().getData();
+ _context.aes().encrypt(data, encryptOffset, data, encryptOffset, cipherKey, iv.getData(), encryptSize);
+
+ // ok, now we need to prepare things for the MAC, which requires reordering
+ int off = packet.getPacket().getOffset();
+ System.arraycopy(data, encryptOffset, data, off, encryptSize);
+ off += encryptSize;
+ System.arraycopy(iv.getData(), 0, data, off, UDPPacket.IV_SIZE);
+ off += UDPPacket.IV_SIZE;
+ DataHelper.toLong(data, off, 2, encryptSize);
+
+ int hmacOff = packet.getPacket().getOffset();
+ int hmacLen = encryptSize + UDPPacket.IV_SIZE + 2;
+ Hash hmac = _context.hmac().calculate(macKey, data, hmacOff, hmacLen);
+
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("Authenticating " + packet.getPacket().getLength() +
+ "\nIV: " + Base64.encode(iv.getData()) +
+ "\nraw mac: " + hmac.toBase64() +
+ "\nMAC key: " + macKey.toBase64());
+ // ok, now lets put it back where it belongs...
+ System.arraycopy(data, hmacOff, data, encryptOffset, encryptSize);
+ System.arraycopy(hmac.getData(), 0, data, hmacOff, UDPPacket.MAC_SIZE);
+ System.arraycopy(iv.getData(), 0, data, hmacOff + UDPPacket.MAC_SIZE, UDPPacket.IV_SIZE);
+ }
+}
diff --git a/router/java/src/net/i2p/router/transport/udp/PacketHandler.java b/router/java/src/net/i2p/router/transport/udp/PacketHandler.java
new file mode 100644
index 000000000..dacd2d8d7
--- /dev/null
+++ b/router/java/src/net/i2p/router/transport/udp/PacketHandler.java
@@ -0,0 +1,293 @@
+package net.i2p.router.transport.udp;
+
+import java.net.InetAddress;
+import java.util.Date;
+
+import net.i2p.data.Base64;
+import net.i2p.router.Router;
+import net.i2p.router.RouterContext;
+import net.i2p.util.I2PThread;
+import net.i2p.util.Log;
+
+/**
+ * Pull inbound packets from the inbound receiver's queue, figure out what
+ * peer session they belong to (if any), authenticate and decrypt them
+ * with the appropriate keys, and push them to the appropriate handler.
+ * Data and ACK packets go to the InboundMessageFragments, the various
+ * establishment packets go to the EstablishmentManager, and, once implemented,
+ * relay packets will go to the relay manager. At the moment, this is
+ * an actual pool of packet handler threads, each pulling off the inbound
+ * receiver's queue and pushing them as necessary.
+ *
+ */
+public class PacketHandler implements Runnable {
+ private RouterContext _context;
+ private Log _log;
+ private UDPTransport _transport;
+ private UDPEndpoint _endpoint;
+ private UDPPacketReader _reader;
+ private EstablishmentManager _establisher;
+ private InboundMessageFragments _inbound;
+ private boolean _keepReading;
+
+ private static final int NUM_HANDLERS = 3;
+
+ public PacketHandler(RouterContext ctx, UDPTransport transport, UDPEndpoint endpoint, EstablishmentManager establisher, InboundMessageFragments inbound) {
+ _context = ctx;
+ _log = ctx.logManager().getLog(PacketHandler.class);
+ _transport = transport;
+ _endpoint = endpoint;
+ _establisher = establisher;
+ _inbound = inbound;
+ _reader = new UDPPacketReader(ctx);
+ _context.statManager().createRateStat("udp.handleTime", "How long it takes to handle a received packet after its been pulled off the queue", "udp", new long[] { 10*60*1000, 60*60*1000 });
+ _context.statManager().createRateStat("udp.queueTime", "How long after a packet is received can we begin handling it", "udp", new long[] { 10*60*1000, 60*60*1000 });
+ _context.statManager().createRateStat("udp.receivePacketSkew", "How long ago after the packet was sent did we receive it", "udp", new long[] { 10*60*1000, 60*60*1000 });
+ }
+
+ public void startup() {
+ _keepReading = true;
+ for (int i = 0; i < NUM_HANDLERS; i++) {
+ I2PThread t = new I2PThread(this, "Packet handler " + i + ": " + _endpoint.getListenPort());
+ t.setDaemon(true);
+ t.start();
+ }
+ }
+
+ public void shutdown() {
+ _keepReading = false;
+ }
+
+ public void run() {
+ while (_keepReading) {
+ UDPPacket packet = _endpoint.receive();
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("Received the packet " + packet);
+ long queueTime = packet.getLifetime();
+ long handleStart = _context.clock().now();
+ handlePacket(packet);
+ long handleTime = _context.clock().now() - handleStart;
+ _context.statManager().addRateData("udp.handleTime", handleTime, packet.getLifetime());
+ _context.statManager().addRateData("udp.queueTime", queueTime, packet.getLifetime());
+
+ if (handleTime > 1000) {
+ if (_log.shouldLog(Log.WARN))
+ _log.warn("Took " + handleTime + " to process the packet "
+ + packet + ": " + _reader);
+ }
+
+ // back to the cache with thee!
+ packet.release();
+ }
+ }
+
+ private void handlePacket(UDPPacket packet) {
+ if (packet == null) return;
+
+ InetAddress remAddr = packet.getPacket().getAddress();
+ int remPort = packet.getPacket().getPort();
+ PeerState state = _transport.getPeerState(remAddr, remPort);
+ if (state == null) {
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("Packet received is not for a connected peer");
+ InboundEstablishState est = _establisher.getInboundState(remAddr, remPort);
+ if (est != null) {
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("Packet received IS for an inbound establishment");
+ receivePacket(packet, est);
+ } else {
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("Packet received is not for an inbound establishment");
+ OutboundEstablishState oest = _establisher.getOutboundState(remAddr, remPort);
+ if (oest != null) {
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("Packet received IS for an outbound establishment");
+ receivePacket(packet, oest);
+ } else {
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("Packet received is not for an inbound or outbound establishment");
+ // ok, not already known establishment, try as a new one
+ receivePacket(packet);
+ }
+ }
+ } else {
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("Packet received IS for an existing peer");
+ receivePacket(packet, state);
+ }
+ }
+
+ private void receivePacket(UDPPacket packet, PeerState state) {
+ boolean isValid = packet.validate(state.getCurrentMACKey());
+ if (!isValid) {
+ if (state.getNextMACKey() != null)
+ isValid = packet.validate(state.getNextMACKey());
+ if (!isValid) {
+ if (_log.shouldLog(Log.WARN))
+ _log.warn("Failed validation with existing con, trying as new con: " + packet);
+
+ isValid = packet.validate(_transport.getIntroKey());
+ if (isValid) {
+ // this is a stray packet from an inbound establishment
+ // process, so try our intro key
+ // (after an outbound establishment process, there wouldn't
+ // be any stray packets)
+ if (_log.shouldLog(Log.INFO))
+ _log.info("Validation with existing con failed, but validation as reestablish/stray passed");
+ packet.decrypt(_transport.getIntroKey());
+ } else {
+ if (_log.shouldLog(Log.WARN))
+ _log.warn("Validation with existing con failed, and validation as reestablish failed too. DROP");
+ return;
+ }
+ } else {
+ packet.decrypt(state.getNextCipherKey());
+ }
+ } else {
+ packet.decrypt(state.getCurrentCipherKey());
+ }
+
+ handlePacket(packet, state, null, null);
+ }
+
+ private void receivePacket(UDPPacket packet) {
+ boolean isValid = packet.validate(_transport.getIntroKey());
+ if (!isValid) {
+ if (_log.shouldLog(Log.WARN))
+ _log.warn("Invalid introduction packet received: " + packet, new Exception("path"));
+ return;
+ } else {
+ if (_log.shouldLog(Log.INFO))
+ _log.info("Valid introduction packet received: " + packet);
+ }
+
+ packet.decrypt(_transport.getIntroKey());
+ handlePacket(packet, null, null, null);
+ }
+
+ private void receivePacket(UDPPacket packet, InboundEstablishState state) {
+ if ( (state != null) && (_log.shouldLog(Log.DEBUG)) ) {
+ StringBuffer buf = new StringBuffer(128);
+ buf.append("Attempting to receive a packet on a known inbound state: ");
+ buf.append(state);
+ buf.append(" MAC key: ").append(state.getMACKey());
+ buf.append(" intro key: ").append(_transport.getIntroKey());
+ _log.debug(buf.toString());
+ }
+ boolean isValid = false;
+ if (state.getMACKey() != null) {
+ isValid = packet.validate(state.getMACKey());
+ if (isValid) {
+ if (_log.shouldLog(Log.WARN))
+ _log.warn("Valid introduction packet received for inbound con: " + packet);
+
+ packet.decrypt(state.getCipherKey());
+ handlePacket(packet, null, null, null);
+ return;
+ } else {
+ if (_log.shouldLog(Log.WARN))
+ _log.warn("Invalid introduction packet received for inbound con, falling back: " + packet);
+
+ }
+ }
+ // ok, we couldn't handle it with the established stuff, so fall back
+ // on earlier state packets
+ receivePacket(packet);
+ }
+
+ private void receivePacket(UDPPacket packet, OutboundEstablishState state) {
+ if ( (state != null) && (_log.shouldLog(Log.DEBUG)) ) {
+ StringBuffer buf = new StringBuffer(128);
+ buf.append("Attempting to receive a packet on a known outbound state: ");
+ buf.append(state);
+ buf.append(" MAC key: ").append(state.getMACKey());
+ buf.append(" intro key: ").append(state.getIntroKey());
+ _log.debug(buf.toString());
+ }
+
+ boolean isValid = false;
+ if (state.getMACKey() != null) {
+ isValid = packet.validate(state.getMACKey());
+ if (isValid) {
+ if (_log.shouldLog(Log.WARN))
+ _log.warn("Valid introduction packet received for outbound established con: " + packet);
+
+ packet.decrypt(state.getCipherKey());
+ handlePacket(packet, null, state, null);
+ return;
+ }
+ }
+
+ // keys not yet exchanged, lets try it with the peer's intro key
+ isValid = packet.validate(state.getIntroKey());
+ if (isValid) {
+ if (_log.shouldLog(Log.WARN))
+ _log.warn("Valid introduction packet received for outbound established con with old intro key: " + packet);
+ packet.decrypt(state.getIntroKey());
+ handlePacket(packet, null, state, null);
+ return;
+ } else {
+ if (_log.shouldLog(Log.WARN))
+ _log.warn("Invalid introduction packet received for outbound established con with old intro key, falling back: " + packet);
+ }
+
+ // ok, we couldn't handle it with the established stuff, so fall back
+ // on earlier state packets
+ receivePacket(packet);
+ }
+
+ /** let packets be up to 30s slow */
+ private static final long GRACE_PERIOD = Router.CLOCK_FUDGE_FACTOR + 30*1000;
+
+ /**
+ * Parse out the interesting bits and honor what it says
+ */
+ private void handlePacket(UDPPacket packet, PeerState state, OutboundEstablishState outState, InboundEstablishState inState) {
+ _reader.initialize(packet);
+ long now = _context.clock().now();
+ long when = _reader.readTimestamp() * 1000;
+ long skew = now - when;
+ if (skew > GRACE_PERIOD) {
+ if (_log.shouldLog(Log.WARN))
+ _log.warn("Packet too far in the future: " + new Date(when) + ": " + packet);
+ return;
+ } else if (skew < 0 - GRACE_PERIOD) {
+ if (_log.shouldLog(Log.WARN))
+ _log.warn("Packet too far in the past: " + new Date(when) + ": " + packet);
+ return;
+ }
+
+ _context.statManager().addRateData("udp.receivePacketSkew", skew, packet.getLifetime());
+
+ InetAddress fromHost = packet.getPacket().getAddress();
+ int fromPort = packet.getPacket().getPort();
+ String from = PeerState.calculateRemoteHostString(fromHost.getAddress(), fromPort);
+
+ switch (_reader.readPayloadType()) {
+ case UDPPacket.PAYLOAD_TYPE_SESSION_REQUEST:
+ _establisher.receiveSessionRequest(from, fromHost, fromPort, _reader);
+ break;
+ case UDPPacket.PAYLOAD_TYPE_SESSION_CONFIRMED:
+ _establisher.receiveSessionConfirmed(from, _reader);
+ break;
+ case UDPPacket.PAYLOAD_TYPE_SESSION_CREATED:
+ _establisher.receiveSessionCreated(from, _reader);
+ break;
+ case UDPPacket.PAYLOAD_TYPE_DATA:
+ if (outState != null)
+ state = _establisher.receiveData(outState);
+ handleData(packet, state);
+ break;
+ default:
+ if (_log.shouldLog(Log.WARN))
+ _log.warn("Unknown payload type: " + _reader.readPayloadType());
+ return;
+ }
+ }
+
+ private void handleData(UDPPacket packet, PeerState peer) {
+ if (_log.shouldLog(Log.INFO))
+ _log.info("Received new DATA packet from " + peer + ": " + packet);
+ _inbound.receiveData(peer, _reader.getDataReader());
+ }
+}
diff --git a/router/java/src/net/i2p/router/transport/udp/PacketPusher.java b/router/java/src/net/i2p/router/transport/udp/PacketPusher.java
new file mode 100644
index 000000000..392f70299
--- /dev/null
+++ b/router/java/src/net/i2p/router/transport/udp/PacketPusher.java
@@ -0,0 +1,43 @@
+package net.i2p.router.transport.udp;
+
+import net.i2p.router.OutNetMessage;
+import net.i2p.router.RouterContext;
+import net.i2p.util.I2PThread;
+import net.i2p.util.Log;
+
+/**
+ * Blocking thread to grab new packets off the outbound fragment
+ * pool and toss 'em onto the outbound packet queue
+ *
+ */
+public class PacketPusher implements Runnable {
+ private RouterContext _context;
+ private Log _log;
+ private OutboundMessageFragments _fragments;
+ private UDPSender _sender;
+ private boolean _alive;
+
+ public PacketPusher(RouterContext ctx, OutboundMessageFragments fragments, UDPSender sender) {
+ _context = ctx;
+ _log = ctx.logManager().getLog(PacketPusher.class);
+ _fragments = fragments;
+ _sender = sender;
+ }
+
+ public void startup() {
+ _alive = true;
+ I2PThread t = new I2PThread(this, "UDP packet pusher");
+ t.setDaemon(true);
+ t.start();
+ }
+
+ public void shutdown() { _alive = false; }
+
+ public void run() {
+ while (_alive) {
+ UDPPacket packet = _fragments.getNextPacket();
+ if (packet != null)
+ _sender.add(packet, true); // blocks
+ }
+ }
+}
diff --git a/router/java/src/net/i2p/router/transport/udp/PeerState.java b/router/java/src/net/i2p/router/transport/udp/PeerState.java
new file mode 100644
index 000000000..33cfde591
--- /dev/null
+++ b/router/java/src/net/i2p/router/transport/udp/PeerState.java
@@ -0,0 +1,438 @@
+package net.i2p.router.transport.udp;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import java.net.InetAddress;
+
+import net.i2p.I2PAppContext;
+import net.i2p.data.Hash;
+import net.i2p.data.SessionKey;
+import net.i2p.util.Log;
+
+/**
+ * Contain all of the state about a UDP connection to a peer
+ *
+ */
+public class PeerState {
+ private I2PAppContext _context;
+ private Log _log;
+ /**
+ * The peer are we talking to. This should be set as soon as this
+ * state is created if we are initiating a connection, but if we are
+ * receiving the connection this will be set only after the connection
+ * is established.
+ */
+ private Hash _remotePeer;
+ /**
+ * The AES key used to verify packets, set only after the connection is
+ * established.
+ */
+ private SessionKey _currentMACKey;
+ /**
+ * The AES key used to encrypt/decrypt packets, set only after the
+ * connection is established.
+ */
+ private SessionKey _currentCipherKey;
+ /**
+ * The pending AES key for verifying packets if we are rekeying the
+ * connection, or null if we are not in the process of rekeying.
+ */
+ private SessionKey _nextMACKey;
+ /**
+ * The pending AES key for encrypting/decrypting packets if we are
+ * rekeying the connection, or null if we are not in the process
+ * of rekeying.
+ */
+ private SessionKey _nextCipherKey;
+ /**
+ * The keying material used for the rekeying, or null if we are not in
+ * the process of rekeying.
+ */
+ private byte[] _nextKeyingMaterial;
+ /** true if we began the current rekeying, false otherwise */
+ private boolean _rekeyBeganLocally;
+ /** when were the current cipher and MAC keys established/rekeyed? */
+ private long _keyEstablishedTime;
+ /** how far off is the remote peer from our clock, in seconds? */
+ private short _clockSkew;
+ /** what is the current receive second, for congestion control? */
+ private long _currentReceiveSecond;
+ /** when did we last send them a packet? */
+ private long _lastSendTime;
+ /** when did we last receive a packet from them? */
+ private long _lastReceiveTime;
+ /** how many seconds have we sent packets without any ACKs received? */
+ private int _consecutiveSendingSecondsWithoutACKs;
+ /** list of messageIds (Long) that we have received but not yet sent */
+ private List _currentACKs;
+ /** when did we last send ACKs to the peer? */
+ private long _lastACKSend;
+ /** have we received a packet with the ECN bit set in the current second? */
+ private boolean _currentSecondECNReceived;
+ /**
+ * have all of the packets received in the current second requested that
+ * the previous second's ACKs be sent?
+ */
+ private boolean _remoteWantsPreviousACKs;
+ /** how many bytes should we send to the peer in a second */
+ private int _sendWindowBytes;
+ /** how many bytes can we send to the peer in the current second */
+ private int _sendWindowBytesRemaining;
+ /** what IP is the peer sending and receiving packets on? */
+ private byte[] _remoteIP;
+ /** what port is the peer sending and receiving packets on? */
+ private int _remotePort;
+ /** cached remoteIP + port, used to find the peerState by remote info */
+ private String _remoteHostString;
+ /** if we need to contact them, do we need to talk to an introducer? */
+ private boolean _remoteRequiresIntroduction;
+ /**
+ * if we are serving as an introducer to them, this is the the tag that
+ * they can publish that, when presented to us, will cause us to send
+ * a relay introduction to the current peer
+ */
+ private long _weRelayToThemAs;
+ /**
+ * If they have offered to serve as an introducer to us, this is the tag
+ * we can use to publish that fact.
+ */
+ private long _theyRelayToUsAs;
+ /** what is the largest packet we can send to the peer? */
+ private int _mtu;
+ /** when did we last check the MTU? */
+ private long _mtuLastChecked;
+
+ private long _messagesReceived;
+ private long _messagesSent;
+
+ private static final int DEFAULT_SEND_WINDOW_BYTES = 16*1024;
+ private static final int MINIMUM_WINDOW_BYTES = DEFAULT_SEND_WINDOW_BYTES;
+ private static final int DEFAULT_MTU = 512;
+
+ public PeerState(I2PAppContext ctx) {
+ _context = ctx;
+ _log = ctx.logManager().getLog(PeerState.class);
+ _remotePeer = null;
+ _currentMACKey = null;
+ _currentCipherKey = null;
+ _nextMACKey = null;
+ _nextCipherKey = null;
+ _nextKeyingMaterial = null;
+ _rekeyBeganLocally = false;
+ _keyEstablishedTime = -1;
+ _clockSkew = Short.MIN_VALUE;
+ _currentReceiveSecond = -1;
+ _lastSendTime = -1;
+ _lastReceiveTime = -1;
+ _currentACKs = new ArrayList(8);
+ _currentSecondECNReceived = false;
+ _remoteWantsPreviousACKs = false;
+ _sendWindowBytes = DEFAULT_SEND_WINDOW_BYTES;
+ _sendWindowBytesRemaining = DEFAULT_SEND_WINDOW_BYTES;
+ _remoteIP = null;
+ _remotePort = -1;
+ _remoteRequiresIntroduction = false;
+ _weRelayToThemAs = 0;
+ _theyRelayToUsAs = 0;
+ _mtu = DEFAULT_MTU;
+ _mtuLastChecked = -1;
+ _lastACKSend = -1;
+ _messagesReceived = 0;
+ _messagesSent = 0;
+ }
+
+ /**
+ * The peer are we talking to. This should be set as soon as this
+ * state is created if we are initiating a connection, but if we are
+ * receiving the connection this will be set only after the connection
+ * is established.
+ */
+ public Hash getRemotePeer() { return _remotePeer; }
+ /**
+ * The AES key used to verify packets, set only after the connection is
+ * established.
+ */
+ public SessionKey getCurrentMACKey() { return _currentMACKey; }
+ /**
+ * The AES key used to encrypt/decrypt packets, set only after the
+ * connection is established.
+ */
+ public SessionKey getCurrentCipherKey() { return _currentCipherKey; }
+ /**
+ * The pending AES key for verifying packets if we are rekeying the
+ * connection, or null if we are not in the process of rekeying.
+ */
+ public SessionKey getNextMACKey() { return _nextMACKey; }
+ /**
+ * The pending AES key for encrypting/decrypting packets if we are
+ * rekeying the connection, or null if we are not in the process
+ * of rekeying.
+ */
+ public SessionKey getNextCipherKey() { return _nextCipherKey; }
+ /**
+ * The keying material used for the rekeying, or null if we are not in
+ * the process of rekeying.
+ */
+ public byte[] getNextKeyingMaterial() { return _nextKeyingMaterial; }
+ /** true if we began the current rekeying, false otherwise */
+ public boolean getRekeyBeganLocally() { return _rekeyBeganLocally; }
+ /** when were the current cipher and MAC keys established/rekeyed? */
+ public long getKeyEstablishedTime() { return _keyEstablishedTime; }
+ /** how far off is the remote peer from our clock, in seconds? */
+ public short getClockSkew() { return _clockSkew; }
+ /** what is the current receive second, for congestion control? */
+ public long getCurrentReceiveSecond() { return _currentReceiveSecond; }
+ /** when did we last send them a packet? */
+ public long getLastSendTime() { return _lastSendTime; }
+ /** when did we last receive a packet from them? */
+ public long getLastReceiveTime() { return _lastReceiveTime; }
+ /** how many seconds have we sent packets without any ACKs received? */
+ public int getConsecutiveSendingSecondsWithoutACKS() { return _consecutiveSendingSecondsWithoutACKs; }
+ /** have we received a packet with the ECN bit set in the current second? */
+ public boolean getCurrentSecondECNReceived() { return _currentSecondECNReceived; }
+ /**
+ * have all of the packets received in the current second requested that
+ * the previous second's ACKs be sent?
+ */
+ public boolean getRemoteWantsPreviousACKs() { return _remoteWantsPreviousACKs; }
+ /** how many bytes should we send to the peer in a second */
+ public int getSendWindowBytes() { return _sendWindowBytes; }
+ /** how many bytes can we send to the peer in the current second */
+ public int getSendWindowBytesRemaining() { return _sendWindowBytesRemaining; }
+ /** what IP is the peer sending and receiving packets on? */
+ public byte[] getRemoteIP() { return _remoteIP; }
+ /** what port is the peer sending and receiving packets on? */
+ public int getRemotePort() { return _remotePort; }
+ /** if we need to contact them, do we need to talk to an introducer? */
+ public boolean getRemoteRequiresIntroduction() { return _remoteRequiresIntroduction; }
+ /**
+ * if we are serving as an introducer to them, this is the the tag that
+ * they can publish that, when presented to us, will cause us to send
+ * a relay introduction to the current peer
+ */
+ public long getWeRelayToThemAs() { return _weRelayToThemAs; }
+ /**
+ * If they have offered to serve as an introducer to us, this is the tag
+ * we can use to publish that fact.
+ */
+ public long getTheyRelayToUsAs() { return _theyRelayToUsAs; }
+ /** what is the largest packet we can send to the peer? */
+ public int getMTU() { return _mtu; }
+ /** when did we last check the MTU? */
+ public long getMTULastChecked() { return _mtuLastChecked; }
+
+
+ /**
+ * The peer are we talking to. This should be set as soon as this
+ * state is created if we are initiating a connection, but if we are
+ * receiving the connection this will be set only after the connection
+ * is established.
+ */
+ public void setRemotePeer(Hash peer) { _remotePeer = peer; }
+ /**
+ * The AES key used to verify packets, set only after the connection is
+ * established.
+ */
+ public void setCurrentMACKey(SessionKey key) { _currentMACKey = key; }
+ /**
+ * The AES key used to encrypt/decrypt packets, set only after the
+ * connection is established.
+ */
+ public void setCurrentCipherKey(SessionKey key) { _currentCipherKey = key; }
+ /**
+ * The pending AES key for verifying packets if we are rekeying the
+ * connection, or null if we are not in the process of rekeying.
+ */
+ public void setNextMACKey(SessionKey key) { _nextMACKey = key; }
+ /**
+ * The pending AES key for encrypting/decrypting packets if we are
+ * rekeying the connection, or null if we are not in the process
+ * of rekeying.
+ */
+ public void setNextCipherKey(SessionKey key) { _nextCipherKey = key; }
+ /**
+ * The keying material used for the rekeying, or null if we are not in
+ * the process of rekeying.
+ */
+ public void setNextKeyingMaterial(byte data[]) { _nextKeyingMaterial = data; }
+ /** true if we began the current rekeying, false otherwise */
+ public void setRekeyBeganLocally(boolean local) { _rekeyBeganLocally = local; }
+ /** when were the current cipher and MAC keys established/rekeyed? */
+ public void setKeyEstablishedTime(long when) { _keyEstablishedTime = when; }
+ /** how far off is the remote peer from our clock, in seconds? */
+ public void setClockSkew(short skew) { _clockSkew = skew; }
+ /** what is the current receive second, for congestion control? */
+ public void setCurrentReceiveSecond(long sec) { _currentReceiveSecond = sec; }
+ /** when did we last send them a packet? */
+ public void setLastSendTime(long when) { _lastSendTime = when; }
+ /** when did we last receive a packet from them? */
+ public void setLastReceiveTime(long when) { _lastReceiveTime = when; }
+ public void incrementConsecutiveSendingSecondsWithoutACKS() { _consecutiveSendingSecondsWithoutACKs++; }
+ public void resetConsecutiveSendingSecondsWithoutACKS() { _consecutiveSendingSecondsWithoutACKs = 0; }
+
+ /*
+ public void migrateACKs(List NACKs, long newSecond) {
+ _previousSecondACKs = _currentSecondACKs;
+ if (_currentSecondECNReceived)
+ _sendWindowBytes /= 2;
+ if (_sendWindowBytes < MINIMUM_WINDOW_BYTES)
+ _sendWindowBytes = MINIMUM_WINDOW_BYTES;
+ _sendWindowBytesRemaining = _sendWindowBytes;
+ _currentSecondECNReceived = false;
+ _remoteWantsPreviousACKs = true;
+ _currentReceiveSecond = newSecond;
+ }
+ */
+
+ /**
+ * have all of the packets received in the current second requested that
+ * the previous second's ACKs be sent?
+ */
+ public void remoteDoesNotWantPreviousACKs() { _remoteWantsPreviousACKs = false; }
+ /**
+ * Decrement the remaining bytes in the current period's window,
+ * returning true if the full size can be decremented, false if it
+ * cannot. If it is not decremented, the window size remaining is
+ * not adjusted at all.
+ */
+ public boolean allocateSendingBytes(int size) {
+ long now = _context.clock().now();
+ if (_lastSendTime > 0) {
+ if (_lastSendTime + 1000 <= now)
+ _sendWindowBytesRemaining = _sendWindowBytes;
+ }
+ if (size <= _sendWindowBytesRemaining) {
+ _sendWindowBytesRemaining -= size;
+ _lastSendTime = now;
+ return true;
+ } else {
+ return false;
+ }
+ }
+ /** what IP+port is the peer sending and receiving packets on? */
+ public void setRemoteAddress(byte ip[], int port) {
+ _remoteIP = ip;
+ _remotePort = port;
+ _remoteHostString = calculateRemoteHostString(ip, port);
+ }
+ /** if we need to contact them, do we need to talk to an introducer? */
+ public void setRemoteRequiresIntroduction(boolean required) { _remoteRequiresIntroduction = required; }
+ /**
+ * if we are serving as an introducer to them, this is the the tag that
+ * they can publish that, when presented to us, will cause us to send
+ * a relay introduction to the current peer
+ */
+ public void setWeRelayToThemAs(long tag) { _weRelayToThemAs = tag; }
+ /**
+ * If they have offered to serve as an introducer to us, this is the tag
+ * we can use to publish that fact.
+ */
+ public void setTheyRelayToUsAs(long tag) { _theyRelayToUsAs = tag; }
+ /** what is the largest packet we can send to the peer? */
+ public void setMTU(int mtu) {
+ _mtu = mtu;
+ _mtuLastChecked = _context.clock().now();
+ }
+
+ /** we received the message specified completely */
+ public void messageFullyReceived(Long messageId) {
+ synchronized (_currentACKs) {
+ if (!_currentACKs.contains(messageId))
+ _currentACKs.add(messageId);
+ }
+ _messagesReceived++;
+ }
+
+ /**
+ * either they told us to back off, or we had to resend to get
+ * the data through.
+ *
+ */
+ public void congestionOccurred() {
+ _sendWindowBytes /= 2;
+ if (_sendWindowBytes < MINIMUM_WINDOW_BYTES)
+ _sendWindowBytes = MINIMUM_WINDOW_BYTES;
+ }
+
+ /** pull off the ACKs (Long) to send to the peer */
+ public List retrieveACKs() {
+ List rv = null;
+ synchronized (_currentACKs) {
+ rv = new ArrayList(_currentACKs);
+ _currentACKs.clear();
+ }
+ return rv;
+ }
+
+ /** we sent a message which was ACKed containing the given # of bytes */
+ public void messageACKed(int bytesACKed) {
+ _consecutiveSendingSecondsWithoutACKs = 0;
+ _sendWindowBytes += bytesACKed;
+ _lastReceiveTime = _context.clock().now();
+ _messagesSent++;
+ }
+
+ public long getMessagesSent() { return _messagesSent; }
+ public long getMessagesReceived() { return _messagesReceived; }
+
+ /**
+ * we received a backoff request, so cut our send window
+ */
+ public void ECNReceived() {
+ congestionOccurred();
+ _currentSecondECNReceived = true;
+ _lastReceiveTime = _context.clock().now();
+ }
+
+ public void dataReceived() {
+ _lastReceiveTime = _context.clock().now();
+ }
+
+ /** when did we last send an ACK to the peer? */
+ public long getLastACKSend() { return _lastACKSend; }
+ public void setLastACKSend(long when) { _lastACKSend = when; }
+
+ public String getRemoteHostString() { return _remoteHostString; }
+
+ public static String calculateRemoteHostString(byte ip[], int port) {
+ StringBuffer buf = new StringBuffer(ip.length * 4 + 5);
+ for (int i = 0; i < ip.length; i++)
+ buf.append((int)ip[i]).append('.');
+ buf.append(port);
+ return buf.toString();
+ }
+
+ public static String calculateRemoteHostString(UDPPacket packet) {
+ InetAddress remAddr = packet.getPacket().getAddress();
+ int remPort = packet.getPacket().getPort();
+ return calculateRemoteHostString(remAddr.getAddress(), remPort);
+ }
+
+ public int hashCode() {
+ if (_remotePeer != null)
+ return _remotePeer.hashCode();
+ else
+ return super.hashCode();
+ }
+ public boolean equals(Object o) {
+ if (o == null) return false;
+ if (o instanceof PeerState) {
+ PeerState s = (PeerState)o;
+ if (_remotePeer == null)
+ return o == this;
+ else
+ return _remotePeer.equals(s.getRemotePeer());
+ } else {
+ return false;
+ }
+ }
+
+ public String toString() {
+ StringBuffer buf = new StringBuffer(64);
+ buf.append(_remoteHostString);
+ if (_remotePeer != null)
+ buf.append(" ").append(_remotePeer.toBase64().substring(0,6));
+ return buf.toString();
+ }
+}
diff --git a/router/java/src/net/i2p/router/transport/udp/RelayPeer.java b/router/java/src/net/i2p/router/transport/udp/RelayPeer.java
new file mode 100644
index 000000000..6b42d0e2e
--- /dev/null
+++ b/router/java/src/net/i2p/router/transport/udp/RelayPeer.java
@@ -0,0 +1,24 @@
+package net.i2p.router.transport.udp;
+
+import net.i2p.data.SessionKey;
+
+/**
+ * Describe the offering to act as an introducer
+ *
+ */
+class RelayPeer {
+ private String _host;
+ private int _port;
+ private byte _tag[];
+ private SessionKey _relayIntroKey;
+ public RelayPeer(String host, int port, byte tag[], SessionKey introKey) {
+ _host = host;
+ _port = port;
+ _tag = tag;
+ _relayIntroKey = introKey;
+ }
+ public String getHost() { return _host; }
+ public int getPort() { return _port; }
+ public byte[] getTag() { return _tag; }
+ public SessionKey getIntroKey() { return _relayIntroKey; }
+}
diff --git a/router/java/src/net/i2p/router/transport/udp/TimedWeightedPriorityMessageQueue.java b/router/java/src/net/i2p/router/transport/udp/TimedWeightedPriorityMessageQueue.java
new file mode 100644
index 000000000..335dd548c
--- /dev/null
+++ b/router/java/src/net/i2p/router/transport/udp/TimedWeightedPriorityMessageQueue.java
@@ -0,0 +1,226 @@
+package net.i2p.router.transport.udp;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import net.i2p.router.OutNetMessage;
+import net.i2p.router.RouterContext;
+import net.i2p.util.I2PThread;
+import net.i2p.util.Log;
+
+/**
+ * Weighted priority queue implementation for the outbound messages, coupled
+ * with code to fail messages that expire.
+ *
+ */
+public class TimedWeightedPriorityMessageQueue implements MessageQueue {
+ private RouterContext _context;
+ private Log _log;
+ /** FIFO queue of messages in a particular priority */
+ private List _queue[];
+ /** all messages in the indexed queue are at or below the given priority. */
+ private int _priorityLimits[];
+ /** weighting for each queue */
+ private int _weighting[];
+ /** how many bytes are enqueued */
+ private long _bytesQueued[];
+ /** how many messages have been pushed out in this pass */
+ private int _messagesFlushed[];
+ /** how many bytes total have been pulled off the given queue */
+ private long _bytesTransferred[];
+ /** lock to notify message enqueue/removal (and block for getNext()) */
+ private Object _nextLock;
+ /** have we shut down or are we still alive? */
+ private boolean _alive;
+ /** which queue should we pull out of next */
+ private int _nextQueue;
+ /** true if a message is enqueued while the getNext() call is in progress */
+ private volatile boolean _addedSincePassBegan;
+ private Expirer _expirer;
+ private FailedListener _listener;
+
+ /**
+ * Build up a new queue
+ *
+ * @param priorityLimits ordered breakpoint for the different message
+ * priorities, with the lowest limit first.
+ * @param weighting how much to prefer a given priority grouping.
+ * specifically, this means how many messages in this queue
+ * should be pulled off in a row before moving on to the next.
+ */
+ public TimedWeightedPriorityMessageQueue(RouterContext ctx, int[] priorityLimits, int[] weighting, FailedListener lsnr) {
+ _context = ctx;
+ _log = ctx.logManager().getLog(TimedWeightedPriorityMessageQueue.class);
+ _queue = new List[weighting.length];
+ _priorityLimits = new int[weighting.length];
+ _weighting = new int[weighting.length];
+ _bytesQueued = new long[weighting.length];
+ _bytesTransferred = new long[weighting.length];
+ _messagesFlushed = new int[weighting.length];
+ for (int i = 0; i < weighting.length; i++) {
+ _queue[i] = new ArrayList(8);
+ _weighting[i] = weighting[i];
+ _priorityLimits[i] = priorityLimits[i];
+ _messagesFlushed[i] = 0;
+ _bytesQueued[i] = 0;
+ _bytesTransferred[i] = 0;
+ }
+ _alive = true;
+ _nextLock = this;
+ _nextQueue = 0;
+ _listener = lsnr;
+ _context.statManager().createRateStat("udp.timeToEntrance", "Message lifetime until it reaches the UDP system", "udp", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
+ _context.statManager().createRateStat("udp.messageQueueSize", "How many messages are on the current class queue at removal", "udp", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
+ _expirer = new Expirer();
+ I2PThread t = new I2PThread(_expirer, "UDP outbound expirer");
+ t.setDaemon(true);
+ t.start();
+ }
+
+ public void add(OutNetMessage message) {
+ if (message == null) return;
+
+ _context.statManager().addRateData("udp.timeToEntrance", message.getLifetime(), message.getLifetime());
+
+ int queue = pickQueue(message);
+ long size = message.getMessageSize();
+ synchronized (_queue[queue]) {
+ _queue[queue].add(message);
+ _bytesQueued[queue] += size;
+ }
+
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("Added a " + size + " byte message to queue " + queue);
+
+ synchronized (_nextLock) {
+ _addedSincePassBegan = true;
+ _nextLock.notifyAll();
+ }
+ }
+
+ /**
+ * Grab the next message out of the next queue. This only advances
+ * the _nextQueue var after pushing _weighting[currentQueue] messages
+ * or the queue is empty. This call blocks until either a message
+ * becomes available or the queue is shut down.
+ *
+ * @param blockUntil expiration, or -1 if indefinite
+ * @return message dequeued, or null if the queue was shut down
+ */
+ public OutNetMessage getNext(long blockUntil) {
+ while (_alive) {
+ _addedSincePassBegan = false;
+ for (int i = 0; i < _queue.length; i++) {
+ int currentQueue = (_nextQueue + i) % _queue.length;
+ synchronized (_queue[currentQueue]) {
+ if (_queue[currentQueue].size() > 0) {
+ OutNetMessage msg = (OutNetMessage)_queue[currentQueue].remove(0);
+ long size = msg.getMessageSize();
+ _bytesQueued[currentQueue] -= size;
+ _bytesTransferred[currentQueue] += size;
+ _messagesFlushed[currentQueue]++;
+ if (_messagesFlushed[currentQueue] >= _weighting[currentQueue]) {
+ _messagesFlushed[currentQueue] = 0;
+ _nextQueue = (currentQueue + 1) % _queue.length;
+ }
+ _context.statManager().addRateData("udp.messageQueueSize", _queue[currentQueue].size(), currentQueue);
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("Pulling a message off queue " + currentQueue + " with "
+ + _queue[currentQueue].size() + " remaining");
+ return msg;
+ } else {
+ // nothing waiting
+ _messagesFlushed[currentQueue] = 0;
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("Nothing on queue " + currentQueue);
+ }
+ }
+ }
+
+ long remaining = blockUntil - _context.clock().now();
+ if ( (blockUntil > 0) && (remaining < 0) ) {
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("Nonblocking, or block time has expired");
+ return null;
+ }
+
+ try {
+ synchronized (_nextLock) {
+ if (!_addedSincePassBegan && _alive) {
+ // nothing added since we begun iterating through,
+ // so we can safely wait for the full period. otoh,
+ // even if this is true, we might be able to safely
+ // wait, but it doesn't hurt to loop again.
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("Wait for activity (up to " + remaining + "ms)");
+ if (blockUntil < 0)
+ _nextLock.wait();
+ else
+ _nextLock.wait(remaining);
+ }
+ }
+ } catch (InterruptedException ie) {}
+ }
+
+ return null;
+ }
+
+ public void shutdown() {
+ _alive = false;
+ synchronized (_nextLock) {
+ _nextLock.notifyAll();
+ }
+ }
+
+ private int pickQueue(OutNetMessage message) {
+ int target = message.getPriority();
+ for (int i = 0; i < _priorityLimits.length; i++) {
+ if (_priorityLimits[i] <= target) {
+ if (i == 0)
+ return 0;
+ else
+ return i - 1;
+ }
+ }
+ return _priorityLimits.length-1;
+ }
+
+ public interface FailedListener {
+ public void failed(OutNetMessage msg);
+ }
+
+ /**
+ * Drop expired messages off the queues
+ */
+ private class Expirer implements Runnable {
+ public void run() {
+ List removed = new ArrayList(1);
+ while (_alive) {
+ long now = _context.clock().now();
+ for (int i = 0; i < _queue.length; i++) {
+ synchronized (_queue[i]) {
+ for (int j = 0; j < _queue[i].size(); j++) {
+ OutNetMessage m = (OutNetMessage)_queue[i].get(j);
+ if (m.getExpiration() < now) {
+ _bytesQueued[i] -= m.getMessageSize();
+ removed.add(m);
+ _queue[i].remove(j);
+ j--;
+ continue;
+ }
+ }
+ }
+ }
+
+ for (int i = 0; i < removed.size(); i++) {
+ OutNetMessage m = (OutNetMessage)removed.get(i);
+ _listener.failed(m);
+ }
+ removed.clear();
+
+ try { Thread.sleep(1000); } catch (InterruptedException ie) {}
+ }
+ }
+ }
+}
diff --git a/router/java/src/net/i2p/router/transport/udp/UDPAddress.java b/router/java/src/net/i2p/router/transport/udp/UDPAddress.java
new file mode 100644
index 000000000..c8ebe9a4f
--- /dev/null
+++ b/router/java/src/net/i2p/router/transport/udp/UDPAddress.java
@@ -0,0 +1,56 @@
+package net.i2p.router.transport.udp;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Properties;
+
+import net.i2p.data.Base64;
+import net.i2p.data.RouterAddress;
+
+/**
+ * basic helper to parse out peer info from a udp address
+ */
+public class UDPAddress {
+ private String _host;
+ private InetAddress _hostAddress;
+ private int _port;
+ private byte[] _introKey;
+
+ public static final String PROP_PORT = "port";
+ public static final String PROP_HOST = "host";
+ public static final String PROP_INTRO_KEY = "key";
+
+ public UDPAddress(RouterAddress addr) {
+ parse(addr);
+ }
+
+ private void parse(RouterAddress addr) {
+ Properties opts = addr.getOptions();
+ _host = opts.getProperty(PROP_HOST);
+ if (_host != null) _host = _host.trim();
+ try {
+ String port = opts.getProperty(PROP_PORT);
+ if (port != null)
+ _port = Integer.parseInt(port);
+ } catch (NumberFormatException nfe) {
+ _port = -1;
+ }
+ String key = opts.getProperty(PROP_INTRO_KEY);
+ if (key != null)
+ _introKey = Base64.decode(key.trim());
+ }
+
+ public String getHost() { return _host; }
+ public InetAddress getHostAddress() {
+ if (_hostAddress == null) {
+ try {
+ _hostAddress = InetAddress.getByName(_host);
+ } catch (UnknownHostException uhe) {
+ _hostAddress = null;
+ }
+ }
+ return _hostAddress;
+ }
+ public int getPort() { return _port; }
+ public byte[] getIntroKey() { return _introKey; }
+}
diff --git a/router/java/src/net/i2p/router/transport/udp/UDPEndpoint.java b/router/java/src/net/i2p/router/transport/udp/UDPEndpoint.java
new file mode 100644
index 000000000..b40cf0c5d
--- /dev/null
+++ b/router/java/src/net/i2p/router/transport/udp/UDPEndpoint.java
@@ -0,0 +1,80 @@
+package net.i2p.router.transport.udp;
+
+import java.net.DatagramSocket;
+import java.net.InetAddress;
+import java.net.SocketException;
+
+import net.i2p.router.RouterContext;
+import net.i2p.util.Log;
+
+/**
+ * Coordinate the low level datagram socket, managing the UDPSender and
+ * UDPReceiver
+ */
+public class UDPEndpoint {
+ private RouterContext _context;
+ private Log _log;
+ private int _listenPort;
+ private UDPSender _sender;
+ private UDPReceiver _receiver;
+
+ public UDPEndpoint(RouterContext ctx, int listenPort) throws SocketException {
+ _context = ctx;
+ _log = ctx.logManager().getLog(UDPEndpoint.class);
+
+ _listenPort = listenPort;
+ }
+
+ public void startup() {
+ shutdown();
+ try {
+ DatagramSocket socket = new DatagramSocket(_listenPort);
+ _sender = new UDPSender(_context, socket, "UDPSend on " + _listenPort);
+ _receiver = new UDPReceiver(_context, socket, "UDPReceive on " + _listenPort);
+ _sender.startup();
+ _receiver.startup();
+ } catch (SocketException se) {
+ if (_log.shouldLog(Log.ERROR))
+ _log.error("Unable to bind on " + _listenPort);
+ }
+ }
+
+ public void shutdown() {
+ if (_sender != null) {
+ _sender.shutdown();
+ _receiver.shutdown();
+ _sender = null;
+ _receiver = null;
+ }
+ }
+
+ public void updateListenPort(int newPort) {
+ if (newPort == _listenPort) return;
+ try {
+ DatagramSocket socket = new DatagramSocket(newPort);
+ _sender.updateListeningPort(socket, newPort);
+ // note: this closes the old socket, so call this after the sender!
+ _receiver.updateListeningPort(socket, newPort);
+ _listenPort = newPort;
+ } catch (SocketException se) {
+ if (_log.shouldLog(Log.ERROR))
+ _log.error("Unable to bind on " + _listenPort);
+ }
+ }
+
+ public int getListenPort() { return _listenPort; }
+ public UDPSender getSender() { return _sender; }
+
+ /**
+ * Add the packet to the outobund queue to be sent ASAP (as allowed by
+ * the bandwidth limiter)
+ *
+ * @return number of packets in the send queue
+ */
+ public int send(UDPPacket packet) { return _sender.add(packet); }
+
+ /**
+ * Blocking call to receive the next inbound UDP packet from any peer.
+ */
+ public UDPPacket receive() { return _receiver.receiveNext(); }
+}
diff --git a/router/java/src/net/i2p/router/transport/udp/UDPEndpointTest.java b/router/java/src/net/i2p/router/transport/udp/UDPEndpointTest.java
new file mode 100644
index 000000000..52d533adb
--- /dev/null
+++ b/router/java/src/net/i2p/router/transport/udp/UDPEndpointTest.java
@@ -0,0 +1,113 @@
+package net.i2p.router.transport.udp;
+
+import java.net.InetAddress;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+
+import net.i2p.router.RouterContext;
+import net.i2p.util.I2PThread;
+import net.i2p.util.Log;
+
+/**
+ *
+ */
+public class UDPEndpointTest {
+ private RouterContext _context;
+ private Log _log;
+ private UDPEndpoint _endpoints[];
+ private boolean _beginTest;
+
+ public UDPEndpointTest(RouterContext ctx) {
+ _context = ctx;
+ _log = ctx.logManager().getLog(UDPEndpointTest.class);
+ }
+
+ public void runTest(int numPeers) {
+ RouterContext ctx = new RouterContext(null);
+ try {
+ _endpoints = new UDPEndpoint[numPeers];
+ int base = 2000 + ctx.random().nextInt(10000);
+ for (int i = 0; i < numPeers; i++) {
+ _log.debug("Building " + i);
+ UDPEndpoint endpoint = new UDPEndpoint(ctx, base + i);
+ _endpoints[i] = endpoint;
+ endpoint.startup();
+ I2PThread read = new I2PThread(new TestRead(endpoint), "Test read " + i);
+ I2PThread write = new I2PThread(new TestWrite(endpoint), "Test write " + i);
+ //read.setDaemon(true);
+ read.start();
+ //write.setDaemon(true);
+ write.start();
+ }
+ } catch (SocketException se) {
+ if (_log.shouldLog(Log.ERROR))
+ _log.error("Error initializing", se);
+ return;
+ }
+ _beginTest = true;
+ _log.debug("Test begin");
+ }
+
+ private class TestRead implements Runnable {
+ private UDPEndpoint _endpoint;
+ public TestRead(UDPEndpoint peer) {
+ _endpoint = peer;
+ }
+ public void run() {
+ while (!_beginTest) {
+ try { Thread.sleep(1000); } catch (InterruptedException ie) {}
+ }
+ _log.debug("Beginning to read");
+ long start = System.currentTimeMillis();
+ int received = 0;
+ while (true) {
+ UDPPacket packet = _endpoint.receive();
+ received++;
+ if (received == 10000) {
+ long time = System.currentTimeMillis() - start;
+ _log.debug("Received 10000 in " + time);
+ }
+ }
+ }
+ }
+
+ private class TestWrite implements Runnable {
+ private UDPEndpoint _endpoint;
+ public TestWrite(UDPEndpoint peer) {
+ _endpoint = peer;
+ }
+ public void run() {
+ while (!_beginTest) {
+ try { Thread.sleep(1000); } catch (InterruptedException ie) {}
+ }
+ _log.debug("Beginning to write");
+ for (int curPacket = 0; curPacket < 10000; curPacket++) {
+ byte data[] = new byte[1024];
+ _context.random().nextBytes(data);
+ int curPeer = (curPacket % _endpoints.length);
+ if (_endpoints[curPeer] == _endpoint)
+ curPeer++;
+ if (curPeer >= _endpoints.length)
+ curPeer = 0;
+ short priority = 1;
+ long expiration = -1;
+ try {
+ UDPPacket packet = UDPPacket.acquire(_context);
+ packet.initialize(priority, expiration, InetAddress.getLocalHost(), _endpoints[curPeer].getListenPort());
+ packet.writeData(data, 0, 1024);
+ _endpoint.send(packet);
+ } catch (UnknownHostException uhe) {
+ _log.error("foo!", uhe);
+ }
+ //if (_log.shouldLog(Log.DEBUG)) {
+ // _log.debug("Sent to " + _endpoints[curPeer].getListenPort() + " from " + _endpoint.getListenPort());
+ //}
+ }
+ }
+ }
+
+ public static void main(String args[]) {
+ UDPEndpointTest test = new UDPEndpointTest(new RouterContext(null));
+ test.runTest(2);
+ }
+}
diff --git a/router/java/src/net/i2p/router/transport/udp/UDPPacket.java b/router/java/src/net/i2p/router/transport/udp/UDPPacket.java
new file mode 100644
index 000000000..4fd392b34
--- /dev/null
+++ b/router/java/src/net/i2p/router/transport/udp/UDPPacket.java
@@ -0,0 +1,188 @@
+package net.i2p.router.transport.udp;
+
+import java.net.DatagramPacket;
+import java.net.InetAddress;
+
+import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.List;
+
+import net.i2p.I2PAppContext;
+import net.i2p.data.Base64;
+import net.i2p.data.ByteArray;
+import net.i2p.data.DataHelper;
+import net.i2p.data.Hash;
+import net.i2p.data.SessionKey;
+import net.i2p.util.ByteCache;
+import net.i2p.util.Log;
+
+/**
+ * Basic delivery unit containing the datagram. This also maintains a cache
+ * of object instances to allow rapid reuse.
+ *
+ */
+public class UDPPacket {
+ private I2PAppContext _context;
+ private Log _log;
+ private DatagramPacket _packet;
+ private short _priority;
+ private long _initializeTime;
+ private long _expiration;
+ private byte[] _data;
+
+ private static final List _packetCache;
+ static {
+ _packetCache = new ArrayList(256);
+ }
+
+ private static final boolean CACHE = false;
+
+ private static final int MAX_PACKET_SIZE = 2048;
+ public static final int IV_SIZE = 16;
+ public static final int MAC_SIZE = 16;
+
+ public static final int PAYLOAD_TYPE_SESSION_REQUEST = 0;
+ public static final int PAYLOAD_TYPE_SESSION_CREATED = 1;
+ public static final int PAYLOAD_TYPE_SESSION_CONFIRMED = 2;
+ public static final int PAYLOAD_TYPE_RELAY_REQUEST = 3;
+ public static final int PAYLOAD_TYPE_RELAY_RESPONSE = 4;
+ public static final int PAYLOAD_TYPE_RELAY_INTRO = 5;
+ public static final int PAYLOAD_TYPE_DATA = 6;
+
+ // various flag fields for use in the data packets
+ public static final byte DATA_FLAG_EXPLICIT_ACK = (byte)(1 << 7);
+ public static final byte DATA_FLAG_EXPLICIT_NACK = (1 << 6);
+ public static final byte DATA_FLAG_NUMACKS = (1 << 5);
+ public static final byte DATA_FLAG_ECN = (1 << 4);
+ public static final byte DATA_FLAG_WANT_ACKS = (1 << 3);
+ public static final byte DATA_FLAG_WANT_REPLY = (1 << 2);
+ public static final byte DATA_FLAG_EXTENDED = (1 << 1);
+
+ private static final int MAX_VALIDATE_SIZE = MAX_PACKET_SIZE;
+ private static final ByteCache _validateCache = ByteCache.getInstance(16, MAX_VALIDATE_SIZE);
+ private static final ByteCache _ivCache = ByteCache.getInstance(16, IV_SIZE);
+
+ private UDPPacket(I2PAppContext ctx) {
+ _context = ctx;
+ _log = ctx.logManager().getLog(UDPPacket.class);
+ _data = new byte[MAX_PACKET_SIZE];
+ _packet = new DatagramPacket(_data, MAX_PACKET_SIZE);
+ _initializeTime = _context.clock().now();
+ }
+
+ public void initialize(short priority, long expiration, InetAddress host, int port) {
+ _priority = priority;
+ _expiration = expiration;
+ resetBegin();
+ Arrays.fill(_data, (byte)0x00);
+ _packet.setLength(0);
+ _packet.setAddress(host);
+ _packet.setPort(port);
+ }
+
+ public void writeData(byte src[], int offset, int len) {
+ System.arraycopy(src, offset, _data, 0, len);
+ _packet.setLength(len);
+ resetBegin();
+ }
+ public DatagramPacket getPacket() { return _packet; }
+ public short getPriority() { return _priority; }
+ public long getExpiration() { return _expiration; }
+ public long getLifetime() { return _context.clock().now() - _initializeTime; }
+ public void resetBegin() { _initializeTime = _context.clock().now(); }
+
+ /**
+ * Validate the packet against the MAC specified, returning true if the
+ * MAC matches, false otherwise.
+ *
+ */
+ public boolean validate(SessionKey macKey) {
+ boolean eq = false;
+ ByteArray buf = _validateCache.acquire();
+
+ // validate by comparing _data[0:15] and
+ // HMAC(payload + IV + payloadLength, macKey)
+
+ int payloadLength = _packet.getLength() - MAC_SIZE - IV_SIZE;
+ if (payloadLength > 0) {
+ int off = 0;
+ System.arraycopy(_data, _packet.getOffset() + MAC_SIZE + IV_SIZE, buf.getData(), off, payloadLength);
+ off += payloadLength;
+ System.arraycopy(_data, _packet.getOffset() + MAC_SIZE, buf.getData(), off, IV_SIZE);
+ off += IV_SIZE;
+ DataHelper.toLong(buf.getData(), off, 2, payloadLength);
+ off += 2;
+
+ Hash calculated = _context.hmac().calculate(macKey, buf.getData(), 0, off);
+
+ if (_log.shouldLog(Log.DEBUG)) {
+ StringBuffer str = new StringBuffer(128);
+ str.append(_packet.getLength()).append(" byte packet received, payload length ");
+ str.append(payloadLength);
+ str.append("\nIV: ").append(Base64.encode(buf.getData(), payloadLength, IV_SIZE));
+ str.append("\nIV2: ").append(Base64.encode(_data, MAC_SIZE, IV_SIZE));
+ str.append("\nlen: ").append(DataHelper.fromLong(buf.getData(), payloadLength + IV_SIZE, 2));
+ str.append("\nMAC key: ").append(macKey.toBase64());
+ str.append("\ncalc HMAC: ").append(calculated.toBase64());
+ str.append("\nread HMAC: ").append(Base64.encode(_data, _packet.getOffset(), MAC_SIZE));
+ str.append("\nraw: ").append(Base64.encode(_data, _packet.getOffset(), _packet.getLength()));
+ _log.debug(str.toString());
+ }
+ eq = DataHelper.eq(calculated.getData(), 0, _data, _packet.getOffset(), MAC_SIZE);
+ } else {
+ if (_log.shouldLog(Log.WARN))
+ _log.warn("Payload length is " + payloadLength);
+ }
+
+ _validateCache.release(buf);
+ return eq;
+ }
+
+ /**
+ * Decrypt this valid packet, overwriting the _data buffer's payload
+ * with the decrypted data (leaving the MAC and IV unaltered)
+ *
+ */
+ public void decrypt(SessionKey cipherKey) {
+ ByteArray iv = _ivCache.acquire();
+ System.arraycopy(_data, MAC_SIZE, iv.getData(), 0, IV_SIZE);
+ _context.aes().decrypt(_data, _packet.getOffset() + MAC_SIZE + IV_SIZE, _data, _packet.getOffset() + MAC_SIZE + IV_SIZE, cipherKey, iv.getData(), _packet.getLength() - MAC_SIZE - IV_SIZE);
+ _ivCache.release(iv);
+ }
+
+ public String toString() {
+ StringBuffer buf = new StringBuffer(64);
+ buf.append(_packet.getLength());
+ buf.append(" byte packet with ");
+ buf.append(_packet.getAddress().getHostAddress()).append(":");
+ buf.append(_packet.getPort());
+ return buf.toString();
+ }
+
+
+ public static UDPPacket acquire(I2PAppContext ctx) {
+ if (CACHE) {
+ synchronized (_packetCache) {
+ if (_packetCache.size() > 0) {
+ UDPPacket rv = (UDPPacket)_packetCache.remove(0);
+ rv._context = ctx;
+ rv._log = ctx.logManager().getLog(UDPPacket.class);
+ rv.resetBegin();
+ Arrays.fill(rv._data, (byte)0x00);
+ return rv;
+ }
+ }
+ }
+ return new UDPPacket(ctx);
+ }
+
+ public void release() {
+ if (!CACHE) return;
+ synchronized (_packetCache) {
+ _packet.setLength(0);
+ _packet.setPort(1);
+ if (_packetCache.size() <= 64)
+ _packetCache.add(this);
+ }
+ }
+}
diff --git a/router/java/src/net/i2p/router/transport/udp/UDPPacketReader.java b/router/java/src/net/i2p/router/transport/udp/UDPPacketReader.java
new file mode 100644
index 000000000..b9cf54299
--- /dev/null
+++ b/router/java/src/net/i2p/router/transport/udp/UDPPacketReader.java
@@ -0,0 +1,448 @@
+package net.i2p.router.transport.udp;
+
+import net.i2p.I2PAppContext;
+import net.i2p.data.Base64;
+import net.i2p.data.DataHelper;
+import net.i2p.data.SessionKey;
+import net.i2p.data.Signature;
+import net.i2p.util.Log;
+
+/**
+ * To read a packet, initialize this reader with the data and fetch out
+ * the appropriate fields. If the interesting bits are in message specific
+ * elements, grab the appropriate subreader.
+ *
+ */
+public class UDPPacketReader {
+ private I2PAppContext _context;
+ private Log _log;
+ private byte _message[];
+ private int _payloadBeginOffset;
+ private int _payloadLength;
+ private SessionRequestReader _sessionRequestReader;
+ private SessionCreatedReader _sessionCreatedReader;
+ private SessionConfirmedReader _sessionConfirmedReader;
+ private DataReader _dataReader;
+
+ private static final int KEYING_MATERIAL_LENGTH = 64;
+
+ public UDPPacketReader(I2PAppContext ctx) {
+ _context = ctx;
+ _log = ctx.logManager().getLog(UDPPacketReader.class);
+ _sessionRequestReader = new SessionRequestReader();
+ _sessionCreatedReader = new SessionCreatedReader();
+ _sessionConfirmedReader = new SessionConfirmedReader();
+ _dataReader = new DataReader();
+ }
+
+ public void initialize(UDPPacket packet) {
+ int off = packet.getPacket().getOffset();
+ int len = packet.getPacket().getLength();
+ off += UDPPacket.MAC_SIZE + UDPPacket.IV_SIZE;
+ len -= UDPPacket.MAC_SIZE + UDPPacket.IV_SIZE;
+ initialize(packet.getPacket().getData(), off, len);
+ }
+
+ public void initialize(byte message[], int payloadOffset, int payloadLength) {
+ _message = message;
+ _payloadBeginOffset = payloadOffset;
+ _payloadLength = payloadLength;
+ }
+
+ /** what type of payload is in here? */
+ public int readPayloadType() {
+ // 3 highest order bits == payload type
+ return _message[_payloadBeginOffset] >>> 4;
+ }
+
+ /** does this packet include rekeying data? */
+ public boolean readRekeying() {
+ return (_message[_payloadBeginOffset] & (1 << 3)) != 0;
+ }
+
+ public boolean readExtendedOptionsIncluded() {
+ return (_message[_payloadBeginOffset] & (1 << 2)) != 0;
+ }
+
+ public long readTimestamp() {
+ return DataHelper.fromLong(_message, _payloadBeginOffset + 1, 4);
+ }
+
+ public void readKeyingMaterial(byte target[], int targetOffset) {
+ if (!readRekeying())
+ throw new IllegalStateException("This packet is not rekeying!");
+ System.arraycopy(_message, _payloadBeginOffset + 1 + 4, target, targetOffset, KEYING_MATERIAL_LENGTH);
+ }
+
+ /** index into the message where the body begins */
+ private int readBodyOffset() {
+ int offset = _payloadBeginOffset + 1 + 4;
+ if (readRekeying())
+ offset += KEYING_MATERIAL_LENGTH;
+ if (readExtendedOptionsIncluded()) {
+ int optionsSize = (int)DataHelper.fromLong(_message, offset, 1);
+ offset += optionsSize + 1;
+ }
+ return offset;
+ }
+
+ public SessionRequestReader getSessionRequestReader() { return _sessionRequestReader; }
+ public SessionCreatedReader getSessionCreatedReader() { return _sessionCreatedReader; }
+ public SessionConfirmedReader getSessionConfirmedReader() { return _sessionConfirmedReader; }
+ public DataReader getDataReader() { return _dataReader; }
+
+ public String toString() {
+ switch (readPayloadType()) {
+ case UDPPacket.PAYLOAD_TYPE_DATA:
+ return _dataReader.toString();
+ case UDPPacket.PAYLOAD_TYPE_SESSION_CONFIRMED:
+ return "Session confirmed packet";
+ case UDPPacket.PAYLOAD_TYPE_SESSION_CREATED:
+ return "Session created packet";
+ case UDPPacket.PAYLOAD_TYPE_SESSION_REQUEST:
+ return "Session request packet";
+ default:
+ return "Other packet type...";
+ }
+ }
+
+ /** Help read the SessionRequest payload */
+ public class SessionRequestReader {
+ public static final int X_LENGTH = 256;
+ public void readX(byte target[], int targetOffset) {
+ int readOffset = readBodyOffset();
+ System.arraycopy(_message, readOffset, target, targetOffset, X_LENGTH);
+ }
+
+ public int readIPSize() {
+ int offset = readBodyOffset() + X_LENGTH;
+ return (int)DataHelper.fromLong(_message, offset, 1);
+ }
+
+ /** what IP bob is reachable on */
+ public void readIP(byte target[], int targetOffset) {
+ int offset = readBodyOffset() + X_LENGTH;
+ int size = (int)DataHelper.fromLong(_message, offset, 1);
+ offset++;
+ System.arraycopy(_message, offset, target, targetOffset, size);
+ }
+ }
+
+ /** Help read the SessionCreated payload */
+ public class SessionCreatedReader {
+ public static final int Y_LENGTH = 256;
+ public void readY(byte target[], int targetOffset) {
+ int readOffset = readBodyOffset();
+ System.arraycopy(_message, readOffset, target, targetOffset, Y_LENGTH);
+ }
+
+ /** sizeof(IP) */
+ public int readIPSize() {
+ int offset = readBodyOffset() + Y_LENGTH;
+ return (int)DataHelper.fromLong(_message, offset, 1);
+ }
+
+ /** what IP do they think we are coming on? */
+ public void readIP(byte target[], int targetOffset) {
+ int offset = readBodyOffset() + Y_LENGTH;
+ int size = (int)DataHelper.fromLong(_message, offset, 1);
+ offset++;
+ System.arraycopy(_message, offset, target, targetOffset, size);
+ }
+
+ /** what port do they think we are coming from? */
+ public int readPort() {
+ int offset = readBodyOffset() + Y_LENGTH + 1 + readIPSize();
+ return (int)DataHelper.fromLong(_message, offset, 2);
+ }
+
+ /** write out the 4 byte relayAs tag */
+ public long readRelayTag() {
+ int offset = readBodyOffset() + Y_LENGTH + 1 + readIPSize() + 2;
+ return DataHelper.fromLong(_message, offset, 4);
+ }
+
+ public long readSignedOnTime() {
+ int offset = readBodyOffset() + Y_LENGTH + 1 + readIPSize() + 2 + 4;
+ long rv = DataHelper.fromLong(_message, offset, 4);
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("Signed on time offset: " + offset + " val: " + rv
+ + "\nRawCreated: " + Base64.encode(_message, _payloadBeginOffset, _payloadLength));
+ return rv;
+ }
+
+ public void readEncryptedSignature(byte target[], int targetOffset) {
+ int offset = readBodyOffset() + Y_LENGTH + 1 + readIPSize() + 2 + 4 + 4;
+ System.arraycopy(_message, offset, target, targetOffset, Signature.SIGNATURE_BYTES + 8);
+ }
+
+ public void readIV(byte target[], int targetOffset) {
+ int offset = _payloadBeginOffset - UDPPacket.IV_SIZE;
+ System.arraycopy(_message, offset, target, targetOffset, UDPPacket.IV_SIZE);
+ }
+ }
+
+ /** parse out the confirmed message */
+ public class SessionConfirmedReader {
+ /** which fragment is this? */
+ public int readCurrentFragmentNum() {
+ int readOffset = readBodyOffset();
+ return _message[readOffset] >>> 4;
+ }
+ /** how many fragments will there be? */
+ public int readTotalFragmentNum() {
+ int readOffset = readBodyOffset();
+ return (_message[readOffset] & 0xF);
+ }
+
+ public int readCurrentFragmentSize() {
+ int readOffset = readBodyOffset() + 1;
+ return (int)DataHelper.fromLong(_message, readOffset, 2);
+ }
+
+ /** read the fragment data from the nonterminal sessionConfirmed packet */
+ public void readFragmentData(byte target[], int targetOffset) {
+ int readOffset = readBodyOffset() + 1 + 2;
+ int len = readCurrentFragmentSize();
+ System.arraycopy(_message, readOffset, target, targetOffset, len);
+ }
+
+ /** read the time at which the signature was generated */
+ public long readFinalFragmentSignedOnTime() {
+ if (readCurrentFragmentNum() != readTotalFragmentNum()-1)
+ throw new IllegalStateException("This is not the final fragment");
+ int readOffset = readBodyOffset() + 1 + 2 + readCurrentFragmentSize();
+ return DataHelper.fromLong(_message, readOffset, 4);
+ }
+
+ /** read the signature from the final sessionConfirmed packet */
+ public void readFinalSignature(byte target[], int targetOffset) {
+ if (readCurrentFragmentNum() != readTotalFragmentNum()-1)
+ throw new IllegalStateException("This is not the final fragment");
+ int readOffset = _payloadBeginOffset + _payloadLength - Signature.SIGNATURE_BYTES;
+ System.arraycopy(_message, readOffset, target, targetOffset, Signature.SIGNATURE_BYTES);
+ }
+ }
+
+ /** parse out the data message */
+ public class DataReader {
+ public boolean readACKsIncluded() {
+ return flagSet(UDPPacket.DATA_FLAG_EXPLICIT_ACK);
+ }
+ public boolean readNACKsIncluded() {
+ return flagSet(UDPPacket.DATA_FLAG_EXPLICIT_NACK);
+ }
+ public boolean readNumACKsIncluded() {
+ return flagSet(UDPPacket.DATA_FLAG_NUMACKS);
+ }
+ public boolean readECN() {
+ return flagSet(UDPPacket.DATA_FLAG_ECN);
+ }
+ public boolean readWantPreviousACKs() {
+ return flagSet(UDPPacket.DATA_FLAG_WANT_ACKS);
+ }
+ public boolean readReplyRequested() {
+ return flagSet(UDPPacket.DATA_FLAG_WANT_REPLY);
+ }
+ public boolean readExtendedDataIncluded() {
+ return flagSet(UDPPacket.DATA_FLAG_EXTENDED);
+ }
+ public long[] readACKs() {
+ if (!readACKsIncluded()) return null;
+ int off = readBodyOffset() + 1;
+ int num = (int)DataHelper.fromLong(_message, off, 1);
+ off++;
+ long rv[] = new long[num];
+ for (int i = 0; i < num; i++) {
+ rv[i] = DataHelper.fromLong(_message, off, 4);
+ off += 4;
+ }
+ return rv;
+ }
+ public long[] readNACKs() {
+ if (!readNACKsIncluded()) return null;
+ int off = readBodyOffset() + 1;
+ if (readACKsIncluded()) {
+ int numACKs = (int)DataHelper.fromLong(_message, off, 1);
+ off++;
+ off += 4 * numACKs;
+ }
+
+ int numNACKs = (int)DataHelper.fromLong(_message, off, 1);
+ off++;
+ long rv[] = new long[numNACKs];
+ for (int i = 0; i < numNACKs; i++) {
+ rv[i] = DataHelper.fromLong(_message, off, 4);
+ off += 4;
+ }
+ return rv;
+ }
+ public int readNumACKs() {
+ if (!readNumACKsIncluded()) return -1;
+ int off = readBodyOffset() + 1;
+
+ if (readACKsIncluded()) {
+ int numACKs = (int)DataHelper.fromLong(_message, off, 1);
+ off++;
+ off += 4 * numACKs;
+ }
+ if (readNACKsIncluded()) {
+ int numNACKs = (int)DataHelper.fromLong(_message, off, 1);
+ off++;
+ off += 4 * numNACKs;
+ }
+ return (int)DataHelper.fromLong(_message, off, 2);
+ }
+
+ public int readFragmentCount() {
+ int off = readBodyOffset() + 1;
+ if (readACKsIncluded()) {
+ int numACKs = (int)DataHelper.fromLong(_message, off, 1);
+ off++;
+ off += 4 * numACKs;
+ }
+ if (readNACKsIncluded()) {
+ int numNACKs = (int)DataHelper.fromLong(_message, off, 1);
+ off++;
+ off += 4 * numNACKs;
+ }
+ if (readNumACKsIncluded())
+ off += 2;
+ if (readExtendedDataIncluded()) {
+ int size = (int)DataHelper.fromLong(_message, off, 1);
+ off++;
+ off += size;
+ }
+ return (int)_message[off];
+ }
+
+ public long readMessageId(int fragmentNum) {
+ int fragmentBegin = getFragmentBegin(fragmentNum);
+ return DataHelper.fromLong(_message, fragmentBegin, 4);
+ }
+ public int readMessageFragmentNum(int fragmentNum) {
+ int off = getFragmentBegin(fragmentNum);
+ off += 4; // messageId
+ return _message[off] >>> 3;
+ }
+ public boolean readMessageIsLast(int fragmentNum) {
+ int off = getFragmentBegin(fragmentNum);
+ off += 4; // messageId
+ return ((_message[off] & (1 << 2)) != 0);
+ }
+ public int readMessageFragmentSize(int fragmentNum) {
+ int off = getFragmentBegin(fragmentNum);
+ off += 4; // messageId
+ off++; // fragment info
+ return (int)DataHelper.fromLong(_message, off, 2);
+ }
+ public void readMessageFragment(int fragmentNum, byte target[], int targetOffset) {
+ int off = getFragmentBegin(fragmentNum);
+ off += 4; // messageId
+ off++; // fragment info
+ int size = (int)DataHelper.fromLong(_message, off, 2);
+ off += 2;
+ System.arraycopy(_message, off, target, targetOffset, size);
+ }
+
+ private int getFragmentBegin(int fragmentNum) {
+ int off = readBodyOffset() + 1;
+ if (readACKsIncluded()) {
+ int numACKs = (int)DataHelper.fromLong(_message, off, 1);
+ off++;
+ off += 4 * numACKs;
+ }
+ if (readNACKsIncluded()) {
+ int numNACKs = (int)DataHelper.fromLong(_message, off, 1);
+ off++;
+ off += 5 * numNACKs;
+ }
+ if (readNumACKsIncluded())
+ off += 2;
+ if (readExtendedDataIncluded()) {
+ int size = (int)DataHelper.fromLong(_message, off, 1);
+ off++;
+ off += size;
+ }
+ off++; // # fragments
+
+ if (fragmentNum == 0) {
+ return off;
+ } else {
+ for (int i = 0; i < fragmentNum; i++) {
+ off += 5; // messageId+info
+ off += (int)DataHelper.fromLong(_message, off, 2);
+ off += 2;
+ }
+ return off;
+ }
+ }
+
+ private boolean flagSet(byte flag) {
+ int flagOffset = readBodyOffset();
+ return ((_message[flagOffset] & flag) != 0);
+ }
+
+ public String toString() {
+ StringBuffer buf = new StringBuffer(256);
+ long msAgo = _context.clock().now() - readTimestamp()*1000;
+ buf.append("Data packet sent ").append(msAgo).append("ms ago ");
+ buf.append("IV ");
+ buf.append(Base64.encode(_message, _payloadBeginOffset-UDPPacket.IV_SIZE, UDPPacket.IV_SIZE));
+ buf.append(" ");
+ int off = readBodyOffset() + 1;
+ if (readACKsIncluded()) {
+ int numACKs = (int)DataHelper.fromLong(_message, off, 1);
+ off++;
+ buf.append("with ACKs for ");
+ for (int i = 0; i < numACKs; i++) {
+ buf.append(DataHelper.fromLong(_message, off, 4)).append(' ');
+ off += 4;
+ }
+ }
+ if (readNACKsIncluded()) {
+ int numNACKs = (int)DataHelper.fromLong(_message, off, 1);
+ off++;
+ buf.append("with NACKs for ");
+ for (int i = 0; i < numNACKs; i++) {
+ buf.append(DataHelper.fromLong(_message, off, 4)).append(' ');
+ off += 5;
+ }
+ off += 5 * numNACKs;
+ }
+ if (readNumACKsIncluded()) {
+ buf.append("with numACKs of ");
+ buf.append(DataHelper.fromLong(_message, off, 2));
+ buf.append(' ');
+ off += 2;
+ }
+ if (readExtendedDataIncluded()) {
+ int size = (int)DataHelper.fromLong(_message, off, 1);
+ off++;
+ buf.append("with extended size of ");
+ buf.append(size);
+ buf.append(' ');
+ off += size;
+ }
+
+ int numFragments = (int)DataHelper.fromLong(_message, off, 1);
+ off++;
+ buf.append("with fragmentCount of ");
+ buf.append(numFragments);
+ buf.append(' ');
+
+ for (int i = 0; i < numFragments; i++) {
+ buf.append("containing messageId ");
+ buf.append(DataHelper.fromLong(_message, off, 4));
+ off += 5; // messageId+info
+ int size = (int)DataHelper.fromLong(_message, off, 2);
+ buf.append(" with ").append(size).append(" bytes");
+ buf.append(' ');
+ off += size;
+ off += 2;
+ }
+
+ return buf.toString();
+ }
+ }
+}
diff --git a/router/java/src/net/i2p/router/transport/udp/UDPReceiver.java b/router/java/src/net/i2p/router/transport/udp/UDPReceiver.java
new file mode 100644
index 000000000..144ae839d
--- /dev/null
+++ b/router/java/src/net/i2p/router/transport/udp/UDPReceiver.java
@@ -0,0 +1,166 @@
+package net.i2p.router.transport.udp;
+
+import java.io.IOException;
+import java.net.DatagramSocket;
+import java.net.DatagramPacket;
+
+import java.util.ArrayList;
+import java.util.List;
+import net.i2p.router.RouterContext;
+import net.i2p.router.transport.FIFOBandwidthLimiter;
+import net.i2p.util.I2PThread;
+import net.i2p.util.Log;
+
+/**
+ * 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}
+ *
+ */
+public class UDPReceiver {
+ private RouterContext _context;
+ private Log _log;
+ private DatagramSocket _socket;
+ private String _name;
+ private List _inboundQueue;
+ private boolean _keepRunning;
+ private Runner _runner;
+
+ public UDPReceiver(RouterContext ctx, DatagramSocket socket, String name) {
+ _context = ctx;
+ _log = ctx.logManager().getLog(UDPReceiver.class);
+ _name = name;
+ _inboundQueue = new ArrayList(128);
+ _socket = socket;
+ _runner = new Runner();
+ _context.statManager().createRateStat("udp.receivePacketSize", "How large packets received are", "udp", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
+ _context.statManager().createRateStat("udp.droppedInbound", "How many packet are queued up but not yet received when we drop", "udp", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
+ }
+
+ public void startup() {
+ _keepRunning = true;
+ I2PThread t = new I2PThread(_runner, _name);
+ t.setDaemon(true);
+ t.start();
+ }
+
+ public void shutdown() {
+ _keepRunning = false;
+ synchronized (_inboundQueue) {
+ _inboundQueue.clear();
+ _inboundQueue.notifyAll();
+ }
+ }
+
+ /**
+ * 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 2 seconds, drop subsequent packets */
+ private static final long MAX_QUEUE_PERIOD = 2*1000;
+
+ private void receive(UDPPacket packet) {
+ synchronized (_inboundQueue) {
+ int queueSize = _inboundQueue.size();
+ if (queueSize > 0) {
+ long headPeriod = ((UDPPacket)_inboundQueue.get(0)).getLifetime();
+ if (headPeriod > MAX_QUEUE_PERIOD) {
+ _context.statManager().addRateData("udp.droppedInbound", queueSize, headPeriod);
+ if (_log.shouldLog(Log.WARN))
+ _log.warn("Dropping inbound packet with " + queueSize + " queued for " + headPeriod);
+ _inboundQueue.notifyAll();
+ return;
+ }
+ }
+ _inboundQueue.add(packet);
+ _inboundQueue.notifyAll();
+ }
+ }
+
+ /**
+ * Blocking call to retrieve the next inbound packet, or null if we have
+ * shut down.
+ *
+ */
+ public UDPPacket receiveNext() {
+ while (_keepRunning) {
+ synchronized (_inboundQueue) {
+ if (_inboundQueue.size() <= 0) {
+ try {
+ _inboundQueue.wait();
+ } catch (InterruptedException ie) {}
+ }
+ if (_inboundQueue.size() > 0)
+ return (UDPPacket)_inboundQueue.remove(0);
+ }
+ }
+ return null;
+ }
+
+ private class Runner implements Runnable {
+ private boolean _socketChanged;
+ public void run() {
+ _socketChanged = false;
+ while (_keepRunning) {
+ if (_socketChanged) {
+ Thread.currentThread().setName(_name);
+ _socketChanged = false;
+ }
+ UDPPacket packet = UDPPacket.acquire(_context);
+
+ // block before we read...
+ while (!_context.throttle().acceptNetworkMessage())
+ try { Thread.sleep(10); } catch (InterruptedException ie) {}
+
+ try {
+ synchronized (Runner.this) {
+ _socket.receive(packet.getPacket());
+ }
+ int size = packet.getPacket().getLength();
+ packet.resetBegin();
+ _context.statManager().addRateData("udp.receivePacketSize", size, 0);
+
+ // and block after we know how much we read but before
+ // we release the packet to the inbound queue
+ if (size > 0) {
+ FIFOBandwidthLimiter.Request req = _context.bandwidthLimiter().requestInbound(size, "UDP receiver");
+ while (req.getPendingInboundRequested() > 0)
+ req.waitForNextAllocation();
+ }
+
+ receive(packet);
+ } catch (IOException ioe) {
+ if (_socketChanged) {
+ if (_log.shouldLog(Log.INFO))
+ _log.info("Changing ports...");
+ } else {
+ if (_log.shouldLog(Log.ERROR))
+ _log.error("Error receiving", ioe);
+ }
+ packet.release();
+ }
+ }
+ }
+
+ 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;
+ }
+ }
+
+}
diff --git a/router/java/src/net/i2p/router/transport/udp/UDPSender.java b/router/java/src/net/i2p/router/transport/udp/UDPSender.java
new file mode 100644
index 000000000..e20cd2c30
--- /dev/null
+++ b/router/java/src/net/i2p/router/transport/udp/UDPSender.java
@@ -0,0 +1,174 @@
+package net.i2p.router.transport.udp;
+
+import java.io.IOException;
+import java.net.DatagramSocket;
+import java.net.DatagramPacket;
+
+import java.util.ArrayList;
+import java.util.List;
+import net.i2p.data.Base64;
+import net.i2p.router.RouterContext;
+import net.i2p.router.transport.FIFOBandwidthLimiter;
+import net.i2p.util.I2PThread;
+import net.i2p.util.Log;
+
+/**
+ * Lowest level packet sender, pushes anything on its queue ASAP.
+ *
+ */
+public class UDPSender {
+ private RouterContext _context;
+ private Log _log;
+ private DatagramSocket _socket;
+ private String _name;
+ private List _outboundQueue;
+ private boolean _keepRunning;
+ private Runner _runner;
+
+ private static final int MAX_QUEUED = 64;
+
+ public UDPSender(RouterContext ctx, DatagramSocket socket, String name) {
+ _context = ctx;
+ _log = ctx.logManager().getLog(UDPSender.class);
+ _outboundQueue = new ArrayList(128);
+ _socket = socket;
+ _runner = new Runner();
+ _name = name;
+ _context.statManager().createRateStat("udp.pushTime", "How long a UDP packet takes to get pushed out", "udp", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
+ _context.statManager().createRateStat("udp.sendPacketSize", "How large packets sent are", "udp", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
+ }
+
+ public void startup() {
+ _keepRunning = true;
+ I2PThread t = new I2PThread(_runner, _name);
+ t.setDaemon(true);
+ t.start();
+ }
+
+ public void shutdown() {
+ _keepRunning = false;
+ synchronized (_outboundQueue) {
+ _outboundQueue.clear();
+ _outboundQueue.notifyAll();
+ }
+ }
+
+ public DatagramSocket updateListeningPort(DatagramSocket socket, int newPort) {
+ return _runner.updateListeningPort(socket, newPort);
+ }
+
+
+ /**
+ * Add the packet to the queue. This may block until there is space
+ * available, if requested, otherwise it returns immediately
+ *
+ * @return number of packets queued
+ */
+ public int add(UDPPacket packet, boolean blocking) {
+ int remaining = -1;
+ while ( (_keepRunning) && (remaining < 0) ) {
+ try {
+ synchronized (_outboundQueue) {
+ if (_outboundQueue.size() < MAX_QUEUED) {
+ _outboundQueue.add(packet);
+ remaining = _outboundQueue.size();
+ _outboundQueue.notifyAll();
+ } else {
+ if (blocking) {
+ _outboundQueue.wait();
+ } else {
+ remaining = _outboundQueue.size();
+ }
+ }
+ }
+ } catch (InterruptedException ie) {}
+ }
+ return remaining;
+ }
+
+ /**
+ *
+ * @return number of packets in the queue
+ */
+ public int add(UDPPacket packet) {
+ int size = 0;
+ synchronized (_outboundQueue) {
+ _outboundQueue.add(packet);
+ size = _outboundQueue.size();
+ _outboundQueue.notifyAll();
+ }
+ return size;
+ }
+
+ private class Runner implements Runnable {
+ private boolean _socketChanged;
+ public void run() {
+ _socketChanged = false;
+ while (_keepRunning) {
+ if (_socketChanged) {
+ Thread.currentThread().setName(_name);
+ _socketChanged = false;
+ }
+
+ UDPPacket packet = getNextPacket();
+ if (packet != null) {
+ int size = packet.getPacket().getLength();
+ if (size > 0) {
+ FIFOBandwidthLimiter.Request req = _context.bandwidthLimiter().requestOutbound(size, "UDP sender");
+ while (req.getPendingOutboundRequested() > 0)
+ req.waitForNextAllocation();
+ }
+
+ if (_log.shouldLog(Log.DEBUG)) {
+ int len = packet.getPacket().getLength();
+ //if (len > 128)
+ // len = 128;
+ _log.debug("Sending packet: \nraw: " + Base64.encode(packet.getPacket().getData(), 0, len));
+ }
+
+ try {
+ synchronized (Runner.this) {
+ // synchronization lets us update safely
+ _socket.send(packet.getPacket());
+ }
+ _context.statManager().addRateData("udp.pushTime", packet.getLifetime(), packet.getLifetime());
+ _context.statManager().addRateData("udp.sendPacketSize", packet.getPacket().getLength(), packet.getLifetime());
+ } catch (IOException ioe) {
+ if (_log.shouldLog(Log.ERROR))
+ _log.error("Error sending", ioe);
+ }
+
+ // back to the cache
+ //packet.release();
+ }
+ }
+ }
+
+ private UDPPacket getNextPacket() {
+ UDPPacket packet = null;
+ while ( (_keepRunning) && (packet == null) ) {
+ try {
+ synchronized (_outboundQueue) {
+ if (_outboundQueue.size() <= 0) {
+ _outboundQueue.wait();
+ } else {
+ packet = (UDPPacket)_outboundQueue.remove(0);
+ _outboundQueue.notifyAll();
+ }
+ }
+ } catch (InterruptedException ie) {}
+ }
+ return packet;
+ }
+ public DatagramSocket updateListeningPort(DatagramSocket socket, int newPort) {
+ _name = "UDPSend on " + newPort;
+ DatagramSocket old = null;
+ synchronized (Runner.this) {
+ old = _socket;
+ _socket = socket;
+ }
+ _socketChanged = true;
+ return old;
+ }
+ }
+}
diff --git a/router/java/src/net/i2p/router/transport/udp/UDPTransport.java b/router/java/src/net/i2p/router/transport/udp/UDPTransport.java
new file mode 100644
index 000000000..e37c87036
--- /dev/null
+++ b/router/java/src/net/i2p/router/transport/udp/UDPTransport.java
@@ -0,0 +1,546 @@
+package net.i2p.router.transport.udp;
+
+import java.net.InetAddress;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+
+import java.io.IOException;
+import java.io.Writer;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+import net.i2p.data.Base64;
+import net.i2p.data.DataHelper;
+import net.i2p.data.Hash;
+import net.i2p.data.RouterAddress;
+import net.i2p.data.RouterInfo;
+import net.i2p.data.SessionKey;
+import net.i2p.data.i2np.I2NPMessage;
+import net.i2p.router.OutNetMessage;
+import net.i2p.router.RouterContext;
+import net.i2p.router.transport.Transport;
+import net.i2p.router.transport.TransportImpl;
+import net.i2p.router.transport.TransportBid;
+import net.i2p.util.Log;
+
+/**
+ *
+ */
+public class UDPTransport extends TransportImpl implements TimedWeightedPriorityMessageQueue.FailedListener {
+ private RouterContext _context;
+ private Log _log;
+ private UDPEndpoint _endpoint;
+ /** Peer (Hash) to PeerState */
+ private Map _peersByIdent;
+ /** Remote host (ip+port as a string) to PeerState */
+ private Map _peersByRemoteHost;
+ /** Relay tag (base64 String) to PeerState */
+ private Map _peersByRelayTag;
+ private PacketHandler _handler;
+ private EstablishmentManager _establisher;
+ private MessageQueue _outboundMessages;
+ private OutboundMessageFragments _fragments;
+ private OutboundRefiller _refiller;
+ private PacketPusher _pusher;
+ private InboundMessageFragments _inboundFragments;
+
+ /** list of RelayPeer objects for people who will relay to us */
+ private List _relayPeers;
+
+ /** summary info to distribute */
+ private RouterAddress _externalAddress;
+ /** port number on which we can be reached, or -1 */
+ private int _externalListenPort;
+ /** IP address of externally reachable host, or null */
+ private InetAddress _externalListenHost;
+ /** introduction key */
+ private SessionKey _introKey;
+
+ /** shared fast bid for connected peers */
+ private TransportBid _fastBid;
+ /** shared slow bid for unconnected peers */
+ private TransportBid _slowBid;
+
+ public static final String STYLE = "udp";
+ public static final String PROP_INTERNAL_PORT = "i2np.udp.internalPort";
+
+ /** define this to explicitly set an external IP address */
+ public static final String PROP_EXTERNAL_HOST = "i2np.udp.host";
+ /** define this to explicitly set an external port */
+ public static final String PROP_EXTERNAL_PORT = "i2np.udp.port";
+
+
+ /** how many relays offered to us will we use at a time? */
+ public static final int PUBLIC_RELAY_COUNT = 3;
+
+ /** configure the priority queue with the given split points */
+ private static final int PRIORITY_LIMITS[] = new int[] { 100, 200, 300, 400, 500, 1000 };
+ /** configure the priority queue with the given weighting per priority group */
+ private static final int PRIORITY_WEIGHT[] = new int[] { 1, 1, 1, 1, 1, 2 };
+
+ public UDPTransport(RouterContext ctx) {
+ super(ctx);
+ _context = ctx;
+ _log = ctx.logManager().getLog(UDPTransport.class);
+ _peersByIdent = new HashMap(128);
+ _peersByRemoteHost = new HashMap(128);
+ _peersByRelayTag = new HashMap(128);
+ _endpoint = null;
+
+ _outboundMessages = new TimedWeightedPriorityMessageQueue(ctx, PRIORITY_LIMITS, PRIORITY_WEIGHT, this);
+ _relayPeers = new ArrayList(1);
+
+ _fastBid = new SharedBid(50);
+ _slowBid = new SharedBid(100);
+
+ _fragments = new OutboundMessageFragments(_context, this);
+ _inboundFragments = new InboundMessageFragments(_context, _fragments, this);
+ }
+
+ public void startup() {
+ if (_fragments != null)
+ _fragments.shutdown();
+ if (_pusher != null)
+ _pusher.shutdown();
+ if (_handler != null)
+ _handler.shutdown();
+ if (_endpoint != null)
+ _endpoint.shutdown();
+ if (_establisher != null)
+ _establisher.shutdown();
+ if (_refiller != null)
+ _refiller.shutdown();
+ if (_inboundFragments != null)
+ _inboundFragments.shutdown();
+
+ _introKey = new SessionKey(new byte[SessionKey.KEYSIZE_BYTES]);
+ System.arraycopy(_context.routerHash().getData(), 0, _introKey.getData(), 0, SessionKey.KEYSIZE_BYTES);
+
+ rebuildExternalAddress();
+
+ if (_endpoint == null) {
+ int port = -1;
+ if (_externalListenPort <= 0) {
+ // no explicit external port, so lets try an internal one
+ String portStr = _context.getProperty(PROP_INTERNAL_PORT);
+ if (portStr != null) {
+ try {
+ port = Integer.parseInt(portStr);
+ } catch (NumberFormatException nfe) {
+ if (_log.shouldLog(Log.ERROR))
+ _log.error("Invalid port specified [" + portStr + "]");
+ }
+ }
+ if (port <= 0) {
+ port = 1024 + _context.random().nextInt(31*1024);
+ if (_log.shouldLog(Log.INFO))
+ _log.info("Selecting a random port to bind to: " + port);
+ }
+ } else {
+ port = _externalListenPort;
+ if (_log.shouldLog(Log.INFO))
+ _log.info("Binding to the explicitly specified external port: " + port);
+ }
+ try {
+ _endpoint = new UDPEndpoint(_context, port);
+ } catch (SocketException se) {
+ if (_log.shouldLog(Log.CRIT))
+ _log.log(Log.CRIT, "Unable to listen on the UDP port (" + port + ")", se);
+ return;
+ }
+ }
+
+ if (_establisher == null)
+ _establisher = new EstablishmentManager(_context, this);
+
+ if (_handler == null)
+ _handler = new PacketHandler(_context, this, _endpoint, _establisher, _inboundFragments);
+
+ if (_refiller == null)
+ _refiller = new OutboundRefiller(_context, _fragments, _outboundMessages);
+
+ _endpoint.startup();
+ _establisher.startup();
+ _handler.startup();
+ _fragments.startup();
+ _inboundFragments.startup();
+ _pusher = new PacketPusher(_context, _fragments, _endpoint.getSender());
+ _pusher.startup();
+ _refiller.startup();
+ }
+
+ public void shutdown() {
+ if (_refiller != null)
+ _refiller.shutdown();
+ if (_handler != null)
+ _handler.shutdown();
+ if (_endpoint != null)
+ _endpoint.shutdown();
+ if (_fragments != null)
+ _fragments.shutdown();
+ if (_pusher != null)
+ _pusher.shutdown();
+ if (_establisher != null)
+ _establisher.shutdown();
+ if (_inboundFragments != null)
+ _inboundFragments.shutdown();
+ }
+
+ /**
+ * Introduction key that people should use to contact us
+ *
+ */
+ public SessionKey getIntroKey() { return _introKey; }
+ public int getLocalPort() { return _externalListenPort; }
+ public InetAddress getLocalAddress() { return _externalListenHost; }
+ public int getExternalPort() { return _externalListenPort; }
+
+ /**
+ * Someone we tried to contact gave us what they think our IP address is.
+ * Right now, we just blindly trust them, changing our IP and port on a
+ * whim. this is not good ;)
+ *
+ */
+ void externalAddressReceived(byte ourIP[], int ourPort) {
+ if (_log.shouldLog(Log.WARN))
+ _log.debug("External address received: " + Base64.encode(ourIP) + ":" + ourPort);
+
+ if (explicitAddressSpecified())
+ return;
+
+ synchronized (this) {
+ if ( (_externalListenHost == null) ||
+ (!eq(_externalListenHost.getAddress(), _externalListenPort, ourIP, ourPort)) ) {
+ try {
+ _externalListenHost = InetAddress.getByAddress(ourIP);
+ _externalListenPort = ourPort;
+ rebuildExternalAddress();
+ replaceAddress(_externalAddress);
+ } catch (UnknownHostException uhe) {
+ _externalListenHost = null;
+ }
+ }
+ }
+ }
+
+ private static final boolean eq(byte laddr[], int lport, byte raddr[], int rport) {
+ return (rport == lport) && DataHelper.eq(laddr, raddr);
+ }
+
+ /**
+ * get the state for the peer at the given remote host/port, or null
+ * if no state exists
+ */
+ public PeerState getPeerState(InetAddress remoteHost, int remotePort) {
+ String hostInfo = PeerState.calculateRemoteHostString(remoteHost.getAddress(), remotePort);
+ synchronized (_peersByRemoteHost) {
+ return (PeerState)_peersByRemoteHost.get(hostInfo);
+ }
+ }
+
+ /**
+ * get the state for the peer with the given ident, or null
+ * if no state exists
+ */
+ public PeerState getPeerState(Hash remotePeer) {
+ synchronized (_peersByIdent) {
+ return (PeerState)_peersByIdent.get(remotePeer);
+ }
+ }
+
+ /**
+ * get the state for the peer being introduced, or null if we aren't
+ * offering to introduce anyone with that tag.
+ */
+ public PeerState getPeerState(String relayTag) {
+ synchronized (_peersByRelayTag) {
+ return (PeerState)_peersByRelayTag.get(relayTag);
+ }
+ }
+
+ /**
+ * add the peer info, returning true if it went in properly, false if
+ * it was rejected (causes include peer ident already connected, or no
+ * remote host info known
+ *
+ */
+ boolean addRemotePeerState(PeerState peer) {
+ if (_log.shouldLog(Log.WARN))
+ _log.debug("Add remote peer state: " + peer);
+ if (peer.getRemotePeer() != null) {
+ synchronized (_peersByIdent) {
+ PeerState oldPeer = (PeerState)_peersByIdent.put(peer.getRemotePeer(), peer);
+ if ( (oldPeer != null) && (oldPeer != peer) ) {
+ _peersByIdent.put(oldPeer.getRemotePeer(), oldPeer);
+ return false;
+ }
+ }
+ }
+
+ String remoteString = peer.getRemoteHostString();
+ if (remoteString == null) return false;
+
+ synchronized (_peersByRemoteHost) {
+ PeerState oldPeer = (PeerState)_peersByRemoteHost.put(remoteString, peer);
+ if ( (oldPeer != null) && (oldPeer != peer) ) {
+ _peersByRemoteHost.put(remoteString, oldPeer);
+ return false;
+ }
+ }
+
+ _context.shitlist().unshitlistRouter(peer.getRemotePeer());
+
+ return true;
+ }
+
+ int send(UDPPacket packet) {
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("Sending packet " + packet);
+ return _endpoint.send(packet);
+ }
+
+ public TransportBid bid(RouterInfo toAddress, long dataSize) {
+ Hash to = toAddress.getIdentity().calculateHash();
+ PeerState peer = getPeerState(to);
+ if (peer != null) {
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("bidding on a message to an established peer: " + peer);
+ return _fastBid;
+ } else {
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("bidding on a message to an unestablished peer: " + to.toBase64());
+ return _slowBid;
+ }
+ }
+
+ public String getStyle() { return STYLE; }
+ public void send(OutNetMessage msg) {
+ Hash to = msg.getTarget().getIdentity().calculateHash();
+ if (getPeerState(to) != null) {
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("Sending outbound message to an established peer: " + to.toBase64());
+ _outboundMessages.add(msg);
+ } else {
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("Sending outbound message to an unestablished peer: " + to.toBase64());
+ _establisher.establish(msg);
+ }
+ }
+ void send(I2NPMessage msg, PeerState peer) {
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("Injecting a data message to a new peer: " + peer);
+ OutboundMessageState state = new OutboundMessageState(_context);
+ state.initialize(msg, peer);
+ _fragments.add(state);
+ }
+
+ public OutNetMessage getNextMessage() { return getNextMessage(-1); }
+ /**
+ * Get the next message, blocking until one is found or the expiration
+ * reached.
+ *
+ * @param blockUntil expiration, or -1 if indefinite
+ */
+ public OutNetMessage getNextMessage(long blockUntil) {
+ return _outboundMessages.getNext(blockUntil);
+ }
+
+
+ // we don't need the following, since we have our own queueing
+ protected void outboundMessageReady() { throw new UnsupportedOperationException("Not used for UDP"); }
+
+ public RouterAddress startListening() {
+ startup();
+ return _externalAddress;
+ }
+
+ public void stopListening() {
+ shutdown();
+ }
+
+ void setExternalListenPort(int port) { _externalListenPort = port; }
+ void setExternalListenHost(InetAddress addr) { _externalListenHost = addr; }
+ void setExternalListenHost(byte addr[]) throws UnknownHostException {
+ _externalListenHost = InetAddress.getByAddress(addr);
+ }
+ void addRelayPeer(String host, int port, byte tag[], SessionKey relayIntroKey) {
+ if ( (_externalListenPort > 0) && (_externalListenHost != null) )
+ return; // no need for relay peers, as we are reachable
+
+ RelayPeer peer = new RelayPeer(host, port, tag, relayIntroKey);
+ synchronized (_relayPeers) {
+ _relayPeers.add(peer);
+ }
+ }
+
+ private boolean explicitAddressSpecified() {
+ return (_context.getProperty(PROP_EXTERNAL_HOST) != null);
+ }
+
+ void rebuildExternalAddress() {
+ if (explicitAddressSpecified()) {
+ try {
+ String host = _context.getProperty(PROP_EXTERNAL_HOST);
+ String port = _context.getProperty(PROP_EXTERNAL_PORT);
+ _externalListenHost = InetAddress.getByName(host);
+ _externalListenPort = Integer.parseInt(port);
+ } catch (UnknownHostException uhe) {
+ _externalListenHost = null;
+ } catch (NumberFormatException nfe) {
+ _externalListenPort = -1;
+ }
+ }
+
+ Properties options = new Properties();
+ if ( (_externalListenPort > 0) && (_externalListenHost != null) ) {
+ options.setProperty(UDPAddress.PROP_PORT, String.valueOf(_externalListenPort));
+ options.setProperty(UDPAddress.PROP_HOST, _externalListenHost.getHostAddress());
+ } else {
+ // grab 3 relays randomly
+ synchronized (_relayPeers) {
+ Collections.shuffle(_relayPeers);
+ int numPeers = PUBLIC_RELAY_COUNT;
+ if (numPeers > _relayPeers.size())
+ numPeers = _relayPeers.size();
+ for (int i = 0; i < numPeers; i++) {
+ RelayPeer peer = (RelayPeer)_relayPeers.get(i);
+ options.setProperty("relay." + i + ".host", peer.getHost());
+ options.setProperty("relay." + i + ".port", String.valueOf(peer.getPort()));
+ options.setProperty("relay." + i + ".tag", Base64.encode(peer.getTag()));
+ options.setProperty("relay." + i + ".key", peer.getIntroKey().toBase64());
+ }
+ }
+ if (options.size() <= 0)
+ return;
+ }
+ options.setProperty(UDPAddress.PROP_INTRO_KEY, _introKey.toBase64());
+
+ RouterAddress addr = new RouterAddress();
+ addr.setCost(5);
+ addr.setExpiration(null);
+ addr.setTransportStyle(STYLE);
+ addr.setOptions(options);
+
+ _externalAddress = addr;
+ replaceAddress(addr);
+ }
+
+ public void failed(OutNetMessage msg) {
+ if (msg == null) return;
+ if (_log.shouldLog(Log.WARN))
+ _log.warn("Sending message failed: " + msg, new Exception("failed from"));
+ super.afterSend(msg, false);
+ }
+ public void succeeded(OutNetMessage msg) {
+ if (msg == null) return;
+ if (_log.shouldLog(Log.INFO))
+ _log.info("Sending message succeeded: " + msg);
+ super.afterSend(msg, true);
+ }
+
+ public int countActivePeers() {
+ long now = _context.clock().now();
+ int active = 0;
+ int inactive = 0;
+ synchronized (_peersByIdent) {
+ for (Iterator iter = _peersByIdent.values().iterator(); iter.hasNext(); ) {
+ PeerState peer = (PeerState)iter.next();
+ if (now-peer.getLastReceiveTime() > 5*60*1000)
+ inactive++;
+ else
+ active++;
+ }
+ }
+ return active;
+ }
+
+ public void renderStatusHTML(Writer out) throws IOException {
+ List peers = null;
+ synchronized (_peersByIdent) {
+ peers = new ArrayList(_peersByIdent.values());
+ }
+
+ StringBuffer buf = new StringBuffer(512);
+ buf.append("UDP connections: ").append(peers.size()).append("
\n");
+ buf.append("
Peer | Location | \n"); + buf.append("Last send | Last recv | \n"); + buf.append("Lifetime | Window size | \n"); + buf.append("Sent | Received | \n"); + buf.append("
"); + buf.append(peer.getRemotePeer().toBase64().substring(0,6)); + buf.append(" | "); + + buf.append(""); + byte ip[] = peer.getRemoteIP(); + for (int j = 0; j < ip.length; j++) { + if (ip[j] < 0) + buf.append(ip[j] + 255); + else + buf.append(ip[j]); + if (j + 1 < ip.length) + buf.append('.'); + } + buf.append(':').append(peer.getRemotePort()); + buf.append(" | "); + + buf.append(""); + buf.append(DataHelper.formatDuration(now-peer.getLastSendTime())); + buf.append(" | "); + + buf.append(""); + buf.append(DataHelper.formatDuration(now-peer.getLastReceiveTime())); + buf.append(" | "); + + buf.append(""); + buf.append(DataHelper.formatDuration(now-peer.getKeyEstablishedTime())); + buf.append(" | "); + + buf.append(""); + buf.append(peer.getSendWindowBytes()); + buf.append(" | "); + + buf.append(""); + buf.append(peer.getMessagesSent()); + buf.append(" | "); + + buf.append(""); + buf.append(peer.getMessagesReceived()); + buf.append(" | "); + + buf.append("