diff --git a/core/java/src/net/i2p/crypto/DHSessionKeyBuilder.java b/core/java/src/net/i2p/crypto/DHSessionKeyBuilder.java index b3c5813f13bf05746f0092b65365166dd5d1b69f..a96a0846a08ab5eb65c73de16743df750f7e74dd 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 59d7fe5974526b95c8298cfcd6af0ced2396c1dd..dd4325fb15ec49befaa56f7229a3b8ce14fafb45 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 1f5d6bdfabbdab1bea8b51c9f6b52814701172af..3ef157ec7491b0bf86e12293dc09b6ba42367b19 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 cea2087b9ad6c43f8beae488bd25be3da1bf0b19..2e6f09149c6705acb22b8839e8b9f0e61a6fb844 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 344e8427ea23b7cab33e0a8c8405233b3f3e7af3..9f3ee4e5351342c52ababafd2efa147ec63d62cb 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 147ad38d92a22964ef5c6fd1062d5576c80ca246..0da951571a33a2fc6e405fc3378cb0ce72a06633 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 a03ea9c9e92d1eb5ce5abf83d29f53522b44acd9..eea745ae23dcde55472a488e71fdb3f7010581ae 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("</pre>\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 70130f32c904faebe4aae7cd413e63dc487b3448..289312c92bdd9104fd23c40c3f1490664e58f53b 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("</ul>"); - 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 0000000000000000000000000000000000000000..8ec022e7c28db53964f918545f201f35690c74a6 --- /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 0000000000000000000000000000000000000000..04654f6b2fcdb0a65fda5b2fb5168804f4360430 --- /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 0000000000000000000000000000000000000000..d24097031afd0cbdc3a2fabfe762d4e8b31a92a2 --- /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 0000000000000000000000000000000000000000..1738e3da902842ed3239b87175aa284a6956ff86 --- /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 0000000000000000000000000000000000000000..a07d45341f5b82d24a48b59e4164575a9bea0d6a --- /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 0000000000000000000000000000000000000000..cc38ebbafec279bca630c800643bc32c2177e144 --- /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 0000000000000000000000000000000000000000..c13071159a97ecc1a8f75011f8e9870d5f565c4c --- /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 0000000000000000000000000000000000000000..e359bccdf09b135d483a0f0f49c0b87862e65a8c --- /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 0000000000000000000000000000000000000000..60c2503a2c767ff2934e8e4c1eee461628da7f93 --- /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 0000000000000000000000000000000000000000..cff837dace30214755b55b1516d63bf65f9e189c --- /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 0000000000000000000000000000000000000000..ee41ad8c096a91bb188050062fa813680b65bf15 --- /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 0000000000000000000000000000000000000000..cdeb8916956cf0d6d7e8255476d08804a0c77d72 --- /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 0000000000000000000000000000000000000000..dacd2d8d7716e97cf245e24573923990b8ada010 --- /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 0000000000000000000000000000000000000000..392f70299190c9dfa8aca20885f0db192a701ff2 --- /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 0000000000000000000000000000000000000000..33cfde591a6347976ee285c3dc85cd3f6fcd0b5a --- /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 0000000000000000000000000000000000000000..6b42d0e2eac2f3b07e9179b0d1c58a853b1ac2fb --- /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 0000000000000000000000000000000000000000..335dd548c62d32419ca484834a88ffd71bf9bae6 --- /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 0000000000000000000000000000000000000000..c8ebe9a4f2da480584df33de5633d17e0c9ab0a3 --- /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 0000000000000000000000000000000000000000..b40cf0c5db2853e3843d4aab4d767338c53a2a5b --- /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 0000000000000000000000000000000000000000..52d533adb7d0f8a6de192c2f936ef7a1f790970a --- /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 0000000000000000000000000000000000000000..4fd392b34e0db6c311b74ae95db5f571b76ecb6a --- /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 0000000000000000000000000000000000000000..b9cf54299eea8bce02c285ac2a0ece50995aefb2 --- /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 0000000000000000000000000000000000000000..144ae839d4742e50851199574c2b1719f5e06d98 --- /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 0000000000000000000000000000000000000000..e20cd2c30c7118ce0c8a8425b37320b062b39f51 --- /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 0000000000000000000000000000000000000000..e37c87036db92b9fdca3fbeade47324fb5c31e9d --- /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("<b>UDP connections: ").append(peers.size()).append("</b><br />\n"); + buf.append("<table border=\"1\">\n"); + buf.append(" <tr><td><b>Peer</b></td><td><b>Location</b></td>\n"); + buf.append(" <td><b>Last send</b></td><td><b>Last recv</b></td>\n"); + buf.append(" <td><b>Lifetime</b></td><td><b>Window size</b></td>\n"); + buf.append(" <td><b>Sent</b></td><td><b>Received</b></td>\n"); + buf.append(" </tr>\n"); + out.write(buf.toString()); + buf.setLength(0); + long now = _context.clock().now(); + for (int i = 0; i < peers.size(); i++) { + PeerState peer = (PeerState)peers.get(i); + if (now-peer.getLastReceiveTime() > 60*60*1000) + continue; // don't include old peers + + buf.append("<tr>"); + + buf.append("<td>"); + buf.append(peer.getRemotePeer().toBase64().substring(0,6)); + buf.append("</td>"); + + buf.append("<td>"); + 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("</td>"); + + buf.append("<td>"); + buf.append(DataHelper.formatDuration(now-peer.getLastSendTime())); + buf.append("</td>"); + + buf.append("<td>"); + buf.append(DataHelper.formatDuration(now-peer.getLastReceiveTime())); + buf.append("</td>"); + + buf.append("<td>"); + buf.append(DataHelper.formatDuration(now-peer.getKeyEstablishedTime())); + buf.append("</td>"); + + buf.append("<td>"); + buf.append(peer.getSendWindowBytes()); + buf.append("</td>"); + + buf.append("<td>"); + buf.append(peer.getMessagesSent()); + buf.append("</td>"); + + buf.append("<td>"); + buf.append(peer.getMessagesReceived()); + buf.append("</td>"); + + buf.append("</tr>"); + out.write(buf.toString()); + buf.setLength(0); + } + + out.write("</table>\n"); + } + + + /** + * Cache the bid to reduce object churn + */ + private class SharedBid extends TransportBid { + private int _ms; + public SharedBid(int ms) { _ms = ms; } + public int getLatency() { return _ms; } + public Transport getTransport() { return UDPTransport.this; } + } +} diff --git a/router/java/src/net/i2p/router/tunnel/TunnelParticipant.java b/router/java/src/net/i2p/router/tunnel/TunnelParticipant.java index 1a691ecae32566c4fceb31f21cfcc0b2dba69506..484cab14ec2328fa26088b3a4253df3046d3e041 100644 --- a/router/java/src/net/i2p/router/tunnel/TunnelParticipant.java +++ b/router/java/src/net/i2p/router/tunnel/TunnelParticipant.java @@ -68,8 +68,8 @@ public class TunnelParticipant { ok = _inboundEndpointProcessor.retrievePreprocessedData(msg.getData(), 0, msg.getData().length, recvFrom); if (!ok) { - if (_log.shouldLog(Log.ERROR)) - _log.error("Failed to dispatch " + msg + ": processor=" + _processor + if (_log.shouldLog(Log.WARN)) + _log.warn("Failed to dispatch " + msg + ": processor=" + _processor + " inboundEndpoint=" + _inboundEndpointProcessor); return; }