diff --git a/build.xml b/build.xml index 57f8bddf71..1de72f42b0 100644 --- a/build.xml +++ b/build.xml @@ -57,7 +57,7 @@ - 257) + throw new IllegalArgumentException("Value is too large! length="+x.length); + + out.flush(); + } + private static final int getSize() { synchronized (_builders) { return _builders.size(); diff --git a/core/java/src/net/i2p/data/DataHelper.java b/core/java/src/net/i2p/data/DataHelper.java index a186ff38a3..84f69aa018 100644 --- a/core/java/src/net/i2p/data/DataHelper.java +++ b/core/java/src/net/i2p/data/DataHelper.java @@ -99,24 +99,28 @@ public class DataHelper { */ public static void writeProperties(OutputStream rawStream, Properties props) throws DataFormatException, IOException { - OrderedProperties p = new OrderedProperties(); - if (props != null) p.putAll(props); - ByteArrayOutputStream baos = new ByteArrayOutputStream(32); - for (Iterator iter = p.keySet().iterator(); iter.hasNext();) { - String key = (String) iter.next(); - String val = p.getProperty(key); - // now make sure they're in UTF-8 - //key = new String(key.getBytes(), "UTF-8"); - //val = new String(val.getBytes(), "UTF-8"); - writeString(baos, key); - baos.write(_equalBytes); - writeString(baos, val); - baos.write(_semicolonBytes); + if (props != null) { + OrderedProperties p = new OrderedProperties(); + p.putAll(props); + ByteArrayOutputStream baos = new ByteArrayOutputStream(32); + for (Iterator iter = p.keySet().iterator(); iter.hasNext();) { + String key = (String) iter.next(); + String val = p.getProperty(key); + // now make sure they're in UTF-8 + //key = new String(key.getBytes(), "UTF-8"); + //val = new String(val.getBytes(), "UTF-8"); + writeString(baos, key); + baos.write(_equalBytes); + writeString(baos, val); + baos.write(_semicolonBytes); + } + baos.close(); + byte propBytes[] = baos.toByteArray(); + writeLong(rawStream, 2, propBytes.length); + rawStream.write(propBytes); + } else { + writeLong(rawStream, 2, 0); } - baos.close(); - byte propBytes[] = baos.toByteArray(); - writeLong(rawStream, 2, propBytes.length); - rawStream.write(propBytes); } /** diff --git a/core/java/src/net/i2p/data/RouterInfo.java b/core/java/src/net/i2p/data/RouterInfo.java index ade83c33d6..77f9b75e8f 100644 --- a/core/java/src/net/i2p/data/RouterInfo.java +++ b/core/java/src/net/i2p/data/RouterInfo.java @@ -322,6 +322,22 @@ public class RouterInfo extends DataStructureImpl { return true; } + + + /** + * Pull the first workable target address for the given transport + * + */ + public RouterAddress getTargetAddress(String transportStyle) { + synchronized (_addresses) { + for (Iterator iter = _addresses.iterator(); iter.hasNext(); ) { + RouterAddress addr = (RouterAddress)iter.next(); + if (addr.getTransportStyle().equals(transportStyle)) + return addr; + } + } + return null; + } /** * Actually validate the signature diff --git a/history.txt b/history.txt index 1441d72976..be4251282d 100644 --- a/history.txt +++ b/history.txt @@ -1,4 +1,20 @@ -$Id: history.txt,v 1.18 2004/09/16 18:55:12 jrandom Exp $ +$Id: history.txt,v 1.19 2004/09/21 19:10:26 jrandom Exp $ + +2004-09-26 jrandom + * Complete rewrite of the TCP transport with IP autodetection and + low CPU overhead reconnections. More concise connectivity errors + are listed on the /oldconsole.jsp as well. The IP autodetection works + by listening to the first person who tells you what your IP address is + when you have not defined one yourself and you have no other TCP + connections. + * Update to the I2NP message format to add transparent verification at + the I2NP level (beyond standard TCP verification). + * Remove a potential weakness in our AESEngine's safeEncrypt and safeDecrypt + implementation (rather than verifying with E(H(key)), we now verify with + E(H(iv))). + * The above changes are NOT BACKWARDS COMPATIBLE. + * Removed all of the old unused PHTTP code. + * Refactor various methods and clean up some javadoc. 2004-09-21 jrandom * Have two tiers of hosts.txt files - the standard "hosts.txt" and diff --git a/router/java/src/net/i2p/data/i2np/I2NPMessage.java b/router/java/src/net/i2p/data/i2np/I2NPMessage.java index 7848e6b255..e4e54757f8 100644 --- a/router/java/src/net/i2p/data/i2np/I2NPMessage.java +++ b/router/java/src/net/i2p/data/i2np/I2NPMessage.java @@ -50,4 +50,7 @@ public interface I2NPMessage extends DataStructure { * */ public Date getMessageExpiration(); + + /** How large the message is, including any checksums */ + public int getSize(); } diff --git a/router/java/src/net/i2p/data/i2np/I2NPMessageImpl.java b/router/java/src/net/i2p/data/i2np/I2NPMessageImpl.java index e178a78140..faf69a6474 100644 --- a/router/java/src/net/i2p/data/i2np/I2NPMessageImpl.java +++ b/router/java/src/net/i2p/data/i2np/I2NPMessageImpl.java @@ -8,6 +8,7 @@ package net.i2p.data.i2np; * */ +import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -17,6 +18,7 @@ import net.i2p.I2PAppContext; import net.i2p.data.DataFormatException; import net.i2p.data.DataHelper; import net.i2p.data.DataStructureImpl; +import net.i2p.data.Hash; import net.i2p.util.Log; /** @@ -72,12 +74,23 @@ public abstract class I2NPMessageImpl extends DataStructureImpl implements I2NPM type = (int)DataHelper.readLong(in, 1); _uniqueId = DataHelper.readLong(in, 4); _expiration = DataHelper.readDate(in); + int size = (int)DataHelper.readLong(in, 2); + Hash h = new Hash(); + h.readBytes(in); + byte data[] = new byte[size]; + int read = DataHelper.read(in, data); + if (read != size) + throw new I2NPMessageException("Payload is too short [" + read + ", wanted " + size + "]"); + Hash calc = _context.sha().calculateHash(data); + if (!calc.equals(h)) + throw new I2NPMessageException("Hash does not match"); + + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Reading bytes: type = " + type + " / uniqueId : " + _uniqueId + " / expiration : " + _expiration); + readMessage(new ByteArrayInputStream(data), type); } catch (DataFormatException dfe) { throw new I2NPMessageException("Error reading the message header", dfe); } - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Reading bytes: type = " + type + " / uniqueId : " + _uniqueId + " / expiration : " + _expiration); - readMessage(in, type); } public void writeBytes(OutputStream out) throws DataFormatException, IOException { try { @@ -87,6 +100,9 @@ public abstract class I2NPMessageImpl extends DataStructureImpl implements I2NPM if (_log.shouldLog(Log.DEBUG)) _log.debug("Writing bytes: type = " + getType() + " / uniqueId : " + _uniqueId + " / expiration : " + _expiration); byte[] data = writeMessage(); + DataHelper.writeLong(out, 2, data.length); + Hash h = _context.sha().calculateHash(data); + h.writeBytes(out); out.write(data); } catch (I2NPMessageException ime) { throw new DataFormatException("Error writing out the I2NP message data", ime); @@ -105,4 +121,15 @@ public abstract class I2NPMessageImpl extends DataStructureImpl implements I2NPM */ public Date getMessageExpiration() { return _expiration; } public void setMessageExpiration(Date exp) { _expiration = exp; } + + public int getSize() { + try { + byte msg[] = writeMessage(); + return msg.length + 43; + } catch (IOException ioe) { + return 0; + } catch (I2NPMessageException ime) { + return 0; + } + } } diff --git a/router/java/src/net/i2p/router/Router.java b/router/java/src/net/i2p/router/Router.java index 477a3f25aa..0ad54e3e42 100644 --- a/router/java/src/net/i2p/router/Router.java +++ b/router/java/src/net/i2p/router/Router.java @@ -30,6 +30,7 @@ import net.i2p.crypto.DHSessionKeyBuilder; import net.i2p.data.DataFormatException; import net.i2p.data.DataHelper; import net.i2p.data.RouterInfo; +import net.i2p.data.SigningPrivateKey; import net.i2p.data.i2np.GarlicMessage; import net.i2p.data.i2np.TunnelMessage; import net.i2p.router.message.GarlicMessageHandler; @@ -220,6 +221,38 @@ public class Router { } public boolean isAlive() { return _isAlive; } + + /** + * Rebuild and republish our routerInfo since something significant + * has changed. + */ + public void rebuildRouterInfo() { + if (_log.shouldLog(Log.INFO)) + _log.info("Rebuilding new routerInfo"); + + RouterInfo ri = null; + if (_routerInfo != null) + ri = new RouterInfo(_routerInfo); + else + ri = new RouterInfo(); + + try { + ri.setPublished(_context.clock().now()); + Properties stats = _context.statPublisher().publishStatistics(); + ri.setOptions(stats); + ri.setAddresses(_context.commSystem().createAddresses()); + SigningPrivateKey key = _context.keyManager().getSigningPrivateKey(); + if (key == null) { + _log.log(Log.CRIT, "Internal error - signing private key not known? wtf"); + return; + } + ri.sign(key); + setRouterInfo(ri); + _context.netDb().publish(ri); + } catch (DataFormatException dfe) { + _log.log(Log.CRIT, "Internal error - unable to sign our own address?!", dfe); + } + } /** * coallesce the stats framework every minute diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java index 3609b73729..4f1475e927 100644 --- a/router/java/src/net/i2p/router/RouterVersion.java +++ b/router/java/src/net/i2p/router/RouterVersion.java @@ -15,9 +15,9 @@ import net.i2p.CoreVersion; * */ public class RouterVersion { - public final static String ID = "$Revision: 1.31 $ $Date: 2004/09/16 18:55:12 $"; + public final static String ID = "$Revision: 1.32 $ $Date: 2004/09/21 19:10:26 $"; public final static String VERSION = "0.4.0.1"; - public final static long BUILD = 3; + public final static long BUILD = 4; public static void main(String args[]) { System.out.println("I2P Router version: " + VERSION); System.out.println("Router ID: " + RouterVersion.ID); diff --git a/router/java/src/net/i2p/router/transport/CommSystemFacadeImpl.java b/router/java/src/net/i2p/router/transport/CommSystemFacadeImpl.java index 5923f7f53a..8f52456b9b 100644 --- a/router/java/src/net/i2p/router/transport/CommSystemFacadeImpl.java +++ b/router/java/src/net/i2p/router/transport/CommSystemFacadeImpl.java @@ -19,7 +19,6 @@ import net.i2p.data.RouterAddress; import net.i2p.router.CommSystemFacade; import net.i2p.router.OutNetMessage; import net.i2p.router.RouterContext; -import net.i2p.router.transport.phttp.PHTTPTransport; import net.i2p.router.transport.tcp.TCPTransport; import net.i2p.util.Log; @@ -70,9 +69,6 @@ public class CommSystemFacadeImpl extends CommSystemFacade { public Set createAddresses() { Set addresses = new HashSet(); RouterAddress addr = createTCPAddress(); - if (addr != null) - addresses.add(addr); - addr = createPHTTPAddress(); if (addr != null) addresses.add(addr); if (_log.shouldLog(Log.INFO)) @@ -82,8 +78,6 @@ public class CommSystemFacadeImpl extends CommSystemFacade { private final static String PROP_I2NP_TCP_HOSTNAME = "i2np.tcp.hostname"; private final static String PROP_I2NP_TCP_PORT = "i2np.tcp.port"; - private final static String PROP_I2NP_PHTTP_SEND_URL = "i2np.phttp.sendURL"; - private final static String PROP_I2NP_PHTTP_REGISTER_URL = "i2np.phttp.registerURL"; private RouterAddress createTCPAddress() { RouterAddress addr = new RouterAddress(); @@ -104,23 +98,4 @@ public class CommSystemFacadeImpl extends CommSystemFacade { addr.setTransportStyle(TCPTransport.STYLE); return addr; } - private RouterAddress createPHTTPAddress() { - RouterAddress addr = new RouterAddress(); - addr.setCost(50); - addr.setExpiration(null); - Properties props = new Properties(); - String regURL = _context.router().getConfigSetting(PROP_I2NP_PHTTP_REGISTER_URL); - String sendURL = _context.router().getConfigSetting(PROP_I2NP_PHTTP_SEND_URL); - if ( (regURL == null) || (sendURL == null) ) { - _log.info("Polling HTTP registration/send URLs not specified in config file - skipping PHTTP transport"); - return null; - } else { - _log.info("Creating Polling HTTP address on " + regURL + " / " + sendURL); - } - props.setProperty(PHTTPTransport.PROP_TO_REGISTER_URL, regURL); - props.setProperty(PHTTPTransport.PROP_TO_SEND_URL, sendURL); - addr.setOptions(props); - addr.setTransportStyle(PHTTPTransport.STYLE); - return addr; - } } diff --git a/router/java/src/net/i2p/router/transport/TransportImpl.java b/router/java/src/net/i2p/router/transport/TransportImpl.java index ab76b0998e..8c76fb4afd 100644 --- a/router/java/src/net/i2p/router/transport/TransportImpl.java +++ b/router/java/src/net/i2p/router/transport/TransportImpl.java @@ -37,6 +37,10 @@ public abstract class TransportImpl implements Transport { private List _sendPool; protected RouterContext _context; + /** + * Initialize the new transport + * + */ public TransportImpl(RouterContext context) { _context = context; _log = _context.logManager().getLog(TransportImpl.class); @@ -52,8 +56,18 @@ public abstract class TransportImpl implements Transport { _currentAddresses = new HashSet(); } + /** + * How many peers can we talk to right now? + * + */ public int countActivePeers() { return 0; } + /** + * Nonblocking call to pull the next outbound message + * off the queue. + * + * @return the next message or null if none are available + */ public OutNetMessage getNextMessage() { OutNetMessage msg = null; synchronized (_sendPool) { @@ -64,16 +78,45 @@ public abstract class TransportImpl implements Transport { return msg; } - public void afterSend(OutNetMessage msg, boolean sendSuccessful) { + /** + * The transport is done sending this message + * + * @param msg message in question + * @param sendSuccessful true if the peer received it + */ + protected void afterSend(OutNetMessage msg, boolean sendSuccessful) { afterSend(msg, sendSuccessful, true, 0); } - public void afterSend(OutNetMessage msg, boolean sendSuccessful, boolean allowRequeue) { + /** + * The transport is done sending this message + * + * @param msg message in question + * @param sendSuccessful true if the peer received it + * @param allowRequeue true if we should try other transports if available + */ + protected void afterSend(OutNetMessage msg, boolean sendSuccessful, boolean allowRequeue) { afterSend(msg, sendSuccessful, allowRequeue, 0); } - public void afterSend(OutNetMessage msg, boolean sendSuccessful, long msToSend) { + /** + * The transport is done sending this message + * + * @param msg message in question + * @param sendSuccessful true if the peer received it + * @param msToSend how long it took to transfer the data to the peer + */ + protected void afterSend(OutNetMessage msg, boolean sendSuccessful, long msToSend) { afterSend(msg, sendSuccessful, true, msToSend); } - public void afterSend(OutNetMessage msg, boolean sendSuccessful, boolean allowRequeue, long msToSend) { + /** + * The transport is done sending this message. This is the method that actually + * does all of the cleanup - firing off jobs, requeueing, updating stats, etc. + * + * @param msg message in question + * @param sendSuccessful true if the peer received it + * @param msToSend how long it took to transfer the data to the peer + * @param allowRequeue true if we should try other transports if available + */ + protected void afterSend(OutNetMessage msg, boolean sendSuccessful, boolean allowRequeue, long msToSend) { boolean log = false; msg.timestamp("afterSend(" + sendSuccessful + ")"); @@ -225,6 +268,10 @@ public abstract class TransportImpl implements Transport { */ protected abstract void outboundMessageReady(); + /** + * Message received from the I2NPMessageReader - send it to the listener + * + */ public void messageReceived(I2NPMessage inMsg, RouterIdentity remoteIdent, Hash remoteIdentHash, long msToReceive, int bytesReceived) { int level = Log.INFO; if (msToReceive > 5000) @@ -273,25 +320,17 @@ public abstract class TransportImpl implements Transport { _log.error("WTF! Null listener! this = " + toString(), new Exception("Null listener")); } } - - /** - * Pull the first workable target address for this transport - * - */ - protected RouterAddress getTargetAddress(RouterInfo address) { - if (address == null) return null; - for (Iterator iter = address.getAddresses().iterator(); iter.hasNext(); ) { - RouterAddress addr = (RouterAddress)iter.next(); - if (getStyle().equals(addr.getTransportStyle())) - return addr; - } - return null; - } - + + /** What addresses are we currently listening to? */ public Set getCurrentAddresses() { return _currentAddresses; } + /** Add an address to our listening set */ protected void addCurrentAddress(RouterAddress address) { _currentAddresses.add(address); } + /** Remove an address from our listening set */ protected void removeCurrentAddress(RouterAddress address) { _currentAddresses.remove(address); } + /** 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; } + + protected 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 0600c1e522..5cf949957c 100644 --- a/router/java/src/net/i2p/router/transport/TransportManager.java +++ b/router/java/src/net/i2p/router/transport/TransportManager.java @@ -30,7 +30,6 @@ import net.i2p.data.i2np.I2NPMessage; import net.i2p.router.InNetMessage; import net.i2p.router.OutNetMessage; import net.i2p.router.RouterContext; -import net.i2p.router.transport.phttp.PHTTPTransport; import net.i2p.router.transport.tcp.TCPTransport; import net.i2p.util.Log; @@ -62,31 +61,14 @@ public class TransportManager implements TransportEventListener { } private void configTransports() { - RouterIdentity ident = _context.router().getRouterInfo().getIdentity(); - Set addresses = _context.commSystem().createAddresses(); - RouterAddress tcpAddr = null; - RouterAddress phttpAddr = null; - for (Iterator iter = addresses.iterator(); iter.hasNext();) { - RouterAddress addr = (RouterAddress)iter.next(); - if (TCPTransport.STYLE.equals(addr.getTransportStyle())) { - tcpAddr = addr; - } - if (PHTTPTransport.STYLE.equals(addr.getTransportStyle())) { - phttpAddr = addr; - } - } - String disableTCP = _context.router().getConfigSetting(PROP_DISABLE_TCP); if ( (disableTCP != null) && (Boolean.TRUE.toString().equalsIgnoreCase(disableTCP)) ) { _log.info("Explicitly disabling the TCP transport!"); } else { - Transport t = new TCPTransport(_context, tcpAddr); + Transport t = new TCPTransport(_context); t.setListener(this); _transports.add(t); } - Transport t = new PHTTPTransport(_context, phttpAddr); - t.setListener(this); - _transports.add(t); } public void startListening() { diff --git a/router/java/src/net/i2p/router/transport/tcp/ConnectionBuilder.java b/router/java/src/net/i2p/router/transport/tcp/ConnectionBuilder.java new file mode 100644 index 0000000000..0704a617af --- /dev/null +++ b/router/java/src/net/i2p/router/transport/tcp/ConnectionBuilder.java @@ -0,0 +1,740 @@ +package net.i2p.router.transport.tcp; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.math.BigInteger; +import java.net.Socket; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +import net.i2p.crypto.AESInputStream; +import net.i2p.crypto.AESOutputStream; +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.Hash; +import net.i2p.data.RouterAddress; +import net.i2p.data.RouterInfo; +import net.i2p.data.SessionKey; +import net.i2p.data.Signature; +import net.i2p.router.RouterContext; +import net.i2p.util.I2PThread; +import net.i2p.util.Log; +import net.i2p.util.NativeBigInteger; +import net.i2p.util.SimpleTimer; + +/** + * Class responsible for all of the handshaking necessary to establish a + * connection with a peer. + * + */ +public class ConnectionBuilder { + private Log _log; + private RouterContext _context; + private TCPTransport _transport; + /** who we're trying to talk with */ + private RouterInfo _target; + /** who we're actually talking with */ + private RouterInfo _actualPeer; + /** raw socket to the peer */ + private Socket _socket; + /** raw stream to read from the peer */ + private InputStream _rawIn; + /** raw stream to write to the peer */ + private OutputStream _rawOut; + /** secure stream to read from the peer */ + private InputStream _connectionIn; + /** secure stream to write to the peer */ + private OutputStream _connectionOut; + /** protocol version agreed to, or -1 */ + private int _agreedProtocol; + /** IP address the peer says we are at */ + private String _localIP; + /** IP address of the peer we connected to */ + private TCPAddress _remoteAddress; + /** connection tag to identify ourselves, or null if no known tag is available */ + private ByteArray _connectionTag; + /** connection tag to identify ourselves next time */ + private ByteArray _nextConnectionTag; + /** nonce the peer gave us */ + private ByteArray _nonce; + /** key that we will be encrypting comm with */ + private SessionKey _key; + /** initialization vector for the encryption */ + private byte[] _iv; + /** + * Contains a message describing why the connection failed (or null if it + * succeeded). This should include a timestamp of some sort. + */ + private String _error; + + /** If the connection hasn't been built in 10 seconds, give up */ + public static final int CONNECTION_TIMEOUT = 10*1000; + + public ConnectionBuilder(RouterContext context, TCPTransport transport, RouterInfo info) { + _context = context; + _log = context.logManager().getLog(ConnectionBuilder.class); + _transport = transport; + _target = info; + _error = null; + _agreedProtocol = -1; + } + + /** + * Blocking call to establish a TCP connection to the given peer through a + * brand new socket. + * + * @return fully established but not yet running connection, or null on error + */ + public TCPConnection establishConnection() { + SimpleTimer.getInstance().addEvent(new DieIfTooSlow(), CONNECTION_TIMEOUT); + try { + return doEstablishConnection(); + } catch (Exception e) { // catchall in case the timeout gets us flat footed + _log.error("Error connecting", e); + return null; + } + } + private TCPConnection doEstablishConnection() { + createSocket(); + if ( (_socket == null) || (_error != null) ) + return null; + + negotiateProtocol(); + + if ( (_agreedProtocol < 0) || (_error != null) ) + return null; + + boolean ok = false; + if (_connectionTag != null) + ok = connectExistingSession(); + else + ok = connectNewSession(); + + if (_log.shouldLog(Log.DEBUG)) + _log.debug("connection ok? " + ok + " error: " + _error); + + if (ok && (_error == null) ) { + establishComplete(); + + TCPConnection con = new TCPConnection(_context); + con.setInputStream(_connectionIn); + con.setOutputStream(_connectionOut); + con.setSocket(_socket); + con.setRemoteRouterIdentity(_actualPeer.getIdentity()); + con.setRemoteAddress(_remoteAddress); + if (_error == null) { + if (_log.shouldLog(Log.INFO)) + _log.info("Establishment successful! returning the con"); + return con; + } else { + return null; + } + } else { + return null; + } + } + + /** + * Agree on what protocol to communicate with, and set _agreedProtocol + * accordingly. If no common protocols are available, disconnect, set + * _agreedProtocol to -1, and update the _error accordingly. + */ + private void negotiateProtocol() { + ConnectionTagManager mgr = _transport.getTagManager(); + ByteArray tag = mgr.getTag(_target.getIdentity().getHash()); + _key = mgr.getKey(_target.getIdentity().getHash()); + _connectionTag = tag; + boolean ok = sendPreferredProtocol(); + if (!ok) return; + ok = receiveAgreedProtocol(); + if (!ok) return; + } + + /** + * Send #bytesFollowing + #versions + v1 [+ v2 [etc]] + + * tag? + tagData + properties + */ + private boolean sendPreferredProtocol() { + try { + // #bytesFollowing + #versions + v1 [+ v2 [etc]] + tag? + tagData + properties + ByteArrayOutputStream baos = new ByteArrayOutputStream(64); + DataHelper.writeLong(baos, 1, TCPTransport.SUPPORTED_PROTOCOLS.length); + for (int i = 0; i < TCPTransport.SUPPORTED_PROTOCOLS.length; i++) { + DataHelper.writeLong(baos, 1, TCPTransport.SUPPORTED_PROTOCOLS[i]); + } + if (_connectionTag != null) { + baos.write(0x1); + baos.write(_connectionTag.getData()); + } else { + baos.write(0x0); + } + DataHelper.writeProperties(baos, null); // no options atm + byte line[] = baos.toByteArray(); + DataHelper.writeLong(_rawOut, 2, line.length); + _rawOut.write(line); + _rawOut.flush(); + + if (_log.shouldLog(Log.DEBUG)) + _log.debug("SendProtocol[X]: tag: " + + (_connectionTag != null ? Base64.encode(_connectionTag.getData()) : "none") + + " socket: " + _socket); + + return true; + } catch (IOException ioe) { + fail("Error sending our handshake to " + + _target.getIdentity().calculateHash().toBase64().substring(0,6) + + ": " + ioe.getMessage(), ioe); + return false; + } catch (DataFormatException dfe) { + fail("Error sending our handshake to " + + _target.getIdentity().calculateHash().toBase64().substring(0,6) + + ": " + dfe.getMessage(), dfe); + return false; + } + } + + /** + * Receive #bytesFollowing + versionOk + #bytesIP + IP + tagOk? + nonce + properties + * + */ + private boolean receiveAgreedProtocol() { + try { + // #bytesFollowing + versionOk + #bytesIP + IP + tagOk? + nonce + properties + int numBytes = (int)DataHelper.readLong(_rawIn, 2); + // 0xFF is a reserved value + if ( (numBytes <= 0) || (numBytes >= 0xFF) ) + throw new IOException("Invalid number of bytes in response"); + + byte line[] = new byte[numBytes]; + int read = DataHelper.read(_rawIn, line); + if (read != numBytes) { + fail("Handshake too short with " + + _target.getIdentity().calculateHash().toBase64().substring(0,6)); + return false; + } + + if (_log.shouldLog(Log.DEBUG)) + _log.debug("ReadProtocol1[X]: " + + "\nLine: " + Base64.encode(line)); + + ByteArrayInputStream bais = new ByteArrayInputStream(line); + + int version = (int)DataHelper.readLong(bais, 1); + for (int i = 0; i < TCPTransport.SUPPORTED_PROTOCOLS.length; i++) { + if (version == TCPTransport.SUPPORTED_PROTOCOLS[i]) { + _agreedProtocol = version; + break; + } + } + if (_agreedProtocol == -1) { + fail("No valid protocol versions to contact " + + _target.getIdentity().calculateHash().toBase64().substring(0,6)); + return false; + } + + int bytesInIP = (int)DataHelper.readLong(bais, 1); + byte ip[] = new byte[bytesInIP]; + DataHelper.read(bais, ip); // ignore return value, this is an array + _localIP = new String(ip); + // if we don't already know our IP address, this may cause us + // to fire up a socket listener, so may take a few seconds. + _transport.ourAddressReceived(_localIP); + + int tagOk = (int)DataHelper.readLong(bais, 1); + if ( (tagOk == 0x01) && (_connectionTag != null) ) { + // tag is ok + } else { + _connectionTag = null; + _key = null; + } + + byte nonce[] = new byte[4]; + read = DataHelper.read(bais, nonce); + if (read != 4) { + fail("No nonce specified by " + + _target.getIdentity().calculateHash().toBase64().substring(0,6)); + return false; + } + _nonce = new ByteArray(nonce); + + Properties opts = DataHelper.readProperties(bais); + + if (_log.shouldLog(Log.DEBUG)) + _log.debug("ReadProtocol[X]: agreed=" + _agreedProtocol + " nonce: " + + Base64.encode(nonce) + " tag: " + + (_connectionTag != null ? Base64.encode(_connectionTag.getData()) : "none") + + " props: " + opts + + " socket: " + _socket + + "\nLine: " + Base64.encode(line)); + + // we dont care about any of the properties, so we can just + // ignore it, and we're done with this step + return true; + } catch (IOException ioe) { + fail("Error reading the handshake from " + + _target.getIdentity().calculateHash().toBase64().substring(0,6) + + ": " + ioe.getMessage(), ioe); + return false; + } catch (DataFormatException dfe) { + fail("Error reading the handshake from " + + _target.getIdentity().calculateHash().toBase64().substring(0,6) + + ": " + dfe.getMessage(), dfe); + return false; + } + + } + + /** Set the next tag to H(E(nonce + tag, sessionKey)) */ + private void updateNextTagExisting() { + byte pre[] = new byte[48]; + byte encr[] = _context.AESEngine().encrypt(pre, _key, _iv); + Hash h = _context.sha().calculateHash(encr); + _nextConnectionTag = new ByteArray(h.getData()); + } + + /** + * We have a valid tag, so use it to do the handshaking. On error, fail() + * appropriately. + * + * @return true if the connection went ok, or false if it failed. + */ + private boolean connectExistingSession() { + // iv to the SHA256 of the tag appended by the nonce. + byte data[] = new byte[36]; + System.arraycopy(_connectionTag.getData(), 0, data, 0, 32); + System.arraycopy(_nonce.getData(), 0, data, 32, 4); + Hash h = _context.sha().calculateHash(data); + _iv = new byte[16]; + System.arraycopy(h.getData(), 0, _iv, 0, 16); + + updateNextTagExisting(); + + _rawOut = new AESOutputStream(_context, _rawOut, _key, _iv); + _rawIn = new AESInputStream(_context, _rawIn, _key, _iv); + + // send: H(nonce) + try { + h = _context.sha().calculateHash(_nonce.getData()); + h.writeBytes(_rawOut); + _rawOut.flush(); + } catch (IOException ioe) { + fail("Error writing the encrypted nonce to " + + _target.getIdentity().calculateHash().toBase64().substring(0,6) + + ": " + ioe.getMessage()); + return false; + } catch (DataFormatException dfe) { + fail("Error writing the encrypted nonce to " + + _target.getIdentity().calculateHash().toBase64().substring(0,6) + + ": " + dfe.getMessage()); + return false; + } + + // read: H(tag) + try { + Hash readHash = new Hash(); + readHash.readBytes(_rawIn); + + Hash expectedHash = _context.sha().calculateHash(_connectionTag.getData()); + + if (!readHash.equals(expectedHash)) { + fail("Key verification failed with " + + _target.getIdentity().calculateHash().toBase64().substring(0,6)); + return false; + } + } catch (IOException ioe) { + fail("Error reading the initial key verification from " + + _target.getIdentity().calculateHash().toBase64().substring(0,6) + + ": " + ioe.getMessage()); + return false; + } catch (DataFormatException dfe) { + fail("Error reading the initial key verification from " + + _target.getIdentity().calculateHash().toBase64().substring(0,6) + + ": " + dfe.getMessage()); + return false; + } + + // send: routerInfo + currentTime + H(routerInfo + currentTime + nonce + tag) + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(512); + _context.router().getRouterInfo().writeBytes(baos); + DataHelper.writeDate(baos, new Date(_context.clock().now())); + + _rawOut.write(baos.toByteArray()); + + baos.write(_nonce.getData()); + baos.write(_connectionTag.getData()); + Hash verification = _context.sha().calculateHash(baos.toByteArray()); + + verification.writeBytes(_rawOut); + _rawOut.flush(); + } catch (IOException ioe) { + fail("Error writing the verified info to " + + _target.getIdentity().calculateHash().toBase64().substring(0,6) + + ": " + ioe.getMessage()); + return false; + } catch (DataFormatException dfe) { + fail("Error writing the verified info to " + + _target.getIdentity().calculateHash().toBase64().substring(0,6) + + ": " + dfe.getMessage()); + return false; + } + + // read: routerInfo + status + properties + // + H(routerInfo + status + properties + nonce + tag) + try { + RouterInfo peer = new RouterInfo(); + peer.readBytes(_rawIn); + int status = (int)_rawIn.read() & 0xFF; + boolean ok = validateStatus(status); + if (!ok) return false; + + Properties props = DataHelper.readProperties(_rawIn); + // ignore these now + + Hash readHash = new Hash(); + readHash.readBytes(_rawIn); + + // H(routerInfo + status + properties + nonce + tag) + ByteArrayOutputStream baos = new ByteArrayOutputStream(512); + peer.writeBytes(baos); + baos.write(status); + DataHelper.writeProperties(baos, props); + baos.write(_nonce.getData()); + baos.write(_connectionTag.getData()); + Hash expectedHash = _context.sha().calculateHash(baos.toByteArray()); + + if (!expectedHash.equals(readHash)) { + fail("Error verifying info from " + + _target.getIdentity().calculateHash().toBase64().substring(0,6) + + " (claiming to be " + + peer.getIdentity().calculateHash().toBase64().substring(0,6) + + ")"); + return false; + } + + _actualPeer = peer; + return true; + } catch (IOException ioe) { + fail("Error reading the verified info from " + + _target.getIdentity().calculateHash().toBase64().substring(0,6) + + ": " + ioe.getMessage()); + return false; + } catch (DataFormatException dfe) { + fail("Error reading the verified info from " + + _target.getIdentity().calculateHash().toBase64().substring(0,6) + + ": " + dfe.getMessage()); + return false; + } + } + + /** + * We do not have a valid tag, so exchange a new one and then do the + * handshaking. On error, fail() appropriately. + * + * @return true if the connection went ok, or false if it failed. + */ + private boolean connectNewSession() { + DHSessionKeyBuilder builder = null; + try { + builder = DHSessionKeyBuilder.exchangeKeys(_rawIn, _rawOut); + } catch (IOException ioe) { + fail("Error exchanging keys with " + + _target.getIdentity().calculateHash().toBase64().substring(0,6)); + return false; + } + + // load up the key initialize the encrypted streams + _key = builder.getSessionKey(); + byte extra[] = builder.getExtraBytes().getData(); + _iv = new byte[16]; + System.arraycopy(extra, 0, _iv, 0, 16); + byte nextTag[] = new byte[32]; + System.arraycopy(extra, 16, nextTag, 0, 32); + _nextConnectionTag = new ByteArray(nextTag); + + if (_log.shouldLog(Log.DEBUG)) + _log.debug("\nNew session[X]: key=" + _key.toBase64() + " iv=" + + Base64.encode(_iv) + " nonce=" + Base64.encode(_nonce.getData()) + + " socket: " + _socket); + + _rawOut = new AESOutputStream(_context, _rawOut, _key, _iv); + _rawIn = new AESInputStream(_context, _rawIn, _key, _iv); + + // send: H(nonce) + try { + Hash h = _context.sha().calculateHash(_nonce.getData()); + h.writeBytes(_rawOut); + _rawOut.flush(); + } catch (IOException ioe) { + fail("Error writing the verification to " + + _target.getIdentity().calculateHash().toBase64().substring(0,6), ioe); + return false; + } catch (DataFormatException dfe) { + fail("Error writing the verification to " + + _target.getIdentity().calculateHash().toBase64().substring(0,6), dfe); + return false; + } + + // read: H(nextTag) + try { + byte val[] = new byte[32]; + int read = DataHelper.read(_rawIn, val); + if (read != 32) { + fail("Not enough data to read the verification from " + + _target.getIdentity().calculateHash().toBase64().substring(0,6)); + return false; + } + + Hash expected = _context.sha().calculateHash(_nextConnectionTag.getData()); + if (!DataHelper.eq(expected.getData(), val)) { + fail("Verification failed from " + + _target.getIdentity().calculateHash().toBase64().substring(0,6)); + return false; + } + } catch (IOException ioe) { + fail("Error reading the verification from " + + _target.getIdentity().calculateHash().toBase64().substring(0,6), ioe); + return false; + } + + // send: routerInfo + currentTime + // + S(routerInfo + currentTime + nonce + nextTag, routerIdent.signingKey) + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(512); + _context.router().getRouterInfo().writeBytes(baos); + DataHelper.writeDate(baos, new Date(_context.clock().now())); + + _rawOut.write(baos.toByteArray()); + + baos.write(_nonce.getData()); + baos.write(_nextConnectionTag.getData()); + Signature sig = _context.dsa().sign(baos.toByteArray(), + _context.keyManager().getSigningPrivateKey()); + + sig.writeBytes(_rawOut); + _rawOut.flush(); + } catch (IOException ioe) { + fail("Error sending the info to " + + _target.getIdentity().calculateHash().toBase64().substring(0,6)); + return false; + } catch (DataFormatException dfe) { + fail("Error sending the info to " + + _target.getIdentity().calculateHash().toBase64().substring(0,6)); + return false; + } + + // read: routerInfo + status + properties + // + S(routerInfo + status + properties + nonce + nextTag, routerIdent.signingKey) + try { + RouterInfo peer = new RouterInfo(); + peer.readBytes(_rawIn); + int status = (int)_rawIn.read() & 0xFF; + boolean ok = validateStatus(status); + if (!ok) return false; + + Properties props = DataHelper.readProperties(_rawIn); + // ignore these now + + Signature sig = new Signature(); + sig.readBytes(_rawIn); + + // S(routerInfo + status + properties + nonce + nextTag, routerIdent.signingKey) + ByteArrayOutputStream baos = new ByteArrayOutputStream(512); + peer.writeBytes(baos); + baos.write(status); + DataHelper.writeProperties(baos, props); + baos.write(_nonce.getData()); + baos.write(_nextConnectionTag.getData()); + ok = _context.dsa().verifySignature(sig, baos.toByteArray(), + peer.getIdentity().getSigningPublicKey()); + + if (!ok) { + fail("Error verifying info from " + + _target.getIdentity().calculateHash().toBase64().substring(0,6) + + " (claiming to be " + + peer.getIdentity().calculateHash().toBase64().substring(0,6) + + ")"); + return false; + } + + _actualPeer = peer; + return true; + } catch (IOException ioe) { + fail("Error reading the verified info from " + + _target.getIdentity().calculateHash().toBase64().substring(0,6) + + ": " + ioe.getMessage()); + return false; + } catch (DataFormatException dfe) { + fail("Error reading the verified info from " + + _target.getIdentity().calculateHash().toBase64().substring(0,6) + + ": " + dfe.getMessage()); + return false; + } + } + + /** + * Is the given status value ok for an existing session? + * + * @return true if ok, false if fail()ed + */ + private boolean validateStatus(int status) { + switch (status) { + case -1: + fail("Error reading the status from " + + _target.getIdentity().calculateHash().toBase64().substring(0,6)); + return false; + case 0: // ok + return true; + case 1: // not reachable + fail("According to " + + _target.getIdentity().calculateHash().toBase64().substring(0,6) + + ", we are not reachable on " + _localIP); + return false; + case 2: // clock skew + fail("According to " + + _target.getIdentity().calculateHash().toBase64().substring(0,6) + + ", our clock is off"); + return false; + case 3: // signature failure (only for new sessions) + fail("Signature failure talking to " + + _target.getIdentity().calculateHash().toBase64().substring(0,6)); + return false; + default: // unknown error + fail("Unknown error [" + status + "] connecting to " + + _target.getIdentity().calculateHash().toBase64().substring(0,6)); + return false; + } + } + + /** + * Finish up the establishment (wrapping the streams, storing the netDb, + * persisting the connection tags, etc) + * + */ + private void establishComplete() { + // todo: add bw limiter + _connectionIn = _rawIn; + _connectionOut = _rawOut; + + Hash peer = _actualPeer.getIdentity().getHash(); + _context.netDb().store(peer, _actualPeer); + _transport.getTagManager().replaceTag(peer, _nextConnectionTag, _key); + } + + /** + * Build a socket to the peer, and populate _socket, _rawIn, and _rawOut + * accordingly. On error or timeout, close and null them all and + * set _error. + * + */ + private void createSocket() { + CreateSocketRunner r = new CreateSocketRunner(); + I2PThread t = new I2PThread(r); + t.start(); + try { t.join(CONNECTION_TIMEOUT); } catch (InterruptedException ie) {} + if (!r.getCreated()) { + fail("Unable to establish a socket in time to " + + _target.getIdentity().calculateHash().toBase64().substring(0,6)); + } + } + + /** Brief description of why the connection failed (or null if it succeeded) */ + public String getError() { return _error; } + + /** + * Kill the builder, closing all sockets and streams, setting everything + * back to failure states, and setting the given error. + * + */ + private void fail(String error) { + fail(error, null); + } + private void fail(String error, Exception e) { + if (_error == null) // only grab the first error + _error = error; + + if (_rawIn != null) try { _rawIn.close(); } catch (IOException ioe) {} + if (_rawOut != null) try { _rawOut.close(); } catch (IOException ioe) {} + if (_socket != null) try { _socket.close(); } catch (IOException ioe) {} + + _socket = null; + _rawIn = null; + _rawOut = null; + _agreedProtocol = -1; + _nonce = null; + _connectionTag = null; + _actualPeer = null; + + if (_log.shouldLog(Log.WARN)) + _log.warn(error, e); + } + + /** + * Lookup and establish a connection to the peer, exposing getCreate() == true + * once we are done. This allows for asynchronous timeouts without depending + * upon the interruptability of the socket (since it isn't open yet). + * + */ + private class CreateSocketRunner implements Runnable { + private boolean _created; + public CreateSocketRunner() { + _created = false; + } + + public boolean getCreated() { return _created; } + + public void run() { + RouterAddress addr = _target.getTargetAddress(_transport.getStyle()); + if (addr == null) { + fail("Peer " + + _target.getIdentity().calculateHash().toBase64().substring(0,6) + + " has no TCP addresses"); + return; + } + TCPAddress tcpAddr = new TCPAddress(addr); + if (tcpAddr.getPort() <= 0) { + fail("Peer " + + _target.getIdentity().calculateHash().toBase64().substring(0,6) + + " has an invalid TCP address"); + return; + } + + try { + _socket = new Socket(tcpAddr.getAddress(), tcpAddr.getPort()); + _rawIn = _socket.getInputStream(); + _rawOut = _socket.getOutputStream(); + _error = null; + _remoteAddress = tcpAddr; + _created = true; + } catch (IOException ioe) { + fail("Error contacting " + + _target.getIdentity().calculateHash().toBase64().substring(0,6) + + " on " + tcpAddr.toString() + ": " + ioe.getMessage()); + return; + } + } + } + + /** + * In addition to the socket creation timeout, we have a timed event for + * the overall connection establishment, killing everything if we haven't + * completed a connection yet. + * + */ + private class DieIfTooSlow implements SimpleTimer.TimedEvent { + public void timeReached() { + if ( (_actualPeer == null) && (_error == null) ) { + fail("Took too long to connect with " + + _target.getIdentity().calculateHash().toBase64().substring(0,6)); + } + } + } +} diff --git a/router/java/src/net/i2p/router/transport/tcp/ConnectionHandler.java b/router/java/src/net/i2p/router/transport/tcp/ConnectionHandler.java new file mode 100644 index 0000000000..65a14ee965 --- /dev/null +++ b/router/java/src/net/i2p/router/transport/tcp/ConnectionHandler.java @@ -0,0 +1,806 @@ +package net.i2p.router.transport.tcp; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.Socket; + +import java.text.SimpleDateFormat; + +import java.util.Date; +import java.util.Properties; + +import net.i2p.crypto.AESInputStream; +import net.i2p.crypto.AESOutputStream; +import net.i2p.crypto.DHSessionKeyBuilder; +import net.i2p.data.Base64; +import net.i2p.data.ByteArray; +import net.i2p.data.DataHelper; +import net.i2p.data.DataFormatException; +import net.i2p.data.Hash; +import net.i2p.data.SessionKey; +import net.i2p.data.Signature; +import net.i2p.data.RouterInfo; +import net.i2p.router.RouterContext; +import net.i2p.router.Router; +import net.i2p.util.Log; +import net.i2p.util.NativeBigInteger; + +/** + * Class responsible for all of the handshaking necessary to turn a socket into + * a TCPConnection. + * + */ +public class ConnectionHandler { + private RouterContext _context; + private Log _log; + private TCPTransport _transport; + /** who we're actually talking with */ + private RouterInfo _actualPeer; + /** raw socket to the peer */ + private Socket _socket; + /** raw stream to read from the peer */ + private InputStream _rawIn; + /** raw stream to write to the peer */ + private OutputStream _rawOut; + /** secure stream to read from the peer */ + private InputStream _connectionIn; + /** secure stream to write to the peer */ + private OutputStream _connectionOut; + /** protocol version agreed to, or -1 */ + private int _agreedProtocol; + /** + * Contains a message describing why the connection failed (or null if it + * succeeded). This should include a timestamp of some sort. + */ + private String _error; + /** + * If we're handing a reachability test, set this to true once + * we're done + */ + private boolean _testComplete; + /** IP address of the peer who contacted us */ + private String _from; + /** Where we verified their address */ + private TCPAddress _remoteAddress; + /** connection tag to identify ourselves, or null if no known tag is available */ + private ByteArray _connectionTag; + /** connection tag to identify ourselves next time */ + private ByteArray _nextConnectionTag; + /** nonce the peer gave us */ + private ByteArray _nonce; + /** key that we will be encrypting comm with */ + private SessionKey _key; + /** initialization vector for the encryption */ + private byte[] _iv; + + public ConnectionHandler(RouterContext ctx, TCPTransport transport, Socket socket) { + _context = ctx; + _log = ctx.logManager().getLog(ConnectionHandler.class); + _transport = transport; + _socket = socket; + _error = null; + _agreedProtocol = -1; + InetAddress addr = _socket.getInetAddress(); + if (addr != null) { + _from = addr.getHostAddress(); + } + } + + /** + * Blocking call to establish a TCP connection over the current socket. + * At this point, no data whatsoever need to have been transmitted over the + * socket - the builder is responsible for all aspects of the handshaking. + * + * @return fully established but not yet running connection, or null on error + */ + public TCPConnection receiveConnection() { + try { + _rawIn = _socket.getInputStream(); + _rawOut = _socket.getOutputStream(); + } catch (IOException ioe) { + fail("Error accessing the socket streams from " + _from, ioe); + return null; + } + + negotiateProtocol(); + + if ( (_agreedProtocol < 0) || (_error != null) ) + return null; + + boolean ok = false; + if ( (_connectionTag != null) && (_key != null) ) + ok = connectExistingSession(); + else + ok = connectNewSession(); + + if (_log.shouldLog(Log.DEBUG)) + _log.debug("connection ok? " + ok + " error: " + _error); + + if (ok && (_error == null) ) { + establishComplete(); + + if (_log.shouldLog(Log.INFO)) + _log.info("Establishment ok... building the con"); + + TCPConnection con = new TCPConnection(_context); + con.setInputStream(_connectionIn); + con.setOutputStream(_connectionOut); + con.setSocket(_socket); + con.setRemoteRouterIdentity(_actualPeer.getIdentity()); + con.setRemoteAddress(_remoteAddress); + if (_error == null) { + if (_log.shouldLog(Log.INFO)) + _log.info("Establishment successful! returning the con"); + return con; + } else { + if (_log.shouldLog(Log.INFO)) + _log.info("Establishment ok but we failed?! error = " + _error); + return null; + } + } else { + return null; + } + } + + /** + * Agree on what protocol to communicate with, and set _agreedProtocol + * accordingly. If no common protocols are available, disconnect, set + * _agreedProtocol to -1, and update the _error accordingly. + */ + private void negotiateProtocol() { + boolean ok = readPreferredProtocol(); + if (!ok) return; + sendAgreedProtocol(); + } + + /** + * Receive #bytesFollowing + #versions + v1 [+ v2 [etc]] + tag? + tagData + properties + * + */ + private boolean readPreferredProtocol() { + try { + int numBytes = (int)DataHelper.readLong(_rawIn, 2); + if (numBytes <= 0) + throw new IOException("Invalid number of bytes in connection"); + // 0xFF is a reserved value identifying the connection as a reachability test + if (numBytes == 0xFFFF) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("ReadProtocol[Y]: test called, handle it"); + handleTest(); + return false; + } else { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("ReadProtocol[Y]: not a test (line len=" + numBytes + ")"); + } + + byte line[] = new byte[numBytes]; + int read = DataHelper.read(_rawIn, line); + if (read != numBytes) { + fail("Handshake too short from " + _from); + return false; + } + + ByteArrayInputStream bais = new ByteArrayInputStream(line); + + int numVersions = (int)DataHelper.readLong(bais, 1); + if ( (numVersions <= 0) || (numVersions > 0x8) ) { + fail("Invalid number of protocol versions from " + _from); + return false; + } + int versions[] = new int[numVersions]; + for (int i = 0; i < numVersions; i++) + versions[i] = (int)DataHelper.readLong(bais, 1); + + for (int i = 0; i < numVersions && _agreedProtocol == -1; i++) { + for (int j = 0; j < TCPTransport.SUPPORTED_PROTOCOLS.length; j++) { + if (versions[i] == TCPTransport.SUPPORTED_PROTOCOLS[j]) { + _agreedProtocol = versions[i]; + break; + } + } + } + + int tag = (int)DataHelper.readLong(bais, 1); + if (tag == 0x1) { + byte tagData[] = new byte[32]; + read = DataHelper.read(bais, tagData); + if (read != 32) + throw new IOException("Not enough data for the tag"); + _connectionTag = new ByteArray(tagData); + _key = _transport.getTagManager().getKey(_connectionTag); + if (_key == null) + _connectionTag = null; + } + + Properties opts = DataHelper.readProperties(bais); + // ignore them + + if (_log.shouldLog(Log.DEBUG)) + _log.debug("ReadProtocol[Y]: agreed=" + _agreedProtocol + " tag: " + + (_connectionTag != null ? Base64.encode(_connectionTag.getData()) : "none")); + return true; + } catch (IOException ioe) { + fail("Error reading the handshake from " + _from + + ": " + ioe.getMessage(), ioe); + return false; + } catch (DataFormatException dfe) { + fail("Error reading the handshake from " + _from + + ": " + dfe.getMessage(), dfe); + return false; + } + } + + /** + * Send #bytesFollowing + versionOk + #bytesIP + IP + tagOk? + nonce + properties + */ + private void sendAgreedProtocol() { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(128); + if (_agreedProtocol <= 0) + baos.write(0x0); + else + baos.write(_agreedProtocol); + + byte ip[] = _from.getBytes(); + baos.write(ip.length); + baos.write(ip); + + if (_key != null) + baos.write(0x1); + else + baos.write(0x0); + + byte nonce[] = new byte[4]; + _context.random().nextBytes(nonce); + _nonce = new ByteArray(nonce); + baos.write(nonce); + + Properties opts = new Properties(); + opts.setProperty("foo", "bar"); + DataHelper.writeProperties(baos, opts); // no options atm + + byte line[] = baos.toByteArray(); + DataHelper.writeLong(_rawOut, 2, line.length); + _rawOut.write(line); + _rawOut.flush(); + + + if (_log.shouldLog(Log.DEBUG)) + _log.debug("SendProtocol[Y]: agreed=" + _agreedProtocol + " IP: " + _from + + " nonce: " + Base64.encode(nonce) + " tag: " + + (_connectionTag != null ? Base64.encode(_connectionTag.getData()) : " none") + + " props: " + opts + + "\nLine: " + Base64.encode(line)); + + if (_agreedProtocol <= 0) { + fail("Connection from " + _from + " rejected, since no compatible protocols were found"); + return; + } + } catch (IOException ioe) { + fail("Error writing the handshake to " + _from + + ": " + ioe.getMessage(), ioe); + return; + } catch (DataFormatException dfe) { + fail("Error writing the handshake to " + _from + + ": " + dfe.getMessage(), dfe); + return; + } + } + + /** Set the next tag to H(E(nonce + tag, sessionKey)) */ + private void updateNextTagExisting() { + byte pre[] = new byte[48]; + byte encr[] = _context.AESEngine().encrypt(pre, _key, _iv); + Hash h = _context.sha().calculateHash(encr); + _nextConnectionTag = new ByteArray(h.getData()); + } + + /** + * We have a valid tag, so use it to do the handshaking. On error, fail() + * appropriately. + * + * @return true if the connection went ok, or false if it failed. + */ + private boolean connectExistingSession() { + // iv to the SHA256 of the tag appended by the nonce. + byte data[] = new byte[36]; + System.arraycopy(_connectionTag.getData(), 0, data, 0, 32); + System.arraycopy(_nonce.getData(), 0, data, 32, 4); + Hash h = _context.sha().calculateHash(data); + _iv = new byte[16]; + System.arraycopy(h.getData(), 0, _iv, 0, 16); + + updateNextTagExisting(); + + _rawOut = new AESOutputStream(_context, _rawOut, _key, _iv); + _rawIn = new AESInputStream(_context, _rawIn, _key, _iv); + + // read: H(nonce) + try { + Hash readHash = new Hash(); + readHash.readBytes(_rawIn); + + Hash expected = _context.sha().calculateHash(_nonce.getData()); + if (!expected.equals(readHash)) { + fail("Verification hash failed from " + _from); + return false; + } + } catch (IOException ioe) { + fail("Error reading the encrypted nonce from " + _from + + ": " + ioe.getMessage(), ioe); + return false; + } catch (DataFormatException dfe) { + fail("Error reading the encrypted nonce from " + _from + + ": " + dfe.getMessage(), dfe); + return false; + } + + // send: H(tag) + try { + Hash tagHash = _context.sha().calculateHash(_connectionTag.getData()); + tagHash.writeBytes(_rawOut); + _rawOut.flush(); + } catch (IOException ioe) { + fail("Error writing the encrypted tag to " + _from + + ": " + ioe.getMessage(), ioe); + return false; + } catch (DataFormatException dfe) { + fail("Error writing the encrypted tag to " + _from + + ": " + dfe.getMessage(), dfe); + return false; + } + + long clockSkew = 0; + + // read: routerInfo + currentTime + H(routerInfo + currentTime + nonce + tag) + try { + RouterInfo peer = new RouterInfo(); + peer.readBytes(_rawIn); + Date now = DataHelper.readDate(_rawIn); + Hash readHash = new Hash(); + readHash.readBytes(_rawIn); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(512); + peer.writeBytes(baos); + DataHelper.writeDate(baos, now); + baos.write(_nonce.getData()); + baos.write(_connectionTag.getData()); + Hash expectedHash = _context.sha().calculateHash(baos.toByteArray()); + + if (!expectedHash.equals(readHash)) { + fail("Invalid hash read for the info from " + _from); + return false; + } + + _actualPeer = peer; + clockSkew = _context.clock().now() - now.getTime(); + } catch (IOException ioe) { + fail("Error reading the peer info from " + _from + + ": " + ioe.getMessage(), ioe); + return false; + } catch (DataFormatException dfe) { + fail("Error reading the peer info from " + _from + + ": " + dfe.getMessage(), dfe); + return false; + } + + // verify routerInfo + boolean reachable = verifyReachability(); + + // send routerInfo + status + properties + H(routerInfo + status + properties + nonce + tag) + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(512); + _context.router().getRouterInfo().writeBytes(baos); + + Properties props = new Properties(); + + int status = -1; + if (!reachable) { + status = 1; + } else if ( (clockSkew > Router.CLOCK_FUDGE_FACTOR) + || (clockSkew < 0 - Router.CLOCK_FUDGE_FACTOR) ) { + status = 2; + SimpleDateFormat fmt = new SimpleDateFormat("yyyyMMddhhmmssSSS"); + props.setProperty("SKEW", fmt.format(new Date(_context.clock().now()))); + } else { + status = 0; + } + + baos.write(status); + + DataHelper.writeProperties(baos, props); + byte beginning[] = baos.toByteArray(); + + baos.write(_nonce.getData()); + baos.write(_connectionTag.getData()); + + Hash verification = _context.sha().calculateHash(baos.toByteArray()); + + _rawOut.write(beginning); + verification.writeBytes(_rawOut); + _rawOut.flush(); + + return handleStatus(status, clockSkew); + } catch (IOException ioe) { + fail("Error writing the peer info to " + _from + + ": " + ioe.getMessage(), ioe); + return false; + } catch (DataFormatException dfe) { + fail("Error writing the peer info to " + _from + + ": " + dfe.getMessage(), dfe); + return false; + } + } + + /** + * + * We do not have a valid tag, so DH then do the handshaking. On error, + * fail() appropriately. + * + * @return true if the connection went ok, or false if it failed. + */ + private boolean connectNewSession() { + DHSessionKeyBuilder builder = null; + try { + builder = DHSessionKeyBuilder.exchangeKeys(_rawIn, _rawOut); + } catch (IOException ioe) { + fail("Error exchanging keys with " + _from); + return false; + } + + // load up the key initialize the encrypted streams + _key = builder.getSessionKey(); + byte extra[] = builder.getExtraBytes().getData(); + _iv = new byte[16]; + System.arraycopy(extra, 0, _iv, 0, 16); + byte nextTag[] = new byte[32]; + System.arraycopy(extra, 16, nextTag, 0, 32); + _nextConnectionTag = new ByteArray(nextTag); + + if (_log.shouldLog(Log.DEBUG)) + _log.debug("\nNew session[Y]: key=" + _key.toBase64() + " iv=" + + Base64.encode(_iv) + " nonce=" + Base64.encode(_nonce.getData()) + + " socket: " + _socket); + + _rawOut = new AESOutputStream(_context, _rawOut, _key, _iv); + _rawIn = new AESInputStream(_context, _rawIn, _key, _iv); + + // read: H(nonce) + try { + Hash h = new Hash(); + h.readBytes(_rawIn); + + Hash expected = _context.sha().calculateHash(_nonce.getData()); + if (!expected.equals(h)) { + fail("Hash after negotiation from " + _from + " does not match"); + return false; + } + } catch (IOException ioe) { + fail("Error reading the hash from " + _from + + ": " + ioe.getMessage(), ioe); + return false; + } catch (DataFormatException dfe) { + fail("Error reading the hash from " + _from + + ": " + dfe.getMessage(), dfe); + return false; + } + + // send: H(nextTag) + try { + Hash h = _context.sha().calculateHash(_nextConnectionTag.getData()); + h.writeBytes(_rawOut); + _rawOut.flush(); + } catch (IOException ioe) { + fail("Error writing the hash to " + _from + + ": " + ioe.getMessage(), ioe); + return false; + } catch (DataFormatException dfe) { + fail("Error writing the hash to " + _from + + ": " + dfe.getMessage(), dfe); + return false; + } + + long clockSkew = 0; + boolean sigOk = false; + + // read: routerInfo + currentTime + // + S(routerInfo + currentTime + nonce + nextTag, routerIdent.signingKey) + try { + RouterInfo info = new RouterInfo(); + info.readBytes(_rawIn); + Date now = DataHelper.readDate(_rawIn); + Signature sig = new Signature(); + sig.readBytes(_rawIn); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(512); + info.writeBytes(baos); + DataHelper.writeDate(baos, now); + baos.write(_nonce.getData()); + baos.write(_nextConnectionTag.getData()); + + sigOk = _context.dsa().verifySignature(sig, baos.toByteArray(), + info.getIdentity().getSigningPublicKey()); + + clockSkew = _context.clock().now() - now.getTime(); + _actualPeer = info; + } catch (IOException ioe) { + fail("Error reading the info from " + _from + + ": " + ioe.getMessage(), ioe); + return false; + } catch (DataFormatException dfe) { + fail("Error reading the info from " + _from + + ": " + dfe.getMessage(), dfe); + return false; + } + + // verify routerInfo + boolean reachable = verifyReachability(); + + // send: routerInfo + status + properties + // + S(routerInfo + status + properties + nonce + nextTag, routerIdent.signingKey) + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(512); + _context.router().getRouterInfo().writeBytes(baos); + + Properties props = new Properties(); + + int status = -1; + if (!reachable) { + status = 1; + } else if ( (clockSkew > Router.CLOCK_FUDGE_FACTOR) + || (clockSkew < 0 - Router.CLOCK_FUDGE_FACTOR) ) { + status = 2; + SimpleDateFormat fmt = new SimpleDateFormat("yyyyMMddhhmmssSSS"); + props.setProperty("SKEW", fmt.format(new Date(_context.clock().now()))); + } else if (!sigOk) { + status = 3; + } else { + status = 0; + } + + baos.write(status); + + DataHelper.writeProperties(baos, props); + byte beginning[] = baos.toByteArray(); + + baos.write(_nonce.getData()); + baos.write(_nextConnectionTag.getData()); + + Signature sig = _context.dsa().sign(baos.toByteArray(), + _context.keyManager().getSigningPrivateKey()); + + _rawOut.write(beginning); + sig.writeBytes(_rawOut); + _rawOut.flush(); + + return handleStatus(status, clockSkew); + } catch (IOException ioe) { + fail("Error writing the info to " + _from + + ": " + ioe.getMessage(), ioe); + return false; + } catch (DataFormatException dfe) { + fail("Error writing the info to " + _from + + ": " + dfe.getMessage(), dfe); + return false; + } + } + + /** + * Act according to the status code, failing as necessary and returning + * whether we should continue going or not. + * + * @return true if we should keep going. + */ + private boolean handleStatus(int status, long clockSkew) { + switch (status) { + case 0: // ok + return true; + case 1: + fail("Peer " + _actualPeer.getIdentity().calculateHash().toBase64().substring(0,6) + + " at " + _from + " is unreachable"); + return false; + case 2: + fail("Peer " + _actualPeer.getIdentity().calculateHash().toBase64().substring(0,6) + + " was skewed by " + DataHelper.formatDuration(clockSkew)); + return false; + case 3: + fail("Forged signature on " + _from + " pretending to be " + + _actualPeer.getIdentity().calculateHash().toBase64().substring(0,6)); + return false; + default: + fail("Unknown error verifying " + + _actualPeer.getIdentity().calculateHash().toBase64().substring(0,6) + + ": " + status); + return false; + } + } + + /** + * Can the peer be contacted on their public addresses? If so, + * be sure to set _remoteAddress. We can do this without branching onto + * another thread because we already have a timer killing this handler if + * it takes too long + */ + private boolean verifyReachability() { + if (_actualPeer == null) return false; + _remoteAddress = new TCPAddress(_actualPeer.getTargetAddress(TCPTransport.STYLE)); + //if (true) return true; + Socket s = null; + try { + s = new Socket(_remoteAddress.getAddress(), _remoteAddress.getPort()); + OutputStream out = s.getOutputStream(); + InputStream in = s.getInputStream(); + + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Beginning verification of reachability"); + + // send: 0xFFFF + #versions + v1 [+ v2 [etc]] + properties + out.write(0xFF); + out.write(0xFF); + out.write(TCPTransport.SUPPORTED_PROTOCOLS.length); + for (int i = 0; i < TCPTransport.SUPPORTED_PROTOCOLS.length; i++) + out.write(TCPTransport.SUPPORTED_PROTOCOLS[i]); + DataHelper.writeProperties(out, null); + out.flush(); + + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Verification of reachability request sent"); + + // read: 0xFFFF + versionOk + #bytesIP + IP + currentTime + properties + int ok = in.read(); + if (ok != 0xFF) + throw new IOException("Unable to verify the peer - invalid response"); + ok = in.read(); + if (ok != 0xFF) + throw new IOException("Unable to verify the peer - invalid response"); + int version = in.read(); + if (version == -1) + throw new IOException("Unable to verify the peer - invalid version"); + if (version == 0) + throw new IOException("Unable to verify the peer - no matching version"); + int numBytes = in.read(); + if ( (numBytes == -1) || (numBytes > 32) ) + throw new IOException("Unable to verify the peer - invalid num bytes"); + byte ip[] = new byte[numBytes]; + int read = DataHelper.read(in, ip); + if (read != numBytes) + throw new IOException("Unable to verify the peer - invalid num bytes"); + Date now = DataHelper.readDate(in); + Properties opts = DataHelper.readProperties(in); + + return true; + } catch (IOException ioe) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Error verifying " + + _actualPeer.getIdentity().calculateHash().toBase64().substring(0,6) + + "at " + _remoteAddress, ioe); + return false; + } catch (DataFormatException dfe) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Error verifying " + + _actualPeer.getIdentity().calculateHash().toBase64().substring(0,6) + + "at " + _remoteAddress, dfe); + return false; + } + } + + /** + * The peer contacting us is just testing us. Verify that we are reachable + * by following the protocol, then close the socket. This is called only + * after reading the initial 0xFF. + * + */ + private void handleTest() { + try { + // read: #versions + v1 [+ v2 [etc]] + properties + int numVersions = _rawIn.read(); + if (numVersions == -1) throw new IOException("Unable to read versions"); + if (numVersions > 256) throw new IOException("Too many versions"); + int versions[] = new int[numVersions]; + for (int i = 0; i < numVersions; i++) { + versions[i] = _rawIn.read(); + if (versions[i] == -1) + throw new IOException("Not enough versions"); + } + Properties opts = DataHelper.readProperties(_rawIn); + + int version = 0; + for (int i = 0; i < versions.length && version == 0; i++) { + for (int j = 0; j < TCPTransport.SUPPORTED_PROTOCOLS.length; j++) { + if (TCPTransport.SUPPORTED_PROTOCOLS[j] == versions[i]) { + version = versions[i]; + break; + } + } + } + + if (_log.shouldLog(Log.DEBUG)) + _log.debug("HandleTest: version=" + version + " opts=" +opts); + + // send: 0xFF + versionOk + #bytesIP + IP + currentTime + properties + _rawOut.write(0xFF); + _rawOut.write(0xFF); + _rawOut.write(version); + byte ip[] = _from.getBytes(); + _rawOut.write(ip.length); + _rawOut.write(ip); + DataHelper.writeDate(_rawOut, new Date(_context.clock().now())); + DataHelper.writeProperties(_rawOut, null); + _rawOut.flush(); + + if (_log.shouldLog(Log.DEBUG)) + _log.debug("HandleTest: result flushed"); + + } catch (IOException ioe) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Unable to verify test connection from " + _from, ioe); + } catch (DataFormatException dfe) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Unable to verify test connection from " + _from, dfe); + } finally { + if (_rawIn != null) try { _rawIn.close(); } catch (IOException ioe) {} + if (_rawOut != null) try { _rawOut.close(); } catch (IOException ioe) {} + if (_socket != null) try { _socket.close(); } catch (IOException ioe) {} + + _socket = null; + _rawIn = null; + _rawOut = null; + _agreedProtocol = -1; + _nonce = null; + _connectionTag = null; + _actualPeer = null; + _testComplete = true; + } + // send: 0xFF + versionOk + #bytesIP + IP + currentTime + properties + } + + /** + * Finish up the establishment (wrapping the streams, storing the netDb, + * persisting the connection tags, etc) + * + */ + private void establishComplete() { + // todo: add bw limiter + _connectionIn = _rawIn; + _connectionOut = _rawOut; + + Hash peer = _actualPeer.getIdentity().getHash(); + _context.netDb().store(peer, _actualPeer); + _transport.getTagManager().replaceTag(peer, _nextConnectionTag, _key); + } + + public String getError() { return _error; } + public boolean getTestComplete() { return _testComplete; } + + /** + * Kill the handler, closing all sockets and streams, setting everything + * back to failure states, and setting the given error. + * + */ + private void fail(String error) { + fail(error, null); + } + private void fail(String error, Exception e) { + if (_error == null) // only grab the first error + _error = error; + + if (_rawIn != null) try { _rawIn.close(); } catch (IOException ioe) {} + if (_rawOut != null) try { _rawOut.close(); } catch (IOException ioe) {} + if (_socket != null) try { _socket.close(); } catch (IOException ioe) {} + + _socket = null; + _rawIn = null; + _rawOut = null; + _agreedProtocol = -1; + _nonce = null; + _connectionTag = null; + _actualPeer = null; + + if (_log.shouldLog(Log.WARN)) + _log.warn(error, e); + } +} diff --git a/router/java/src/net/i2p/router/transport/tcp/ConnectionRunner.java b/router/java/src/net/i2p/router/transport/tcp/ConnectionRunner.java new file mode 100644 index 0000000000..11ba15c326 --- /dev/null +++ b/router/java/src/net/i2p/router/transport/tcp/ConnectionRunner.java @@ -0,0 +1,84 @@ +package net.i2p.router.transport.tcp; + +import java.io.IOException; +import java.io.OutputStream; + +import net.i2p.data.DataFormatException; +import net.i2p.data.i2np.I2NPMessage; +import net.i2p.router.OutNetMessage; +import net.i2p.router.Router; +import net.i2p.router.RouterContext; +import net.i2p.util.I2PThread; +import net.i2p.util.Log; + +/** + * Push out I2NPMessages across the wire + * + */ +class ConnectionRunner implements Runnable { + private Log _log; + private RouterContext _context; + private TCPConnection _con; + private boolean _keepRunning; + + public ConnectionRunner(RouterContext ctx, TCPConnection con) { + _context = ctx; + _log = ctx.logManager().getLog(ConnectionRunner.class); + _con = con; + _keepRunning = false; + } + + public void startRunning() { + _keepRunning = true; + + String name = "TCP " + _context.routerHash().toBase64().substring(0,6) + + " to " + + _con.getRemoteRouterIdentity().calculateHash().toBase64().substring(0,6); + I2PThread t = new I2PThread(this, name); + t.start(); + } + public void stopRunning() { + _keepRunning = false; + } + + public void run() { + while (_keepRunning && !_con.getIsClosed()) { + OutNetMessage msg = _con.getNextMessage(); + if ( (msg == null) && (_keepRunning) ) { + _log.error("next message is null but we should keep running?"); + } else { + sendMessage(msg); + } + } + } + + private void sendMessage(OutNetMessage msg) { + byte data[] = msg.getMessageData(); + if (data == null) { + if (_log.shouldLog(Log.WARN)) + _log.warn("message " + msg.getMessageType() + "/" + msg.getMessageId() + + " expired before it could be sent"); + _con.sent(msg, false, 0); + return; + } + + OutputStream out = _con.getOutputStream(); + boolean ok = false; + long before = -1; + long after = -1; + try { + synchronized (out) { + before = _context.clock().now(); + out.write(data); + out.flush(); + after = _context.clock().now(); + } + + ok = true; + } catch (IOException ioe) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Error writing out the message", ioe); + } + _con.sent(msg, ok, after - before); + } +} diff --git a/router/java/src/net/i2p/router/transport/tcp/ConnectionTagManager.java b/router/java/src/net/i2p/router/transport/tcp/ConnectionTagManager.java new file mode 100644 index 0000000000..b79ab695b1 --- /dev/null +++ b/router/java/src/net/i2p/router/transport/tcp/ConnectionTagManager.java @@ -0,0 +1,64 @@ +package net.i2p.router.transport.tcp; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import net.i2p.data.ByteArray; +import net.i2p.data.Hash; +import net.i2p.data.SessionKey; +import net.i2p.router.RouterContext; + +/** + * Organize the tags used to connect with peers. + * + */ +public class ConnectionTagManager { + private RouterContext _context; + /** H(routerIdentity) to ByteArray */ + private Map _tags; + /** H(routerIdentity) to SessionKey */ + private Map _keys; + + /** synchronize against this when dealing with the data */ + private Object _lock; + + public ConnectionTagManager(RouterContext context) { + _context = context; + _tags = new HashMap(128); + _keys = new HashMap(128); + _lock = new Object(); + } + + /** Retrieve the associated tag (but do not consume it) */ + public ByteArray getTag(Hash peer) { + synchronized (_lock) { + return (ByteArray)_tags.get(peer); + } + } + + public SessionKey getKey(Hash peer) { + synchronized (_lock) { // + return (SessionKey)_keys.get(peer); + } + } + public SessionKey getKey(ByteArray tag) { + synchronized (_lock) { // + for (Iterator iter = _tags.keySet().iterator(); iter.hasNext(); ) { + Hash peer = (Hash)iter.next(); + ByteArray cur = (ByteArray)_tags.get(peer); + if (cur.equals(tag)) + return (SessionKey)_keys.get(peer); + } + return null; + } + } + + /** Update the tag associated with a peer, dropping the old one */ + public void replaceTag(Hash peer, ByteArray newTag, SessionKey key) { + synchronized (_lock) { + _tags.put(peer, newTag); + _keys.put(peer, key); + } + } +} diff --git a/router/java/src/net/i2p/router/transport/tcp/MessageHandler.java b/router/java/src/net/i2p/router/transport/tcp/MessageHandler.java new file mode 100644 index 0000000000..d0bdeef221 --- /dev/null +++ b/router/java/src/net/i2p/router/transport/tcp/MessageHandler.java @@ -0,0 +1,37 @@ +package net.i2p.router.transport.tcp; + +import net.i2p.data.Hash; +import net.i2p.data.RouterIdentity; +import net.i2p.data.i2np.I2NPMessageReader; +import net.i2p.data.i2np.I2NPMessage; + +/** + * Receive messages from a message reader and bounce them off to the transport + * for further enqueueing. + */ +public class MessageHandler implements I2NPMessageReader.I2NPMessageEventListener { + private TCPTransport _transport; + private TCPConnection _con; + private RouterIdentity _ident; + private Hash _identHash; + + public MessageHandler(TCPTransport transport, TCPConnection con) { + _transport = transport; + _con = con; + _ident = con.getRemoteRouterIdentity(); + _identHash = _ident.calculateHash(); + } + + public void disconnected(I2NPMessageReader reader) { + _con.closeConnection(); + } + + public void messageReceived(I2NPMessageReader reader, I2NPMessage message, long msToRead) { + _transport.messageReceived(message, _ident, _identHash, msToRead, message.getSize()); + } + + public void readError(I2NPMessageReader reader, Exception error) { + _con.closeConnection(); + } + +} diff --git a/router/java/src/net/i2p/router/transport/tcp/RestrictiveTCPConnection.java b/router/java/src/net/i2p/router/transport/tcp/RestrictiveTCPConnection.java deleted file mode 100644 index 5621095fc6..0000000000 --- a/router/java/src/net/i2p/router/transport/tcp/RestrictiveTCPConnection.java +++ /dev/null @@ -1,338 +0,0 @@ -package net.i2p.router.transport.tcp; -/* - * free (adj.): unencumbered; not under the control of others - * Written by jrandom in 2003 and released into the public domain - * with no warranty of any kind, either expressed or implied. - * It probably won't make your computer catch on fire, or eat - * your children, but it might. Use at your own risk. - * - */ - -import java.io.BufferedOutputStream; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.math.BigInteger; -import java.net.Socket; -import java.util.Date; -import java.util.Iterator; - -import net.i2p.crypto.AESInputStream; -import net.i2p.crypto.AESOutputStream; -import net.i2p.data.DataFormatException; -import net.i2p.data.DataHelper; -import net.i2p.data.RouterAddress; -import net.i2p.data.RouterIdentity; -import net.i2p.router.Router; -import net.i2p.router.RouterContext; -import net.i2p.router.transport.BandwidthLimitedInputStream; -import net.i2p.router.transport.BandwidthLimitedOutputStream; -import net.i2p.util.I2PThread; -import net.i2p.util.Log; - -/** - * TCPConnection that validates the time and protocol version, dropping connection if - * the clocks are too skewed or the versions don't match. - * - */ -class RestrictiveTCPConnection extends TCPConnection { - private Log _log; - - public RestrictiveTCPConnection(RouterContext context, Socket s, boolean locallyInitiated) throws IOException { - super(context, s, locallyInitiated); - _log = context.logManager().getLog(RestrictiveTCPConnection.class); - _context.statManager().createRateStat("tcp.establishConnectionTime", "How long does it take for us to successfully establish a connection (either locally or remotely initiated)?", "TCP Transport", new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l }); - } - - /** passed in the handshake process for the connection, and only equivilant protocols will be accepted */ - private final static long PROTO_ID = 13; - - /** read / write buffer size */ - private final static int BUF_SIZE = 2*1024; - - private boolean validateVersion() throws DataFormatException, IOException { - if (_log.shouldLog(Log.DEBUG)) _log.debug("Before validating version"); - ByteArrayOutputStream baos = new ByteArrayOutputStream(8); - DataHelper.writeLong(baos, 4, PROTO_ID); - byte encr[] = _context.AESEngine().safeEncrypt(baos.toByteArray(), _key, _iv, 16); - DataHelper.writeLong(_out, 2, encr.length); - _out.write(encr); - - if (_log.shouldLog(Log.DEBUG)) _log.debug("Version sent"); - // we've sent our version, now read what theirs is - - int rlen = (int)DataHelper.readLong(_in, 2); - byte pencr[] = new byte[rlen]; - int read = DataHelper.read(_in, pencr); - if (read != rlen) - throw new DataFormatException("Not enough data in peer version"); - byte decr[] = _context.AESEngine().safeDecrypt(pencr, _key, _iv); - if (decr == null) - throw new DataFormatException("Unable to decrypt - failed version?"); - - ByteArrayInputStream bais = new ByteArrayInputStream(decr); - long peerProtoId = DataHelper.readLong(bais, 4); - - - if (_log.shouldLog(Log.DEBUG)) _log.debug("Version received [" + peerProtoId + "]"); - - return validateVersion(PROTO_ID, peerProtoId); - } - - private boolean validateVersion(long us, long them) throws DataFormatException, IOException { - if (us != them) { - if (_log.shouldLog(Log.ERROR)) - _log.error("INVALID PROTOCOL VERSIONS! us = " + us + " them = " + them + ": " + _remoteIdentity.getHash()); - if (them > us) - _context.router().setHigherVersionSeen(true); - return false; - } else { - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Valid protocol version: us = " + us + " them = " + them + ": " + _remoteIdentity.getHash()); - return true; - } - } - - private boolean validateTime() throws DataFormatException, IOException { - Date now = new Date(_context.clock().now()); - ByteArrayOutputStream baos = new ByteArrayOutputStream(8); - DataHelper.writeDate(baos, now); - - byte encr[] = _context.AESEngine().safeEncrypt(baos.toByteArray(), _key, _iv, 16); - DataHelper.writeLong(_out, 2, encr.length); - _out.write(encr); - - // we've sent our date, now read what theirs is - - int rlen = (int)DataHelper.readLong(_in, 2); - byte pencr[] = new byte[rlen]; - int read = DataHelper.read(_in, pencr); - if (read != rlen) - throw new DataFormatException("Not enough data in peer date"); - byte decr[] = _context.AESEngine().safeDecrypt(pencr, _key, _iv); - if (decr == null) - throw new DataFormatException("Unable to decrypt - failed date?"); - - ByteArrayInputStream bais = new ByteArrayInputStream(decr); - Date theirNow = DataHelper.readDate(bais); - - long diff = now.getTime() - theirNow.getTime(); - if ( (diff > Router.CLOCK_FUDGE_FACTOR) || (diff < (0-Router.CLOCK_FUDGE_FACTOR)) ) { - if (_log.shouldLog(Log.ERROR)) - _log.error("Peer is out of time sync by " + DataHelper.formatDuration(diff) - + "! They think it is " + theirNow + ", we think it is " - + new Date(_context.clock().now()) + ": " + _remoteIdentity.getHash(), - new Exception("Time sync error - please make sure your clock is correct!")); - return false; - } else { - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Peer sync difference: " + diff + "ms: " + _remoteIdentity.getHash()); - return true; - } - } - - /** - * Exchange TCP addresses, and if we're didn't establish this connection, validate - * the peer with validatePeerAddresses(TCPAddress[]). - * - * @return true if the peer is valid (and reachable) - */ - private boolean validatePeerAddress() throws DataFormatException, IOException { - if (_log.shouldLog(Log.DEBUG)) _log.debug("Before sending my addresses"); - TCPAddress me[] = _transport.getMyAddresses(); - ByteArrayOutputStream baos = new ByteArrayOutputStream(256); - if (_log.shouldLog(Log.DEBUG)) _log.debug("Sending " + me.length + " addresses"); - DataHelper.writeLong(baos, 1, me.length); - for (int i = 0; i < me.length; i++) { - DataHelper.writeString(baos, me[i].getHost()); - DataHelper.writeLong(baos, 2, me[i].getPort()); - if (_log.shouldLog(Log.DEBUG)) _log.debug("Sent my address [" + me[i].getHost() + ":" + me[i].getPort() + "]"); - } - if (_log.shouldLog(Log.DEBUG)) _log.debug("Sent my " + me.length + " addresses"); - - byte encr[] = _context.AESEngine().safeEncrypt(baos.toByteArray(), _key, _iv, 256); - DataHelper.writeLong(_out, 2, encr.length); - _out.write(encr); - - // we've sent our addresses, now read their addresses - - int rlen = (int)DataHelper.readLong(_in, 2); - byte pencr[] = new byte[rlen]; - int read = DataHelper.read(_in, pencr); - if (read != rlen) - throw new DataFormatException("Not enough data in peer addresses"); - byte decr[] = _context.AESEngine().safeDecrypt(pencr, _key, _iv); - if (decr == null) - throw new DataFormatException("Unable to decrypt - invalid addresses?"); - - ByteArrayInputStream bais = new ByteArrayInputStream(decr); - long numAddresses = DataHelper.readLong(bais, 1); - if (_log.shouldLog(Log.DEBUG)) _log.debug("Peer will send us " + numAddresses + " addresses"); - TCPAddress peer[] = new TCPAddress[(int)numAddresses]; - for (int i = 0; i < peer.length; i++) { - String host = DataHelper.readString(bais); - int port = (int)DataHelper.readLong(bais, 2); - peer[i] = new TCPAddress(host, port); - if (_log.shouldLog(Log.DEBUG)) _log.debug("Received peer address [" + peer[i].getHost() + ":" + peer[i].getPort() + "]"); - } - - // ok, we've received their addresses, now we determine whether we need to - // validate them or not - if (weInitiatedConnection()) { - if (_log.shouldLog(Log.DEBUG)) _log.debug("We initiated the connection, so no need to validate"); - return true; // we connected to them, so we know we can, um, connect to them - } else { - if (_log.shouldLog(Log.DEBUG)) _log.debug("We received the connection, so validate"); - boolean valid = validatePeerAddresses(peer); - if (_log.shouldLog(Log.DEBUG)) _log.debug("We received the connection, validated? " + valid); - return valid; - } - } - - /** - * They connected to us, but since we don't want to deal with restricted route topologies - * (yet), we want to make sure *they* are reachable by other people. In the long run, we'll - * likely want to test this by routing messages through random peers to see if *they* can - * contact them (but only when we want to determine whether to use them as a gateway, etc). - * - * Oh, I suppose I should explain what this method does, not just why. Ok, this iterates - * through all of the supplied TCP addresses attempting to open a socket. If it receives - * any data on that socket, we'll assume their address is valid and we're satisfied. (yes, - * this means it could point at random addresses, etc - this is not sufficient for dealing - * with hostile peers, just with misconfigured peers). If we can't find a peer address that - * we can connect to, they suck and can go eat worms. - * - */ - private boolean validatePeerAddresses(TCPAddress addresses[]) throws DataFormatException, IOException { - if (_log.shouldLog(Log.DEBUG)) _log.debug("Before validating peer addresses [" + addresses.length + "]..."); - for (int i = 0; i < addresses.length; i++) { - if (_log.shouldLog(Log.DEBUG)) _log.debug("Before validating peer address (" + addresses[i].getHost() + ":"+ addresses[i].getPort() + ")..."); - boolean ok = sendsUsData(addresses[i]); - if (_log.shouldLog(Log.DEBUG)) _log.debug("Before validating peer address (" + addresses[i].getHost() + ":"+ addresses[i].getPort() + ") [" + ok + "]..."); - if (ok) { - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Peer address " + addresses[i].getHost() + ":" + addresses[i].getPort() + " validated!"); - return true; - } else { - if (_log.shouldLog(Log.WARN)) - _log.warn("Peer address " + addresses[i].getHost() + ":" + addresses[i].getPort() + " could NOT be validated"); - } - } - if (_log.shouldLog(Log.WARN)) - _log.warn("None of the peer addresses could be validated!"); - return false; - } - - private boolean sendsUsData(TCPAddress peer) { - SocketCreator creator = new SocketCreator(peer.getHost(), peer.getPort(), false); - // blocking call, timing out after the SOCKET_CREATE_TIMEOUT if there - // isn't a definitive yes or no on whether the peer is running I2NP or not - // the call closes the socket created regardless - boolean established = creator.verifyReachability(TCPTransport.SOCKET_CREATE_TIMEOUT); - if (_log.shouldLog(Log.DEBUG)) - _log.debug("After joining socket creator via peer callback [could establish? " + established + "]"); - return established; - } - - public RouterIdentity establishConnection() { - long start = _context.clock().now(); - long success = 0; - if (_log.shouldLog(Log.DEBUG)) _log.debug("Establishing connection..."); - - BigInteger myPub = _builder.getMyPublicValue(); - try { - _socket.setSoTimeout(ESTABLISHMENT_TIMEOUT); - if (_log.shouldLog(Log.DEBUG)) _log.debug("Before key exchange..."); - exchangeKey(); - if (_log.shouldLog(Log.DEBUG)) _log.debug("Key exchanged..."); - // key exchanged. now say who we are and prove it - boolean ok = identifyStationToStation(); - if (_log.shouldLog(Log.DEBUG)) _log.debug("After station to station [" + ok + "]..."); - - if (!ok) { - throw new DataFormatException("Station to station identification failed! MITM?"); - } - - if (_log.shouldLog(Log.DEBUG)) _log.debug("before validateVersion..."); - boolean versionOk = validateVersion(); - if (_log.shouldLog(Log.DEBUG)) _log.debug("after validateVersion [" + versionOk + "]..."); - - if (!versionOk) { - // not only do we remove the reference to the invalid peer - _context.netDb().fail(_remoteIdentity.getHash()); - // but we make sure that we don't try to talk to them soon even if we get a new ref - _context.shitlist().shitlistRouter(_remoteIdentity.getHash(), "Invalid protocol version"); - throw new DataFormatException("Peer uses an invalid version! dropping"); - } - - if (_log.shouldLog(Log.DEBUG)) _log.debug("before validateTime..."); - boolean timeOk = validateTime(); - if (_log.shouldLog(Log.DEBUG)) _log.debug("after validateTime [" + timeOk + "]..."); - if (!timeOk) { - _context.shitlist().shitlistRouter(_remoteIdentity.getHash(), "Time too far out of sync"); - throw new DataFormatException("Peer is too far out of sync with the current router's clock! dropping"); - } - - try { - _context.netDb().store(_remoteIdentity.getHash(), _remoteInfo); - } catch (IllegalArgumentException iae) { - if (_log.shouldLog(Log.ERROR)) - _log.error("Peer gave us invalid router info", iae); - // not only do we remove the reference to the invalid peer - _context.netDb().fail(_remoteIdentity.getHash()); - // but we make sure that we don't try to talk to them soon even if we get a new ref - _context.shitlist().shitlistRouter(_remoteIdentity.getHash(), "Invalid peer info"); - throw new DataFormatException("Invalid peer info provided"); - } - - if (_log.shouldLog(Log.DEBUG)) _log.debug("before validate peer address..."); - boolean peerReachable = validatePeerAddress(); - if (_log.shouldLog(Log.DEBUG)) _log.debug("after validatePeerAddress [" + peerReachable + "]..."); - if (!peerReachable) { - _context.shitlist().shitlistRouter(_remoteIdentity.getHash(), "Unreachable address"); - throw new DataFormatException("Peer provided us with an unreachable router address, and we can't handle restricted routes yet! dropping"); - } - - if (_log.shouldLog(Log.INFO)) - _log.info("TCP connection " + _id + " established with " + _remoteIdentity.getHash().toBase64()); - - _in = new BandwidthLimitedInputStream(_context, new AESInputStream(_context, _in, _key, _iv), _remoteIdentity); - _out = new AESOutputStream(_context, new BufferedOutputStream(new BandwidthLimitedOutputStream(_context, _out, _remoteIdentity), BUF_SIZE), _key, _iv); - _socket.setSoTimeout(0); - success = _context.clock().now(); - - for (Iterator iter = _remoteInfo.getAddresses().iterator(); iter.hasNext(); ) { - RouterAddress curAddr = (RouterAddress)iter.next(); - if (TCPTransport.STYLE.equals(curAddr.getTransportStyle())) { - _remoteAddress = new TCPAddress(curAddr); - break; - } - } - if (_remoteAddress == null) { - throw new DataFormatException("wtf, no TCP addresses? we already verified!"); - } - - established(); - return _remoteIdentity; - - } catch (IOException ioe) { - if (_log.shouldLog(Log.WARN)) - _log.warn("Error establishing connection with " + _remoteHost + ":" + _remotePort, ioe); - closeConnection(); - return null; - } catch (DataFormatException dfe) { - if (_log.shouldLog(Log.WARN)) - _log.warn("Error establishing connection with " + _remoteHost + ":" + _remotePort, dfe); - closeConnection(); - return null; - } catch (Throwable t) { - if (_log.shouldLog(Log.ERROR)) - _log.error("jrandom is paranoid so we're catching it all during establishConnection " + _remoteHost + ":" + _remotePort, t); - closeConnection(); - return null; - } finally { - if (success > 0) - _context.statManager().addRateData("tcp.establishConnectionTime", success-start, success-start); - } - } -} diff --git a/router/java/src/net/i2p/router/transport/tcp/SocketCreator.java b/router/java/src/net/i2p/router/transport/tcp/SocketCreator.java deleted file mode 100644 index bc8e48e95c..0000000000 --- a/router/java/src/net/i2p/router/transport/tcp/SocketCreator.java +++ /dev/null @@ -1,217 +0,0 @@ -package net.i2p.router.transport.tcp; - -import java.io.IOException; -import java.io.OutputStream; -import java.net.Socket; -import java.net.UnknownHostException; - -import net.i2p.util.Log; -import net.i2p.util.SimpleTimer; - -/** - * Helper class to coordinate the creation of sockets to I2P routers - * - */ -class SocketCreator implements SimpleTimer.TimedEvent { - private final static Log _log = new Log(SocketCreator.class); - private String _host; - private int _port; - private Socket _socket; - private boolean _keepOpen; - private boolean _established; - private long _created; - private long _timeoutMs; - private String _caller; - - public SocketCreator(String host, int port) { - this(host, port, true); - } - public SocketCreator(String host, int port, boolean keepOpen) { - _host = host; - _port = port; - _socket = null; - _keepOpen = keepOpen; - _established = false; - _created = System.currentTimeMillis(); - } - - public Socket getSocket() { return _socket; } - - public boolean couldEstablish() { return _established; } - - /** the first byte sent and received must be 0x42 */ - public final static int I2P_FLAG = 0x42; - /** sent if we arent trying to talk */ - private final static int NOT_I2P_FLAG = 0x2B; - - /** - * Blocking call to determine whether the socket configured can be reached - * (and whether it is a valid I2P router). The socket created to test this - * will be closed afterwards. - * - * @param timeoutMs max time to wait for validation - * @return true if the peer is reachable and sends us the I2P_FLAG, false - * otherwise - */ - public boolean verifyReachability(long timeoutMs) { - _timeoutMs = timeoutMs; - _caller = Thread.currentThread().getName(); - SimpleTimer.getInstance().addEvent(this, timeoutMs); - checkEstablish(); - if (_log.shouldLog(Log.DEBUG)) - _log.debug("veriyReachability complete, established? " + _established); - return _established; - } - - /** - * Blocking call to establish a socket connection to the peer. After either - * the timeout has expired or the socket has been created, the socket and/or - * its status can be accessed via couldEstablish() and getSocket(), - * respectively. If the socket could not be established in the given time - * frame, the socket is closed. - * - */ - public void establishConnection(long timeoutMs) { - _timeoutMs = timeoutMs; - _caller = Thread.currentThread().getName(); - SimpleTimer.getInstance().addEvent(this, timeoutMs); - doEstablish(); - if (_log.shouldLog(Log.DEBUG)) - _log.debug("EstablishConnection complete, established? " + _established); - } - - /** - * Called when the timeout was reached - depending on our configuration and - * whether a connection was established, we may want to tear down the socket. - * - */ - public void timeReached() { - long duration = System.currentTimeMillis() - _created; - if (!_keepOpen) { - if (_socket != null) { - if (_log.shouldLog(Log.DEBUG)) - _log.debug(_caller + ": timeReached(), dont keep open, and we have a socket. kill it (" - + duration + "ms, delay " + _timeoutMs + ")"); - try { _socket.close(); } catch (IOException ioe) {} - _socket = null; - } else { - if (_log.shouldLog(Log.DEBUG)) - _log.debug(_caller + ": timeReached(), dont keep open, but we don't have a socket. noop"); - } - } else { - if (_established) { - // noop - if (_log.shouldLog(Log.DEBUG)) - _log.debug(_caller + ": timeReached(), keep open, and we have an established socket. noop"); - } else { - if (_log.shouldLog(Log.DEBUG)) - _log.debug(_caller + ": timeReached(), keep open, but we havent established yet. kill the socket! (" - + duration + "ms, delay " + _timeoutMs + ")"); - if (_socket != null) try { _socket.close(); } catch (IOException ioe) {} - _socket = null; - } - } - } - - /** - * Create the socket with the intent of keeping it open - * - */ - private void doEstablish() { - try { - _socket = new Socket(_host, _port); - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Socket created"); - - if (_socket == null) return; - OutputStream os = _socket.getOutputStream(); - os.write(I2P_FLAG); - os.flush(); - if (_log.shouldLog(Log.DEBUG)) - _log.debug("I2P flag sent"); - - if (_socket == null) return; - int val = _socket.getInputStream().read(); - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Value read: [" + val + "] == flag? [" + I2P_FLAG + "]"); - if (val != I2P_FLAG) { - if (_socket != null) - _socket.close(); - _socket = null; - } - _established = true; - return; - } catch (UnknownHostException uhe) { - if (_log.shouldLog(Log.WARN)) - _log.warn("Error establishing connection to " + _host + ':' + _port, uhe); - if (_socket != null) try { _socket.close(); } catch (IOException ioe2) {} - _socket = null; - return; - } catch (IOException ioe) { - if (_log.shouldLog(Log.WARN)) - _log.warn("Error establishing connection to " + _host + ':' + _port + ": "+ ioe.getMessage()); - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Error establishing", ioe); - if (_socket != null) try { _socket.close(); } catch (IOException ioe2) {} - _socket = null; - return; - } catch (Exception e) { - if (_log.shouldLog(Log.WARN)) - _log.warn("Unknown error establishing connection to " + _host + ':' + _port + ": " + e.getMessage()); - if (_socket != null) try { _socket.close(); } catch (IOException ioe2) {} - _socket = null; - return; - } - } - - /** - * Try to establish the connection, but don't actually send the I2P flag. The - * other side will timeout waiting for it and consider it a dropped connection, - * but since they will have sent us the I2P flag first we will still know they are - * reachable. - * - */ - private void checkEstablish() { - try { - _socket = new Socket(_host, _port); - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Socket created (but we're not sending the flag, since we're just testing them)"); - - if (_socket == null) return; - OutputStream os = _socket.getOutputStream(); - os.write(NOT_I2P_FLAG); - os.flush(); - - if (_socket == null) return; - int val = _socket.getInputStream().read(); - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Value read: [" + val + "] == flag? [" + I2P_FLAG + "]"); - - - if (_socket == null) return; - _socket.close(); - _socket = null; - _established = (val == I2P_FLAG); - return; - } catch (UnknownHostException uhe) { - if (_log.shouldLog(Log.WARN)) - _log.warn("Error establishing connection to " + _host + ':' + _port, uhe); - if (_socket != null) try { _socket.close(); } catch (IOException ioe) {} - return; - } catch (IOException ioe) { - if (_log.shouldLog(Log.WARN)) - _log.warn("Error establishing connection to " + _host + ':' + _port + ": "+ ioe.getMessage()); - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Error establishing", ioe); - if (_socket != null) try { _socket.close(); } catch (IOException ioe2) {} - _socket = null; - return; - } catch (Exception e) { - if (_log.shouldLog(Log.WARN)) - _log.warn("Unknown error establishing connection to " + _host + ':' + _port + ": " + e.getMessage()); - if (_socket != null) try { _socket.close(); } catch (IOException ioe) {} - _socket = null; - return; - } - } -} diff --git a/router/java/src/net/i2p/router/transport/tcp/TCPAddress.java b/router/java/src/net/i2p/router/transport/tcp/TCPAddress.java index e21956a4c0..1575cda429 100644 --- a/router/java/src/net/i2p/router/transport/tcp/TCPAddress.java +++ b/router/java/src/net/i2p/router/transport/tcp/TCPAddress.java @@ -10,6 +10,7 @@ package net.i2p.router.transport.tcp; import java.net.InetAddress; import java.net.UnknownHostException; +import java.util.Properties; import net.i2p.data.DataHelper; import net.i2p.data.RouterAddress; @@ -44,17 +45,18 @@ public class TCPAddress { } public TCPAddress() { - _host = null; - _port = -1; - _addr = null; + _host = null; + _port = -1; + _addr = null; } public TCPAddress(InetAddress addr, int port) { if (addr != null) _host = addr.getHostAddress(); - _addr = addr; - _port = port; + _addr = addr; + _port = port; } + public TCPAddress(RouterAddress addr) { if (addr == null) throw new IllegalArgumentException("Null router address"); String host = addr.getOptions().getProperty(PROP_HOST); @@ -80,6 +82,21 @@ public class TCPAddress { } } + public RouterAddress toRouterAddress() { + if ( (_host == null) || (_port <= 0) ) + return null; + + RouterAddress addr = new RouterAddress(); + + Properties props = new Properties(); + props.setProperty(PROP_HOST, _host); + props.setProperty(PROP_PORT, ""+_port); + + addr.setOptions(props); + addr.setTransportStyle(TCPTransport.STYLE); + return addr; + } + public String getHost() { return _host; } public void setHost(String host) { _host = host; } public InetAddress getAddress() { return _addr; } @@ -88,44 +105,46 @@ public class TCPAddress { public void setPort(int port) { _port = port; } public boolean isPubliclyRoutable() { - if (_host == null) return false; - try { - InetAddress addr = InetAddress.getByName(_host); - byte quad[] = addr.getAddress(); - if (quad[0] == (byte)127) return false; - if (quad[0] == (byte)10) return false; - if ( (quad[0] == (byte)172) && (quad[1] >= (byte)16) && (quad[1] <= (byte)31) ) return false; - if ( (quad[0] == (byte)192) && (quad[1] == (byte)168) ) return false; - if (quad[0] >= (byte)224) return false; // no multicast - return true; // or at least possible to be true - } catch (Throwable t) { - _log.error("Error checking routability", t); - return false; - } + if (_host == null) return false; + try { + InetAddress addr = InetAddress.getByName(_host); + byte quad[] = addr.getAddress(); + if (quad[0] == (byte)127) return false; + if (quad[0] == (byte)10) return false; + if ( (quad[0] == (byte)172) && (quad[1] >= (byte)16) && (quad[1] <= (byte)31) ) return false; + if ( (quad[0] == (byte)192) && (quad[1] == (byte)168) ) return false; + if (quad[0] >= (byte)224) return false; // no multicast + return true; // or at least possible to be true + } catch (Throwable t) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Error checking routability", t); + return false; + } } public String toString() { return _host + ":" + _port; } public int hashCode() { - int rv = 0; - rv += _port; - if (_addr != null) rv += _addr.getHostAddress().hashCode(); - else - if (_host != null) rv += _host.hashCode(); - return rv; + int rv = 0; + rv += _port; + if (_addr != null) + rv += _addr.getHostAddress().hashCode(); + else + if (_host != null) rv += _host.hashCode(); + return rv; } public boolean equals(Object val) { - if ( (val != null) && (val instanceof TCPAddress) ) { - TCPAddress addr = (TCPAddress)val; - if ( (_addr != null) && (_addr.getHostAddress() != null) ) { - return DataHelper.eq(getAddress().getHostAddress(), addr.getAddress().getHostAddress()) && - (getPort() == addr.getPort()); + if ( (val != null) && (val instanceof TCPAddress) ) { + TCPAddress addr = (TCPAddress)val; + if ( (_addr != null) && (_addr.getHostAddress() != null) ) { + return DataHelper.eq(getAddress().getHostAddress(), addr.getAddress().getHostAddress()) + && (getPort() == addr.getPort()); } else { - return DataHelper.eq(getHost(), addr.getHost()) && - (getPort() == addr.getPort()); + return DataHelper.eq(getHost(), addr.getHost()) + && (getPort() == addr.getPort()); } - } - return false; + } + return false; } } diff --git a/router/java/src/net/i2p/router/transport/tcp/TCPConnection.java b/router/java/src/net/i2p/router/transport/tcp/TCPConnection.java index 33906cac64..eff802d57b 100644 --- a/router/java/src/net/i2p/router/transport/tcp/TCPConnection.java +++ b/router/java/src/net/i2p/router/transport/tcp/TCPConnection.java @@ -1,666 +1,210 @@ package net.i2p.router.transport.tcp; -/* - * free (adj.): unencumbered; not under the control of others - * Written by jrandom in 2003 and released into the public domain - * with no warranty of any kind, either expressed or implied. - * It probably won't make your computer catch on fire, or eat - * your children, but it might. Use at your own risk. - * - */ -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.math.BigInteger; -import java.net.InetAddress; + import java.net.Socket; + import java.util.ArrayList; -import java.util.Iterator; import java.util.List; -import net.i2p.crypto.AESInputStream; -import net.i2p.crypto.AESOutputStream; -import net.i2p.crypto.DHSessionKeyBuilder; -import net.i2p.data.ByteArray; -import net.i2p.data.DataFormatException; -import net.i2p.data.DataHelper; -import net.i2p.data.Hash; -import net.i2p.data.RouterAddress; import net.i2p.data.RouterIdentity; import net.i2p.data.RouterInfo; -import net.i2p.data.SessionKey; -import net.i2p.data.Signature; -import net.i2p.data.i2np.I2NPMessage; import net.i2p.data.i2np.I2NPMessageReader; import net.i2p.router.OutNetMessage; import net.i2p.router.RouterContext; -import net.i2p.router.transport.BandwidthLimitedInputStream; -import net.i2p.router.transport.BandwidthLimitedOutputStream; -import net.i2p.router.transport.FIFOBandwidthLimiter; -import net.i2p.util.I2PThread; import net.i2p.util.Log; -import net.i2p.util.NativeBigInteger; /** - * Wraps a connection - this contains a reader thread (via I2NPMessageReader) and - * a writer thread (ConnectionRunner). The writer reads the pool of outbound - * messages and writes them in order, while the reader fires off events + * Central choke point for a single TCP connection to a single peer. * */ -class TCPConnection implements I2NPMessageReader.I2NPMessageEventListener { +public class TCPConnection { private Log _log; - protected static int _idCounter = 0; - protected int _id; - protected DHSessionKeyBuilder _builder; - protected Socket _socket; - protected String _remoteHost; - protected int _remotePort; - protected I2NPMessageReader _reader; - protected InputStream _in; - protected OutputStream _out; - protected RouterIdentity _remoteIdentity; - protected RouterInfo _remoteInfo; - protected TCPTransport _transport; - protected ConnectionRunner _runner; - protected List _toBeSent; - protected SessionKey _key; - protected ByteArray _extraBytes; - protected byte[] _iv; + private RouterContext _context; + private RouterIdentity _ident; + private TCPAddress _remoteAddress; + private List _pendingMessages; + private InputStream _in; + private OutputStream _out; + private Socket _socket; + private TCPTransport _transport; + private ConnectionRunner _runner; + private I2NPMessageReader _reader; + private long _started; private boolean _closed; - private boolean _weInitiated; - private long _created; - protected RouterContext _context; - protected TCPAddress _remoteAddress; - public TCPConnection(RouterContext context, Socket s, boolean locallyInitiated) throws IOException { - _context = context; - _log = context.logManager().getLog(TCPConnection.class); - _context.statManager().createRateStat("tcp.queueSize", "How many messages were already in the queue when a new message was added (only when it wasnt empty)?", - "TCP Transport", new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l }); - _context.statManager().createRateStat("tcp.writeTimeLarge", "How long it takes to write a message that is over 2K?", - "TCP Transport", new long[] { 60*1000l, 10*60*1000l, 60*60*1000l, 24*60*60*1000l }); - _context.statManager().createRateStat("tcp.writeTimeSmall", "How long it takes to write a message that is under 2K?", - "TCP Transport", new long[] { 60*1000l, 10*60*1000l, 60*60*1000l, 24*60*60*1000l }); - _context.statManager().createRateStat("tcp.writeTimeSlow", "How long it takes to write a message (ignoring messages transferring in under a second)?", - "TCP Transport", new long[] { 60*1000l, 10*60*1000l, 60*60*1000l, 24*60*60*1000l }); - _id = ++_idCounter; - _weInitiated = locallyInitiated; + public TCPConnection(RouterContext ctx) { + _context = ctx; + _log = ctx.logManager().getLog(TCPConnection.class); + _pendingMessages = new ArrayList(4); + _ident = null; + _remoteAddress = null; + _in = null; + _out = null; + _socket = null; + _transport = null; + _started = -1; _closed = false; - _socket = s; - _created = -1; - _toBeSent = new ArrayList(); - try { - _in = _socket.getInputStream(); - _out = _socket.getOutputStream(); - } catch (IOException ioe) { - _log.error("Error getting streams for the connection", ioe); - } - _builder = new DHSessionKeyBuilder(); - _extraBytes = null; - - // sun keeps the socket's InetAddress around after its been closed, but kaffe (and the rest of classpath) - // doesn't, so we've got to check & cache it here if we want to log it later. (kaffe et al are acting per - // spec, btw) - try { - InetAddress addr = s.getInetAddress(); - if (addr != null) { - _remoteHost = addr.getHostAddress(); - } - _remotePort = s.getPort(); - if (locallyInitiated) - _remoteAddress = new TCPAddress(_remoteHost, _remotePort); - } catch (NullPointerException npe) { - throw new IOException("kaffe is being picky since the socket closed too fast..."); - } - if (_log.shouldLog(Log.INFO)) - _log.info("Connected with peer: " + _remoteHost + ":" + _remotePort); + _runner = new ConnectionRunner(_context, this); } - + + /** Who are we talking with (or null if not identified) */ + public RouterIdentity getRemoteRouterIdentity() { return _ident; } + /** What is the peer's TCP address (using the IP address not hostname) */ public TCPAddress getRemoteAddress() { return _remoteAddress; } + /** Who are we talking with (or null if not identified) */ + public void setRemoteRouterIdentity(RouterIdentity ident) { _ident = ident; } + /** What is the peer's TCP address (using the IP address not hostname) */ + public void setRemoteAddress(TCPAddress addr) { _remoteAddress = addr; } - /** how long has this connection been around for, or -1 if it isn't established yet */ - public long getLifetime() { - if (_created > 0) - return _context.clock().now() - _created; - else - return -1; - } - - protected boolean weInitiatedConnection() { return _weInitiated; } - - public RouterIdentity getRemoteRouterIdentity() { return _remoteIdentity; } - int getId() { return _id; } - int getPendingMessageCount() { - synchronized (_toBeSent) { - return _toBeSent.size(); - } - } - - protected void exchangeKey() throws IOException, DataFormatException { - BigInteger myPub = _builder.getMyPublicValue(); - byte myPubBytes[] = myPub.toByteArray(); - DataHelper.writeLong(_out, 2, myPubBytes.length); - _out.write(myPubBytes); - - int rlen = (int)DataHelper.readLong(_in, 2); - byte peerPubBytes[] = new byte[rlen]; - int read = DataHelper.read(_in, peerPubBytes); - - if (_log.shouldLog(Log.DEBUG)) - _log.debug("rlen: " + rlen + " peerBytes: " + DataHelper.toString(peerPubBytes) + " read: " + read); - - BigInteger peerPub = new NativeBigInteger(1, peerPubBytes); - _builder.setPeerPublicValue(peerPub); - - _key = _builder.getSessionKey(); - _extraBytes = _builder.getExtraBytes(); - _iv = new byte[16]; - System.arraycopy(_extraBytes.getData(), 0, _iv, 0, Math.min(_extraBytes.getData().length, _iv.length)); - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Session key: " + _key.toBase64() + " extra bytes: " + _extraBytes.getData().length); - } - - protected boolean identifyStationToStation() throws IOException, DataFormatException { - ByteArrayOutputStream baos = new ByteArrayOutputStream(10*1024); - _context.router().getRouterInfo().writeBytes(baos); - Hash keyHash = _context.sha().calculateHash(_key.getData()); - keyHash.writeBytes(baos); - Signature sig = _context.dsa().sign(baos.toByteArray(), _context.keyManager().getSigningPrivateKey()); - sig.writeBytes(baos); - - byte encr[] = _context.AESEngine().safeEncrypt(baos.toByteArray(), _key, _iv, 10*1024); - DataHelper.writeLong(_out, 2, encr.length); - _out.write(encr); - - // we've identified ourselves, now read who they are - int rlen = (int)DataHelper.readLong(_in, 2); - byte pencr[] = new byte[rlen]; - int read = DataHelper.read(_in, pencr); - if (read != rlen) - throw new DataFormatException("Not enough data in peer ident"); - byte decr[] = _context.AESEngine().safeDecrypt(pencr, _key, _iv); - if (decr == null) - throw new DataFormatException("Unable to decrypt - failed exchange?"); - - ByteArrayInputStream bais = new ByteArrayInputStream(decr); - RouterInfo peer = new RouterInfo(); - peer.readBytes(bais); - _remoteIdentity = peer.getIdentity(); - Hash peerKeyHash = new Hash(); - peerKeyHash.readBytes(bais); - - if (!peerKeyHash.equals(keyHash)) { - if (_log.shouldLog(Log.ERROR)) - _log.error("Peer tried to spoof!"); - return false; - } - - Signature rsig = new Signature(); - rsig.readBytes(bais); - byte signedData[] = new byte[decr.length - rsig.getData().length]; - System.arraycopy(decr, 0, signedData, 0, signedData.length); - boolean valid = _context.dsa().verifySignature(rsig, signedData, _remoteIdentity.getSigningPublicKey()); - _remoteInfo = peer; - return valid; - } - - protected final static int ESTABLISHMENT_TIMEOUT = 10*1000; // 10 second lag (not necessarily for the entire establish) - - public RouterIdentity establishConnection() { - BigInteger myPub = _builder.getMyPublicValue(); - try { - _socket.setSoTimeout(ESTABLISHMENT_TIMEOUT); - exchangeKey(); - // key exchanged. now say who we are and prove it - boolean ok = identifyStationToStation(); - - if (!ok) - throw new DataFormatException("Station to station identification failed! MITM?"); - else { - if (_log.shouldLog(Log.INFO)) - _log.info("TCP connection " + _id + " established with " - + _remoteIdentity.getHash().toBase64()); - _in = new AESInputStream(_context, new BandwidthLimitedInputStream(_context, _in, _remoteIdentity), _key, _iv); - _out = new AESOutputStream(_context, new BandwidthLimitedOutputStream(_context, _out, _remoteIdentity), _key, _iv); - _socket.setSoTimeout(0); - - for (Iterator iter = _remoteInfo.getAddresses().iterator(); iter.hasNext(); ) { - RouterAddress curAddr = (RouterAddress)iter.next(); - if (TCPTransport.STYLE.equals(curAddr.getTransportStyle())) { - _remoteAddress = new TCPAddress(curAddr); - break; - } - } - if (_remoteAddress == null) { - throw new DataFormatException("wtf, peer " + _remoteIdentity.calculateHash().toBase64() - + " unreachable? we already verified!"); - } - established(); - return _remoteIdentity; - } - } catch (IOException ioe) { - if (_log.shouldLog(Log.ERROR)) - _log.error("Error establishing connection", ioe); - closeConnection(); - return null; - } catch (DataFormatException dfe) { - if (_log.shouldLog(Log.ERROR)) - _log.error("Error establishing connection", dfe); - closeConnection(); - return null; - } catch (Throwable t) { - if (_log.shouldLog(Log.ERROR)) - _log.error("jrandom is paranoid so we're catching it all during establishConnection", t); - closeConnection(); - return null; - } - } - - protected void established() { _created = _context.clock().now(); } - + /** + * Actually start processing the messages on the connection (and reading + * from the peer, of course). This call should not block. + * + */ public void runConnection() { - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Run connection"); - _runner = new ConnectionRunner(); - Thread t = new I2PThread(_runner); - t.setName("Run Conn [" + _id + "]"); - t.setDaemon(true); - t.start(); - _reader = new I2NPMessageReader(_context, _in, this, "TCP Read [" + _id + ":" + _transport.getListenPort() + "]"); + String name = "TCP Read [" + _ident.calculateHash().toBase64().substring(0,6) + "]"; + _reader = new I2NPMessageReader(_context, _in, new MessageHandler(_transport, this), name); _reader.startReading(); + _runner.startRunning(); + _started = _context.clock().now(); } - public void setTransport(TCPTransport trans) { _transport = trans; } - - /** dont bitch about expiring messages if they don't even last 60 seconds */ - private static final long MIN_MESSAGE_LIFETIME_FOR_PENALTY = 60*1000; - - public void addMessage(OutNetMessage msg) { - msg.timestamp("TCPConnection.addMessage"); - int totalPending = 0; - boolean fail = false; - long beforeAdd = _context.clock().now(); - StringBuffer pending = new StringBuffer(64); - List removed = null; - synchronized (_toBeSent) { - for (int i = 0; i < _toBeSent.size(); i++) { - OutNetMessage cur = (OutNetMessage)_toBeSent.get(i); - if (cur.getExpiration() < beforeAdd) { - if (cur.getLifetime() > MIN_MESSAGE_LIFETIME_FOR_PENALTY) { - fail = true; - break; - } else { - // yeah, it expired, so drop it, but it wasn't our - // fault (since it was almost expired when we got it - if (removed == null) - removed = new ArrayList(2); - removed.add(cur); - _toBeSent.remove(i); - i--; - } - } - } - if (!fail) { - _toBeSent.add(msg); - } - totalPending = _toBeSent.size(); - pending.append(totalPending).append(": "); - if (fail) { - for (int i = 0; i < totalPending; i++) { - OutNetMessage cur = (OutNetMessage)_toBeSent.get(i); - pending.append(cur.getMessageSize()).append(" byte "); - pending.append(cur.getMessageType()).append(" message added"); - pending.append(" added ").append(cur.getLifetime()).append(" ms ago, "); - } - } - - // the ConnectionRunner.getNext does a wait() until we have messages - _toBeSent.notifyAll(); - } - long afterAdd = _context.clock().now(); - - if (totalPending >= 2) - _context.statManager().addRateData("tcp.queueSize", totalPending-1, 0); - - if (removed != null) { - if (_log.shouldLog(Log.WARN)) - _log.warn("messages expired on the queue to " + _remoteIdentity.getHash().toBase64() - + " but they weren't that old: " + removed.size()); - for (int i = 0; i < removed.size(); i++) { - OutNetMessage cur = (OutNetMessage)removed.get(i); - msg.timestamp("TCPConnection.addMessage expired but not our fault"); - _transport.afterSend(cur, false, false); - } - } - - if (fail) { - if (_log.shouldLog(Log.ERROR)) - _log.error("messages expired on the queue to " + _remoteIdentity.getHash().toBase64() + ": " + totalPending); - if (_log.shouldLog(Log.WARN)) - _log.warn("messages expired on the queue to " + _remoteIdentity.getHash().toBase64() + ": " + pending.toString()); - - if (_out instanceof BandwidthLimitedOutputStream) { - BandwidthLimitedOutputStream o = (BandwidthLimitedOutputStream)_out; - FIFOBandwidthLimiter.Request req = o.getCurrentRequest(); - if (req != null) { - if (_log.shouldLog(Log.ERROR)) - _log.error("When the messages timed out, our outbound con requested " - + req.getTotalOutboundRequested() + " bytes (" + req.getPendingOutboundRequested() - + " pending) after waiting " + (_context.clock().now() - req.getRequestTime()) + "ms"); - } - } - // do we really want to give them a comm error because they're so.damn.slow reading their stream? - _context.profileManager().commErrorOccurred(_remoteIdentity.getHash()); - - msg.timestamp("TCPConnection.addMessage saw an expired queued message"); - _transport.afterSend(msg, false, false); - // should we really be closing a connection if they're that slow? - // yeah, i think we should. - closeConnection(); - } else { - - long diff = afterAdd - beforeAdd; - if (diff > 500) { - if (_log.shouldLog(Log.WARN)) - _log.warn("Lock contention adding a message: " + diff + "ms to " - + _remoteIdentity.getHash().toBase64() + ": " + totalPending); - } - - msg.timestamp("TCPConnection.addMessage after toBeSent.add and notify"); - - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Add message with toBeSent.size = " + totalPending + " to " + _remoteIdentity.getHash().toBase64()); - if (totalPending <= 0) { - if (_log.shouldLog(Log.ERROR)) - _log.error("WTF, total pending after adding " + msg.getMessage().getClass().getName() + " <= 0! " + msg); - } - } - } - - void closeConnection() { + /** + * Disconnect from the peer immediately. This stops any related helper + * threads, closes all streams, and fails all pending messages. This can + * be called multiple times safely. + * + */ + public synchronized void closeConnection() { + if (_log.shouldLog(Log.INFO)) + _log.info("Connection closed", new Exception("Closed by")); if (_closed) return; - _closed = true; - if (_remoteIdentity != null) { - if (_log.shouldLog(Log.WARN)) - _log.warn("Closing the connection to " + _remoteIdentity.getHash().toBase64(), - new Exception("Closed by")); - } else { - if (_socket != null) { - if (_log.shouldLog(Log.WARN)) { - _log.warn("Closing the unestablished connection with " - + _remoteHost + ":" - + _remotePort, new Exception("Closed by")); - } - } else { - if (_log.shouldLog(Log.WARN)) - _log.warn("Closing the unestablished connection", new Exception("Closed by")); - } - } - if (_reader != null) _reader.stopReading(); - if (_runner != null) _runner.stopRunning(); + _closed = true; + if (_runner != null) + _runner.stopRunning(); + if (_reader != null) + _reader.stopReading(); if (_in != null) try { _in.close(); } catch (IOException ioe) {} if (_out != null) try { _out.close(); } catch (IOException ioe) {} if (_socket != null) try { _socket.close(); } catch (IOException ioe) {} - if (_toBeSent != null) { - long now = _context.clock().now(); - synchronized (_toBeSent) { - for (Iterator iter = _toBeSent.iterator(); iter.hasNext(); ) { - OutNetMessage msg = (OutNetMessage)iter.next(); - msg.timestamp("TCPTransport.closeConnection caused fail"); - if (_log.shouldLog(Log.WARN)) - _log.warn("Connection closed to " + _remoteIdentity.getHash().toBase64() - + " while the message was sitting on the TCP Connection's queue! too slow by: " - + (now-msg.getExpiration()) + "ms: " + msg); - _transport.afterSend(msg, false, false); - } - _toBeSent.clear(); - } + List msgs = clearPendingMessages(); + for (int i = 0; i < msgs.size(); i++) { + OutNetMessage msg = (OutNetMessage)msgs.get(0); + _transport.afterSend(msg, false, true, -1); } - _transport.connectionClosed(this); - } - - List getPendingMessages() { - synchronized (_toBeSent) { - return new ArrayList(_toBeSent); - } - } - - public void disconnected(I2NPMessageReader reader) { - if (_log.shouldLog(Log.WARN)) - _log.warn("Remote disconnected: " + _remoteIdentity.getHash().toBase64()); - closeConnection(); - } - - public void messageReceived(I2NPMessageReader reader, I2NPMessage message, long msToReceive) { - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Message received from " + _remoteIdentity.getHash().toBase64()); - try { - ByteArrayOutputStream baos = new ByteArrayOutputStream(2*1024); - message.writeBytes(baos); - int size = baos.size(); - // this is called by the I2NPMessageReader's thread, so it delays the reading from this peer only - //_log.debug("Delaying inbound for size " + size); - //BandwidthLimiter.getInstance().delayInbound(_remoteIdentity, size); - _transport.messageReceived(message, _remoteIdentity, null, msToReceive, size); - } catch (DataFormatException dfe) { - if (_log.shouldLog(Log.WARN)) - _log.warn("How did we read a message that is poorly formatted...", dfe); - } catch (IOException ioe) { - if (_log.shouldLog(Log.WARN)) - _log.warn("How did we read a message that can't be written to memory...", ioe); - } - } - - public void readError(I2NPMessageReader reader, Exception error) { - if (_log.shouldLog(Log.ERROR)) - _log.error("Error reading from stream to " + _remoteIdentity.getHash().toBase64() + ": " + error.getMessage()); - if (_log.shouldLog(Log.WARN)) - _log.warn("Error reading from stream to " + _remoteIdentity.getHash().toBase64(), error); } /** - * If we are taking an absurdly long time to send out a message, drop it - * since we're overloaded. + * Pull off any unsent OutNetMessages from the queue * */ - private static final long MAX_LIFETIME_BEFORE_OUTBOUND_EXPIRE = 15*1000; + public List clearPendingMessages() { + List rv = null; + synchronized (_pendingMessages) { + rv = new ArrayList(_pendingMessages); + _pendingMessages.clear(); + } + return rv; + } + /** + * Add the given message to the outbound queue, notifying our + * runners that we want to send it. + * + */ + public void addMessage(OutNetMessage msg) { + synchronized (_pendingMessages) { + _pendingMessages.add(msg); + _pendingMessages.notifyAll(); + } + } - class ConnectionRunner implements Runnable { - private boolean _running; - public void run() { - _running = true; - while (_running && !_closed) { - OutNetMessage nextMessage = getNext(); - if (nextMessage != null) { - boolean sent = doSend(nextMessage); - if (!sent) { - _running = false; - } - } - } - - closeConnection(); - } - - private OutNetMessage getNext() { - OutNetMessage msg = null; - // _running is kept seperate from _closed, since _running refers - // to the ConnectionRunner, while _closed refers to the TCPConnection - // (in case we want to pause running, etc). they both need to be - // checked here, since the connection may be closed before this - // thread even gets started up - while ( (msg == null) && (_running) && (!_closed) ) { - synchronized (_toBeSent) { - if (_toBeSent.size() <= 0) { - try { - _toBeSent.wait(); - } catch (InterruptedException ie) {} - } - - boolean ancientFound = locked_expireOldMessages(); - if (ancientFound) { - _running = false; - return null; - } - - if (_toBeSent.size() > 0) { - msg = (OutNetMessage)_toBeSent.remove(0); - } - } - } - return msg; - } - - /** - * Fail any messages that have expired on the queue - * - * @return true if any of the messages expired are really really old - * (indicating a hung connection) - */ - private boolean locked_expireOldMessages() { + /** + * Blocking call to retrieve the next pending message. As a side effect, + * this fails messages on the queue that have expired, and in turn never + * returns an expired message. + * + * @return next message or null if the connection has been closed. + */ + OutNetMessage getNextMessage() { + OutNetMessage msg = null; + while ( (msg == null) && (!_closed) ) { + List expired = null; long now = _context.clock().now(); - List timedOut = null; - for (int i = 0; i < _toBeSent.size(); i++) { - OutNetMessage cur = (OutNetMessage)_toBeSent.get(i); - if (cur.getExpiration() < now) { - if (timedOut == null) - timedOut = new ArrayList(2); - timedOut.add(cur); - _toBeSent.remove(i); - i--; - } else { - long lifetime = cur.timestamp("TCPConnection.runner.locked_expireOldMessages still ok with " - + (i) + " ahead and " + (_toBeSent.size()-i-1) - + " behind on the queue"); - if (lifetime > MAX_LIFETIME_BEFORE_OUTBOUND_EXPIRE) { - cur.timestamp("TCPConnection.runner.locked_expireOldMessages lifetime too long - " + lifetime); - if (timedOut == null) - timedOut = new ArrayList(2); - timedOut.add(cur); - _toBeSent.remove(i); + synchronized (_pendingMessages) { + for (int i = 0; i < _pendingMessages.size(); i++) { + OutNetMessage cur = (OutNetMessage)_pendingMessages.get(i); + if (cur.getExpiration() < now) { + if (expired == null) + expired = new ArrayList(1); + expired.add(cur); + _pendingMessages.remove(i); i--; } } - } - - boolean reallySlowFound = false; - - if (timedOut != null) { - for (int i = 0; i < timedOut.size(); i++) { - OutNetMessage failed = (OutNetMessage)timedOut.get(i); - if (_log.shouldLog(Log.WARN)) - _log.warn("Message " + i + "/" + timedOut.size() - + " timed out while sitting on the TCP Connection's queue! was too slow by: " - + (now-failed.getExpiration()) + "ms to " - + _remoteIdentity.getHash().toBase64() + ": " + failed); - failed.timestamp("TCPConnection.runner.locked_expireOldMessages expired with " + _toBeSent.size() + " left"); - _transport.afterSend(failed, false, false); - if (failed.getLifetime() >= MIN_MESSAGE_LIFETIME_FOR_PENALTY) - reallySlowFound = true; + + if (_pendingMessages.size() > 0) { + msg = (OutNetMessage)_pendingMessages.remove(0); + } else { + if (expired == null) { + try { + _pendingMessages.wait(); + } catch (InterruptedException ie) {} + } } } - return reallySlowFound; - } - - /** - * send the message - * - * @return true if the message was sent ok, false if the connection b0rked - */ - private boolean doSend(OutNetMessage msg) { - msg.timestamp("TCPConnection.runner.doSend fetched"); - long afterExpire = _context.clock().now(); - - long remaining = msg.getExpiration() - afterExpire; - if (remaining < 0) { - if (_log.shouldLog(Log.WARN)) - _log.warn("Message " + msg.getMessageType() + "/" + msg.getMessageId() - + " expired before doSend (too slow by " + remaining + "ms)"); - _transport.afterSend(msg, false, false); - return true; - } - - byte data[] = msg.getMessageData(); - if (data == null) { - if (_log.shouldLog(Log.WARN)) - _log.warn("message " + msg.getMessageType() + "/" + msg.getMessageId() - + " expired before it could be sent"); - _transport.afterSend(msg, false, false); - return true; - } - msg.timestamp("TCPConnection.runner.doSend before sending " - + data.length + " bytes"); - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Sending " + data.length + " bytes to " - + _remoteIdentity.getHash().toBase64()); - - long exp = msg.getMessage().getMessageExpiration().getTime(); - - long beforeWrite = 0; - try { - synchronized (_out) { - beforeWrite = _context.clock().now(); - _out.write(data); - _out.flush(); + if (expired != null) { + for (int i = 0; i < expired.size(); i++) { + OutNetMessage cur = (OutNetMessage)expired.get(i); + sent(cur, false, 0); } - } catch (IOException ioe) { - if (_log.shouldLog(Log.ERROR)) - _log.error("IO error writing out a " + data.length + " byte message to " - + _remoteIdentity.getHash().toBase64()); - return false; - } - - long end = _context.clock().now(); - long timeLeft = exp - end; - - msg.timestamp("TCPConnection.runner.doSend sent and flushed " + data.length + " bytes"); - - if (_log.shouldLog(Log.INFO)) - _log.info("Message " + msg.getMessageType() - + " (expiring in " + timeLeft + "ms) sent to " - + _remoteIdentity.getHash().toBase64() + " from " - + _context.routerHash().toBase64() - + " over connection " + _id + " with " + data.length - + " bytes in " + (end - afterExpire) + "ms (write took " - + (end - beforeWrite) + "ms, prepare took " - + (beforeWrite - afterExpire) + "ms)"); - - long lifetime = msg.getLifetime(); - if (lifetime > 10*1000) { - if (_log.shouldLog(Log.WARN)) - _log.warn("The processing of the message took way too long (" + lifetime - + "ms) - time left (" + timeLeft + ") to " - + _remoteIdentity.getHash().toBase64() + "\n" + msg.toString()); - } - _transport.afterSend(msg, true, (end-beforeWrite)); - - if (_log.shouldLog(Log.DEBUG)) - _log.debug("doSend - message sent completely: " - + msg.getMessageSize() + " byte " + msg.getMessageType() + " message to " - + _remoteIdentity.getHash().toBase64()); - if (end - afterExpire > 1000) { - if (_log.shouldLog(Log.WARN)) - _log.warn("Actual sending took too long ( " + (end-afterExpire) - + "ms) sending " + data.length + " bytes to " - + _remoteIdentity.getHash().toBase64()); - } - if (data.length > 2*1024) - _context.statManager().addRateData("tcp.writeTimeLarge", end - beforeWrite, end - beforeWrite); - else - _context.statManager().addRateData("tcp.writeTimeSmall", end - beforeWrite, end - beforeWrite); - if (end-beforeWrite > 1*1024) - _context.statManager().addRateData("tcp.writeTimeSlow", end - beforeWrite, end - beforeWrite); - return true; - } - - public void stopRunning() { - _running = false; - // stop the wait(...) - synchronized (_toBeSent) { - _toBeSent.notifyAll(); } } + return msg; + } + + /** How long has this connection been active for? */ + public long getLifetime() { return _context.clock().now() - _started; } + + void setTransport(TCPTransport transport) { _transport = transport; } + + /** + * Configure where this connection should read its data from. + * This should have any necessary bandwidth limiting and + * encryption filters already wrapped in it. + * + */ + void setInputStream(InputStream in) { _in = in; } + /** + * Configure where this connection should write its data to. + * This should have any necessary bandwidth limiting and + * encryption filters already wrapped in it. + * + */ + void setOutputStream(OutputStream out) { _out = out; } + /** + * Configure what underlying socket this connection uses. + * This is only referenced when closing the connection, and + * only if it was set. + */ + void setSocket(Socket socket) { _socket = socket; } + + /** Where this connection should write its data to. */ + OutputStream getOutputStream() { return _out; } + + /** Have we been closed already? */ + boolean getIsClosed() { return _closed; } + + /** + * The message was sent. + * + * @param msg message in question + * @param ok was the message sent ok? + * @param time how long did it take to write the message? + */ + void sent(OutNetMessage msg, boolean ok, long time) { + _transport.afterSend(msg, ok, true, time); } } diff --git a/router/java/src/net/i2p/router/transport/tcp/TCPConnectionEstablisher.java b/router/java/src/net/i2p/router/transport/tcp/TCPConnectionEstablisher.java new file mode 100644 index 0000000000..2fbb96aad4 --- /dev/null +++ b/router/java/src/net/i2p/router/transport/tcp/TCPConnectionEstablisher.java @@ -0,0 +1,43 @@ +package net.i2p.router.transport.tcp; + +import net.i2p.data.RouterInfo; +import net.i2p.router.RouterContext; +import net.i2p.util.Log; + +/** + * Build new outbound connections, one at a time. All the heavy lifting is in + * {@link ConnectionBuilder#establishConnection} + * + */ +public class TCPConnectionEstablisher implements Runnable { + private Log _log; + private RouterContext _context; + private TCPTransport _transport; + + public TCPConnectionEstablisher(RouterContext ctx, TCPTransport transport) { + _context = ctx; + _transport = transport; + _log = ctx.logManager().getLog(TCPConnectionEstablisher.class); + } + + public void run() { + while (true) { + RouterInfo info = _transport.getNextPeer(); + + ConnectionBuilder cb = new ConnectionBuilder(_context, _transport, info); + TCPConnection con = cb.establishConnection(); + if (con != null) { + _transport.connectionEstablished(con); + } else { + _transport.addConnectionErrorMessage(cb.getError()); + _context.shitlist().shitlistRouter(info.getIdentity().getHash(), "Unable to contact"); + } + + // this removes the _pending block on the address and + // identity we attempted to contact. if the peer changed + // identities, any additional _pending blocks will also have + // been cleared above with .connectionEstablished + _transport.establishmentComplete(info); + } + } +} diff --git a/router/java/src/net/i2p/router/transport/tcp/TCPListener.java b/router/java/src/net/i2p/router/transport/tcp/TCPListener.java index 9b205b92b0..50df5f837b 100644 --- a/router/java/src/net/i2p/router/transport/tcp/TCPListener.java +++ b/router/java/src/net/i2p/router/transport/tcp/TCPListener.java @@ -31,41 +31,52 @@ import net.i2p.util.SimpleTimer; class TCPListener { private Log _log; private TCPTransport _transport; - private TCPAddress _myAddress; private ServerSocket _socket; private ListenerRunner _listener; private RouterContext _context; private List _pendingSockets; private List _handlers; + /** + * How many concurrent connection attempts from peers we will try to + * deal with at once. + */ + private static final int CONCURRENT_HANDLERS = 3; + /** + * When things really suck, how long should we wait between attempts to + * listen to the socket? + */ + private final static int MAX_FAIL_DELAY = 5*60*1000; + /** if we're not making progress in 10s, drop 'em */ + private final static long HANDLE_TIMEOUT = 10*1000; + /** id generator for the connections */ + private static volatile int __handlerId = 0; + + public TCPListener(RouterContext context, TCPTransport transport) { _context = context; _log = context.logManager().getLog(TCPListener.class); - _myAddress = null; _transport = transport; _pendingSockets = new ArrayList(10); _handlers = new ArrayList(CONCURRENT_HANDLERS); } - - public void setAddress(TCPAddress address) { _myAddress = address; } - public TCPAddress getAddress() { return _myAddress; } - - private static final int CONCURRENT_HANDLERS = 3; - + public void startListening() { - for (int i = 0; i < CONCURRENT_HANDLERS; i++) { - SocketHandler handler = new SocketHandler(); - _handlers.add(handler); - Thread t = new I2PThread(handler); - t.setName("Handler" + i+" [" + _myAddress.getPort()+"]"); + TCPAddress addr = _transport.getMyAddress(); + if (addr != null) { + _listener = new ListenerRunner(addr); + Thread t = new I2PThread(_listener, "Listener [" + addr.getPort()+"]"); t.setDaemon(true); t.start(); + + for (int i = 0; i < CONCURRENT_HANDLERS; i++) { + SocketHandler handler = new SocketHandler(); + _handlers.add(handler); + Thread th = new I2PThread(handler, "Handler " + addr.getPort() + ": " + i); + th.setDaemon(true); + th.start(); + } } - _listener = new ListenerRunner(); - Thread t = new I2PThread(_listener); - t.setName("Listener [" + _myAddress.getPort()+"]"); - t.setDaemon(true); - t.start(); } public void stopListening() { @@ -75,11 +86,13 @@ class TCPListener { h.stopHandling(); } _handlers.clear(); - if (_socket != null) + + if (_socket != null) { try { _socket.close(); _socket = null; } catch (IOException ioe) {} + } } private InetAddress getInetAddress(String host) { @@ -96,13 +109,13 @@ class TCPListener { } } - private final static int MAX_FAIL_DELAY = 5*60*1000; - class ListenerRunner implements Runnable { private boolean _isRunning; private int _nextFailDelay = 1000; - public ListenerRunner() { + private TCPAddress _myAddress; + public ListenerRunner(TCPAddress address) { _isRunning = true; + _myAddress = address; } public void stopListening() { _isRunning = false; } @@ -111,34 +124,36 @@ class TCPListener { _log.info("Beginning TCP listener"); int curDelay = 0; - while ( (_isRunning) && (curDelay < MAX_FAIL_DELAY) ) { + while (_isRunning) { try { - if (_transport.getListenAddressIsValid()) { - _socket = new ServerSocket(_myAddress.getPort(), 5, getInetAddress(_myAddress.getHost())); - } else { + if (_transport.shouldListenToAllInterfaces()) { _socket = new ServerSocket(_myAddress.getPort()); + } else { + InetAddress listenAddr = getInetAddress(_myAddress.getHost()); + _socket = new ServerSocket(_myAddress.getPort(), 5, listenAddr); } if (_log.shouldLog(Log.INFO)) _log.info("Begin looping for host " + _myAddress.getHost() + ":" + _myAddress.getPort()); curDelay = 0; loop(); } catch (IOException ioe) { - if (_log.shouldLog(Log.ERROR)) - _log.error("Error listening to tcp connection " + _myAddress.getHost() + ":" + if (_log.shouldLog(Log.WARN)) + _log.warn("Error listening to tcp connection " + _myAddress.getHost() + ":" + _myAddress.getPort(), ioe); } if (_socket != null) { - stopListening(); try { _socket.close(); } catch (IOException ioe) {} _socket = null; } - if (_log.shouldLog(Log.ERROR)) - _log.error("Error listening, waiting " + _nextFailDelay + "ms before we try again"); + if (_log.shouldLog(Log.WARN)) + _log.warn("Error listening, waiting " + _nextFailDelay + "ms before we try again"); try { Thread.sleep(_nextFailDelay); } catch (InterruptedException ie) {} curDelay += _nextFailDelay; _nextFailDelay *= 5; + if (_nextFailDelay > MAX_FAIL_DELAY) + _nextFailDelay = MAX_FAIL_DELAY; } if (_log.shouldLog(Log.ERROR)) _log.error("CANCELING TCP LISTEN. delay = " + curDelay); @@ -242,60 +257,31 @@ class TCPListener { } } - /** if we're not making progress in 30s, drop 'em */ - private final static long HANDLE_TIMEOUT = 10*1000; - private static volatile int __handlerId = 0; - private class TimedHandler implements SimpleTimer.TimedEvent { private int _handlerId; private Socket _socket; private boolean _wasSuccessful; - private boolean _receivedIdentByte; public TimedHandler(Socket socket) { _socket = socket; _wasSuccessful = false; _handlerId = ++__handlerId; - _receivedIdentByte = false; } public int getHandlerId() { return _handlerId; } public void handle() { SimpleTimer.getInstance().addEvent(TimedHandler.this, HANDLE_TIMEOUT); - try { - OutputStream os = _socket.getOutputStream(); - os.write(SocketCreator.I2P_FLAG); - os.flush(); - if (_log.shouldLog(Log.DEBUG)) - _log.debug("listener: I2P flag sent"); - int val = _socket.getInputStream().read(); - if (_log.shouldLog(Log.DEBUG)) - _log.debug("listener: Value read: [" + val + "] == flag? [" + SocketCreator.I2P_FLAG + "]"); - if (val == -1) - throw new UnsupportedOperationException("Peer disconnected while we were looking for the I2P flag"); - if (val != SocketCreator.I2P_FLAG) { - throw new UnsupportedOperationException("Peer connecting to us didn't send the right I2P byte [" + val + "]"); - } - - _receivedIdentByte = true; - - TCPConnection c = new RestrictiveTCPConnection(_context, _socket, false); - _transport.handleConnection(c, null); + ConnectionHandler ch = new ConnectionHandler(_context, _transport, _socket); + TCPConnection con = ch.receiveConnection(); + if (con != null) { + _wasSuccessful = true; + _transport.connectionEstablished(con); + } else if (ch.getTestComplete()) { + // not a connection, but we verified the test _wasSuccessful = true; - } catch (UnsupportedOperationException uoe) { - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Failed to state they wanted to connect as I2P", uoe); - _wasSuccessful = false; - } catch (IOException ioe) { - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Error listening to the peer", ioe); - _wasSuccessful = false; - } catch (Throwable t) { - if (_log.shouldLog(Log.ERROR)) - _log.error("Error handling", t); - _wasSuccessful = false; } + if (!_wasSuccessful) + _transport.addConnectionErrorMessage(ch.getError()); } public boolean wasSuccessful() { return _wasSuccessful; } - public boolean receivedIdentByte() { return _receivedIdentByte; } /** * Called after a timeout period - if we haven't already established the @@ -307,13 +293,8 @@ class TCPListener { if (_log.shouldLog(Log.DEBUG)) _log.debug("Handle successful"); } else { - if (receivedIdentByte()) { - if (_log.shouldLog(Log.WARN)) - _log.warn("Unable to handle in the time allotted"); - } else { - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Peer didn't send the ident byte, so either they were testing us, or portscanning"); - } + if (_log.shouldLog(Log.WARN)) + _log.warn("Unable to handle in the time allotted"); try { _socket.close(); } catch (IOException ioe) {} } } 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 f3b25cf476..1052de7b87 100644 --- a/router/java/src/net/i2p/router/transport/tcp/TCPTransport.java +++ b/router/java/src/net/i2p/router/transport/tcp/TCPTransport.java @@ -1,968 +1,559 @@ package net.i2p.router.transport.tcp; -/* - * free (adj.): unencumbered; not under the control of others - * Written by jrandom in 2003 and released into the public domain - * with no warranty of any kind, either expressed or implied. - * It probably won't make your computer catch on fire, or eat - * your children, but it might. Use at your own risk. - * - */ -import java.net.Socket; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; -import java.util.LinkedList; import java.util.List; import java.util.Map; -import java.util.Properties; import java.util.Set; +import java.text.SimpleDateFormat; + import net.i2p.data.DataHelper; import net.i2p.data.Hash; import net.i2p.data.RouterAddress; import net.i2p.data.RouterIdentity; import net.i2p.data.RouterInfo; -import net.i2p.data.SigningPrivateKey; -import net.i2p.router.JobImpl; import net.i2p.router.OutNetMessage; import net.i2p.router.RouterContext; -import net.i2p.router.transport.TransportBid; import net.i2p.router.transport.TransportImpl; +import net.i2p.router.transport.TransportBid; import net.i2p.util.I2PThread; import net.i2p.util.Log; /** - * Defines a way to send a message to another peer and start listening for messages + * TCP Transport implementation, coordinating the connections + * between peers and the transmission of messages across those + * connections. * */ public class TCPTransport extends TransportImpl { - private Log _log; - public final static String STYLE = "TCP"; - private List _listeners; - /** RouterIdentity to List of TCPConnections */ - private Map _connections; - /** TCPAddress (w/ IP not hostname) to List of TCPConnections */ - private Map _connectionAddresses; - private String _listenHost; - private int _listenPort; - private RouterAddress _address; - private TCPAddress _tcpAddress; - private boolean _listenAddressIsValid; - /** H(ident) to PendingMessages for unestablished connections */ - private Map _msgs; - private boolean _running; + private final Log _log; + /** Our local TCP address, if known */ + private TCPAddress _myAddress; + /** How we receive connections */ + private TCPListener _listener; + /** Coordinate the agreed connection tags */ + private ConnectionTagManager _tagManager; - private int _numConnectionEstablishers; - private final static String PROP_ESTABLISHERS = "i2np.tcp.concurrentEstablishers"; - private final static int DEFAULT_ESTABLISHERS = 3; + /** H(RouterIdentity) to TCPConnection for fully established connections */ + private Map _connectionsByIdent; + /** TCPAddress::toString() to TCPConnection for fully established connections */ + private Map _connectionsByAddress; - public static String PROP_LISTEN_IS_VALID = "i2np.tcp.listenAddressIsValid"; + /** H(RouterIdentity) for not yet established connections */ + private Set _pendingConnectionsByIdent; + /** TCPAddress::toString() for not yet established connections */ + private Set _pendingConnectionsByAddress; - /** - * pre 1.4 java doesn't have a way to timeout the creation of sockets (which - * can take up to 3 minutes), so we do it on a seperate thread and wait for - * either that thread to complete, or for this timeout to be reached. + /** + * H(RouterIdentity) to List of OutNetMessage for messages targetting + * not yet established connections */ - final static long SOCKET_CREATE_TIMEOUT = 10*1000; + private Map _pendingMessages; + /** + * Object to lock on when touching the _connection maps or + * the pendingMessages map. In addition, this lock is notified whenever + * a brand new peer is added to the pendingMessages map + */ + private Object _connectionLock; + /** + * List of the most recent connection establishment error messages (where the + * message includes the time) + */ + private List _lastConnectionErrors; + /** All of the operating TCPConnectionEstablisher objects */ + private List _connectionEstablishers; - public TCPTransport(RouterContext context, RouterAddress address) { + /** What is this transport's identifier? */ + public static final String STYLE = "TCP"; + /** Should the TCP listener bind to all interfaces? */ + public static final String BIND_ALL_INTERFACES = "i2np.tcp.bindAllInterfaces"; + /** What host/ip should we be addressed as? */ + public static final String LISTEN_ADDRESS = "i2np.tcp.hostname"; + /** What port number should we listen to? */ + public static final String LISTEN_PORT = "i2np.tcp.port"; + /** Should we allow the transport to listen on a non routable address? */ + public static final String LISTEN_ALLOW_LOCAL = "i2np.tcp.allowLocal"; + /** Keep track of the last 10 error messages wrt establishing a connection */ + public static final int MAX_ERR_MESSAGES = 10; + public static final String PROP_ESTABLISHERS = "i2np.tcp.concurrentEstablishers"; + public static final int DEFAULT_ESTABLISHERS = 3; + + /** Ordered list of supported I2NP protocols */ + public static final int[] SUPPORTED_PROTOCOLS = new int[] { 1 }; + + /** Creates a new instance of TCPTransport */ + public TCPTransport(RouterContext context) { super(context); _log = context.logManager().getLog(TCPTransport.class); - if (_context == null) throw new RuntimeException("Context is null"); - if (_context.statManager() == null) throw new RuntimeException("Stat manager is null"); - _context.statManager().createFrequencyStat("tcp.attemptFailureFrequency", "How often do we attempt to contact someone, and fail?", "TCP Transport", new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l }); - _context.statManager().createFrequencyStat("tcp.attemptSuccessFrequency", "How often do we attempt to contact someone, and succeed?", "TCP Transport", new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l }); - _context.statManager().createFrequencyStat("tcp.acceptFailureFrequency", "How often do we reject someone who contacts us?", "TCP Transport", new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l }); - _context.statManager().createFrequencyStat("tcp.acceptSuccessFrequency", "How often do we accept someone who contacts us?", "TCP Transport", new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l }); - _context.statManager().createRateStat("tcp.connectionLifetime", "How long do connections last (measured when they close)?", "TCP Transport", new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l }); + _listener = new TCPListener(context, this); + _myAddress = null; + _tagManager = new ConnectionTagManager(context); + _connectionsByIdent = new HashMap(16); + _connectionsByAddress = new HashMap(16); + _pendingConnectionsByIdent = new HashSet(16); + _pendingConnectionsByAddress = new HashSet(16); + _connectionLock = new Object(); + _pendingMessages = new HashMap(16); + _lastConnectionErrors = new ArrayList(); - _listeners = new ArrayList(); - _connections = new HashMap(); - _connectionAddresses = new HashMap(); - _msgs = new HashMap(); - _address = address; - if (address != null) { - _listenHost = address.getOptions().getProperty(TCPAddress.PROP_HOST); - String portStr = address.getOptions().getProperty(TCPAddress.PROP_PORT); - try { - _listenPort = Integer.parseInt(portStr); - } catch (NumberFormatException nfe) { - _log.error("Invalid port: " + portStr + " Address: \n" + address, nfe); - } - _tcpAddress = new TCPAddress(_listenHost, _listenPort); - } - _listenAddressIsValid = false; - try { - String setting = _context.router().getConfigSetting(PROP_LISTEN_IS_VALID); - _listenAddressIsValid = Boolean.TRUE.toString().equalsIgnoreCase(setting); - } catch (Throwable t) { - _listenAddressIsValid = false; - if (_log.shouldLog(Log.WARN)) - _log.warn("Unable to determine whether TCP listening address is valid, so we're assuming it isn't. Set " + PROP_LISTEN_IS_VALID + " otherwise"); - } - _running = false; - } - - boolean getListenAddressIsValid() { return _listenAddressIsValid; } - SigningPrivateKey getMySigningKey() { return _context.keyManager().getSigningPrivateKey(); } - int getListenPort() { return _listenPort; } - - - public int countActivePeers() { - synchronized (_connections) { - return _connections.size(); - } - } - - /** fetch all of our TCP listening addresses */ - TCPAddress[] getMyAddresses() { - if (_address != null) { - TCPAddress rv[] = new TCPAddress[1]; - rv[0] = new TCPAddress(_listenHost, _listenPort); - return rv; - } else { - return new TCPAddress[0]; - } - } - - /** - * This message is called whenever a new message is added to the send pool, - * and it should not block - */ - protected void outboundMessageReady() { - //_context.jobQueue().addJob(new NextJob()); - NextJob j = new NextJob(); - j.runJob(); - } - - private class NextJob extends JobImpl { - public NextJob() { - super(TCPTransport.this._context); - } - public void runJob() { - OutNetMessage msg = getNextMessage(); - if (msg != null) { - handleOutbound(msg); // this just adds to either the establish thread's queue or the conn's queue - } else { - if (_log.shouldLog(Log.DEBUG)) - _log.debug("OutboundMessageReady called, but none were available"); - } - } - public String getName() { return "TCP Message Ready to send"; } - } - - /** - * Return a random connection to the peer from the set of known connections - * - */ - private TCPConnection getConnection(RouterIdentity peer) { - synchronized (_connections) { - if (!_connections.containsKey(peer)) - return null; - List cons = (List)_connections.get(peer); - if (cons.size() <= 0) - return null; - TCPConnection first = (TCPConnection)cons.get(0); - return first; - } - } - - protected void handleOutbound(OutNetMessage msg) { - msg.timestamp("TCPTransport.handleOutbound before handleConnection"); - TCPConnection con = getConnection(msg.getTarget().getIdentity()); - if (con == null) { - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Handling outbound message to an unestablished peer"); - msg.timestamp("TCPTransport.handleOutbound to addPending"); - addPending(msg); - } else { - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Toss the message onto an established peer's connection"); - msg.timestamp("TCPTransport.handleOutbound to con.addMessage"); - con.addMessage(msg); - } - } - - protected boolean establishConnection(RouterInfo target) { - long startEstablish = 0; - long socketCreated = 0; - long conCreated = 0; - long conEstablished = 0; - try { - for (Iterator iter = target.getAddresses().iterator(); iter.hasNext(); ) { - RouterAddress addr = (RouterAddress)iter.next(); - startEstablish = _context.clock().now(); - if (getStyle().equals(addr.getTransportStyle())) { - - TCPAddress tcpAddr = new TCPAddress(addr); - synchronized (_connectionAddresses) { - if (_connectionAddresses.containsKey(tcpAddr)) { - if (_log.shouldLog(Log.WARN)) - _log.warn("We already have a connection to another router at " + tcpAddr); - _context.shitlist().shitlistRouter(target.getIdentity().getHash(), "Duplicate TCP address (changed identities?)"); - _context.netDb().fail(target.getIdentity().getHash()); - return false; - } - } - - if (tcpAddr.equals(_tcpAddress)) { - if (_log.shouldLog(Log.WARN)) - _log.warn("Peer " + target.getIdentity().getHash().toBase64() - + " has OUR address [" + tcpAddr + "]"); - _context.profileManager().commErrorOccurred(target.getIdentity().getHash()); - _context.shitlist().shitlistRouter(target.getIdentity().getHash(), "Points at us"); - _context.netDb().fail(target.getIdentity().getHash()); - return false; - } - - if (!tcpAddr.isPubliclyRoutable() && false) { - if (_log.shouldLog(Log.WARN)) - _log.warn("Peer " + target.getIdentity().getHash().toBase64() - + " has an unroutable address [" + tcpAddr + "]"); - _context.profileManager().commErrorOccurred(target.getIdentity().getHash()); - _context.shitlist().shitlistRouter(target.getIdentity().getHash(), "Unroutable address"); - _context.netDb().fail(target.getIdentity().getHash()); - return false; - } - - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Establishing a connection with address " + addr); - Socket s = createSocket(addr); - socketCreated = _context.clock().now(); - if (s == null) { - if (_log.shouldLog(Log.WARN)) - _log.warn("Unable to establish a socket in time to " + addr); - _context.profileManager().commErrorOccurred(target.getIdentity().getHash()); - _context.shitlist().shitlistRouter(target.getIdentity().getHash(), "Unable to contact host"); - return false; - } - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Socket created"); - - TCPConnection con = new RestrictiveTCPConnection(_context, s, true); - conCreated = _context.clock().now(); - if (_log.shouldLog(Log.DEBUG)) - _log.debug("TCPConnection created"); - boolean established = handleConnection(con, target); - conEstablished = _context.clock().now(); - if (_log.shouldLog(Log.DEBUG)) - _log.debug("connection handled"); - return established; - } - } - - _context.shitlist().shitlistRouter(target.getIdentity().getHash(), "No addresses we can handle"); - return false; - } catch (Throwable t) { - if (_log.shouldLog(Log.WARN)) - _log.warn("Unexpected error establishing the connection", t); - _context.shitlist().shitlistRouter(target.getIdentity().getHash(), "Internal error connecting"); - return false; - } finally { - long diff = conEstablished - startEstablish; - if ( ( (diff > 6000) || (conEstablished == 0) ) && (_log.shouldLog(Log.WARN)) ) { - _log.warn("establishConnection took too long: socketCreate: " + - (socketCreated-startEstablish) + "ms conCreated: " + - (conCreated-socketCreated) + "ms conEstablished: " + - (conEstablished - conCreated) + "ms overall: " + diff); - } - } - } - - protected Socket createSocket(RouterAddress addr) { - String host = addr.getOptions().getProperty(TCPAddress.PROP_HOST); - String portStr = addr.getOptions().getProperty(TCPAddress.PROP_PORT); - int port = -1; - try { - port = Integer.parseInt(portStr); - } catch (NumberFormatException nfe) { - if (_log.shouldLog(Log.ERROR)) - _log.error("Invalid port number in router address: " + portStr, nfe); - return null; - } - - long start = _context.clock().now(); - SocketCreator creator = new SocketCreator(host, port); - // blocking call, timing out after the SOCKET_CREATE_TIMEOUT and - // killing the socket if it hasn't established the connection yet - creator.establishConnection(SOCKET_CREATE_TIMEOUT); - - long finish = _context.clock().now(); - long diff = finish - start; - if (diff > 6000) { - if (_log.shouldLog(Log.WARN)) - _log.warn("Creating a new socket took too long? wtf?! " + diff + "ms for " + host + ':' + port); - } - if (creator.couldEstablish()) - return creator.getSocket(); - else - return null; - } - - private boolean isConnected(RouterInfo info) { - return (null != getConnection(info.getIdentity())); - } - - public TransportBid bid(RouterInfo toAddress, long dataSize) { - TCPConnection con = getConnection(toAddress.getIdentity()); - int latencyStartup = 0; - if (con == null) - latencyStartup = 2000; - else - latencyStartup = 0; - - int sendTime = (int)((dataSize)/(16*1024)); // 16K/sec - int bytes = (int)dataSize+8; - - if (con != null) - sendTime += 50000 * con.getPendingMessageCount(); // try to avoid backed up (throttled) connections - - TransportBid bid = new TransportBid(); - bid.setBandwidthBytes(bytes); - bid.setExpiration(new Date(_context.clock().now()+1000*60)); // 1 minute - bid.setLatencyMs(latencyStartup + sendTime); - bid.setMessageSize((int)dataSize); - bid.setRouter(toAddress); - bid.setTransport(this); - - RouterAddress addr = getTargetAddress(toAddress); - if (addr == null) { - if (con == null) { - if (_log.shouldLog(Log.INFO)) - _log.info("No address or connection to " + toAddress.getIdentity().getHash().toBase64()); - // don't bid if we can't send them a message - return null; - } else { - if (_log.shouldLog(Log.DEBUG)) - _log.debug("No address, but we're connected to " + toAddress.getIdentity().getHash().toBase64()); - } - } - - return bid; - } - - public void rotateAddresses() { - // noop - } - public void addAddressInfo(Properties infoForNewAddress) { - // noop - } - - - public RouterAddress startListening() { - RouterAddress address = new RouterAddress(); - - address.setTransportStyle(getStyle()); - address.setCost(10); - address.setExpiration(null); - Properties options = new Properties(); - if (_address != null) { - options.setProperty(TCPAddress.PROP_HOST, _listenHost); - options.setProperty(TCPAddress.PROP_PORT, _listenPort+""); - } - address.setOptions(options); - - if (_address != null) { - try { - TCPAddress addr = new TCPAddress(); - addr.setHost(_listenHost); - addr.setPort(_listenPort); - TCPListener listener = new TCPListener(_context, this); - listener.setAddress(addr); - _listeners.add(listener); - listener.startListening(); - } catch (NumberFormatException nfe) { - if (_log.shouldLog(Log.ERROR)) - _log.error("Error parsing port number", nfe); - } - - addCurrentAddress(address); - } - - String str = _context.router().getConfigSetting(PROP_ESTABLISHERS); + String str = _context.getProperty(PROP_ESTABLISHERS); + int establishers = 0; if (str != null) { try { - _numConnectionEstablishers = Integer.parseInt(str); + establishers = Integer.parseInt(str); } catch (NumberFormatException nfe) { if (_log.shouldLog(Log.ERROR)) _log.error("Invalid number of connection establishers [" + str + "]"); - _numConnectionEstablishers = DEFAULT_ESTABLISHERS; + establishers = DEFAULT_ESTABLISHERS; } } else { - _numConnectionEstablishers = DEFAULT_ESTABLISHERS; + establishers = DEFAULT_ESTABLISHERS; } - - _running = true; - for (int i = 0; i < _numConnectionEstablishers; i++) { - Thread t = new I2PThread(new ConnEstablisher(i), "Conn Establisher" + i + ':' + _listenPort); + + _connectionEstablishers = new ArrayList(establishers); + for (int i = 0; i < establishers; i++) { + TCPConnectionEstablisher est = new TCPConnectionEstablisher(_context, this); + _connectionEstablishers.add(est); + String name = _context.routerHash().toBase64().substring(0,6) + " Est" + i; + I2PThread t = new I2PThread(est, name); t.setDaemon(true); t.start(); } + } + + public TransportBid bid(RouterInfo toAddress, long dataSize) { + TransportBid bid = new TransportBid(); + bid.setBandwidthBytes((int)dataSize); + bid.setExpiration(_context.clock().now() + 30*1000); + bid.setMessageSize((int)dataSize); + bid.setRouter(toAddress); + bid.setTransport(this); + int latency = 200; + if (!getIsConnected(toAddress.getIdentity())) + latency += 5000; + bid.setLatencyMs(latency); + return bid; + } + + private boolean getIsConnected(RouterIdentity ident) { + Hash peer = ident.calculateHash(); + synchronized (_connectionLock) { + return _connectionsByIdent.containsKey(peer); + } + } + + /** + * Called whenever a new message is ready to be sent. This should + * not block. + * + */ + protected void outboundMessageReady() { + OutNetMessage msg = getNextMessage(); + //if (_log.shouldLog(Log.DEBUG)) + // _log.debug("Outbound message ready: " + msg); - return address; + if (msg != null) { + TCPConnection con = null; + boolean newPeer = false; + synchronized (_connectionLock) { + Hash peer = msg.getTarget().getIdentity().calculateHash(); + con = (TCPConnection)_connectionsByIdent.get(peer); + if (con == null) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("No connections to " + peer.toBase64() + + ", request one"); + List msgs = (List)_pendingMessages.get(peer); + if (msgs == null) { + msgs = new ArrayList(4); + _pendingMessages.put(peer, msgs); + newPeer = true; + } + msgs.add(msg); + } + + if (newPeer) + _connectionLock.notifyAll(); + } + + if (con != null) + con.addMessage(msg); + } + } + + + /** + * The connection specified has been fully built + */ + void connectionEstablished(TCPConnection con) { + TCPAddress remAddr = con.getRemoteAddress(); + RouterIdentity ident = con.getRemoteRouterIdentity(); + if ( (remAddr == null) || (ident == null) ) { + con.closeConnection(); + return; + } + + List waitingMsgs = null; + List oldCons = null; + synchronized (_connectionLock) { + if (_connectionsByAddress.containsKey(remAddr.toString())) { + if (oldCons == null) + oldCons = new ArrayList(1); + oldCons.add(_connectionsByAddress.remove(remAddr.toString())); + } + _connectionsByAddress.put(remAddr.toString(), con); + + if (_connectionsByIdent.containsKey(ident.calculateHash())) { + if (oldCons == null) + oldCons = new ArrayList(1); + oldCons.add(_connectionsByIdent.remove(ident.calculateHash())); + } + _connectionsByIdent.put(ident.calculateHash(), con); + + // just drop the _pending connections - the establisher should fail + // them accordingly. + _pendingConnectionsByAddress.remove(remAddr.toString()); + _pendingConnectionsByIdent.remove(ident.calculateHash()); + + waitingMsgs = (List)_pendingMessages.remove(ident.calculateHash()); + } + + // close any old connections, moving any queued messages to the new one + if (oldCons != null) { + for (int i = 0; i < oldCons.size(); i++) { + TCPConnection cur = (TCPConnection)oldCons.get(i); + List msgs = cur.clearPendingMessages(); + for (int j = 0; j < msgs.size(); j++) { + con.addMessage((OutNetMessage)msgs.get(j)); + } + cur.closeConnection(); + } + } + + if (waitingMsgs != null) { + for (int i = 0; i < waitingMsgs.size(); i++) { + con.addMessage((OutNetMessage)waitingMsgs.get(i)); + } + } + + _context.shitlist().unshitlistRouter(ident.calculateHash()); + + con.setTransport(this); + con.runConnection(); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Connection set to run"); + } + + /** + * Blocking call from when a remote peer tells us what they think our + * IP address is. This may do absolutely nothing, or it may fire up a + * new socket listener after stopping an existing one. + * + * @param address address that the remote host said was ours + */ + void ourAddressReceived(String address) { + if (allowAddressUpdate()) { + int port = getPort(); + TCPAddress addr = new TCPAddress(address, port); + if (addr.getPort() > 0) { + if (allowAddress(addr)) { + if (_myAddress != null) { + if (addr.getAddress().equals(_myAddress.getAddress())) { + // ignore, since there is no change + return; + } + } + updateAddress(addr); + } + } else { + if (_log.shouldLog(Log.ERROR)) + _log.error("Address specified is not valid [" + address + ":" + port + "]"); + } + } + } + + public RouterAddress startListening() { + configureLocalAddress(); + if (_myAddress != null) { + _listener.startListening(); + return _myAddress.toRouterAddress(); + } else { + return null; + } } public void stopListening() { - if (_log.shouldLog(Log.ERROR)) - _log.error("Stop listening called! No more TCP", new Exception("Die tcp, die")); - _running = false; - - for (int i = 0; i < _listeners.size(); i++) { - TCPListener lsnr = (TCPListener)_listeners.get(i); - lsnr.stopListening(); + _listener.stopListening(); + } + + /** + * Should we listen to all interfaces, or just the one specified in + * our TCPAddress? + * + */ + boolean shouldListenToAllInterfaces() { + String val = getContext().getProperty(BIND_ALL_INTERFACES, "TRUE"); + return Boolean.valueOf(val).booleanValue(); + } + + private SimpleDateFormat _fmt = new SimpleDateFormat("dd MMM HH:mm:ss"); + + /** + * Add the given message to the list of most recent connection + * establishment error messages. This should include a timestamp of + * some sort in it. + * + */ + void addConnectionErrorMessage(String msg) { + synchronized (_fmt) { + msg = _fmt.format(new Date(_context.clock().now())) + ": " + msg; } - Set allCons = new HashSet(); - synchronized (_connections) { - for (Iterator iter = _connections.values().iterator(); iter.hasNext(); ) { - List cons = (List)iter.next(); - for (Iterator citer = cons.iterator(); citer.hasNext(); ) { - TCPConnection con = (TCPConnection)citer.next(); - allCons.add(con); - } - } - _connectionAddresses.clear(); - } - for (Iterator iter = allCons.iterator(); iter.hasNext(); ) { - TCPConnection con = (TCPConnection)iter.next(); - con.closeConnection(); + synchronized (_lastConnectionErrors) { + while (_lastConnectionErrors.size() >= MAX_ERR_MESSAGES) + _lastConnectionErrors.remove(0); + _lastConnectionErrors.add(msg); } } - public RouterIdentity getMyIdentity() { return _context.router().getRouterInfo().getIdentity(); } + TCPAddress getMyAddress() { return _myAddress; } + public String getStyle() { return STYLE; } + ConnectionTagManager getTagManager() { return _tagManager; } - void connectionClosed(TCPConnection con) { - if (_log.shouldLog(Log.INFO)) - _log.info("Connection closed with " + con.getRemoteRouterIdentity()); - StringBuffer buf = new StringBuffer(256); - buf.append("Still connected to: "); - synchronized (_connections) { - List cons = (List)_connections.get(con.getRemoteRouterIdentity()); - if ( (cons != null) && (cons.size() > 0) ) { - cons.remove(con); - long lifetime = con.getLifetime(); - if (_log.shouldLog(Log.INFO)) - _log.info("Connection closed (with remaining) after lifetime " + lifetime); - _context.statManager().addRateData("tcp.connectionLifetime", lifetime, 0); + /** + * Initialize the _myAddress var with our local address (if possible) + * + */ + private void configureLocalAddress() { + String addr = _context.getProperty(LISTEN_ADDRESS); + int port = getPort(); + if (port != -1) { + TCPAddress address = new TCPAddress(addr, port); + boolean ok = allowAddress(address); + if (ok) { + _myAddress = address; + } else { + if (_log.shouldLog(Log.ERROR)) + _log.error("External address " + addr + " is not valid"); } - Set toRemove = new HashSet(); - for (Iterator iter = _connections.keySet().iterator(); iter.hasNext();) { - RouterIdentity ident = (RouterIdentity)iter.next(); - List all = (List)_connections.get(ident); - if (all.size() > 0) - buf.append(ident.getHash().toBase64()).append(" "); - else - toRemove.add(ident); - } - for (Iterator iter = toRemove.iterator(); iter.hasNext(); ) { - _connections.remove(iter.next()); - } - } - - TCPAddress address = con.getRemoteAddress(); - if (address != null) { - synchronized (_connectionAddresses) { - _connectionAddresses.remove(address); - } - } - if (_log.shouldLog(Log.INFO)) - _log.info(buf.toString()); - //if (con.getRemoteRouterIdentity() != null) + } else { + if (_log.shouldLog(Log.ERROR)) + _log.error("External port is not valid"); + } } - boolean handleConnection(TCPConnection con, RouterInfo target) { - con.setTransport(this); - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Before establishing connection"); - TCPAddress remAddr = con.getRemoteAddress(); - if (remAddr != null) { - synchronized (_connectionAddresses) { - if (_connectionAddresses.containsKey(remAddr)) { - if (_log.shouldLog(Log.WARN)) - _log.warn("refusing connection from " + remAddr + " as it is a dup"); - con.closeConnection(); - return false; - } - } - - if (_tcpAddress.equals(remAddr)) { - if (_log.shouldLog(Log.WARN)) - _log.warn("refusing connection to ourselves..."); - _context.shitlist().shitlistRouter(target.getIdentity().getHash(), "Our old address"); - _context.netDb().fail(target.getIdentity().getHash()); - con.closeConnection(); + /** + * Is the given address a valid one that we could listen to? + * + */ + private boolean allowAddress(TCPAddress address) { + if (address == null) return false; + if ( (address.getPort() <= 0) || (address.getPort() > 65535) ) + return false; + if (!address.isPubliclyRoutable()) { + String allowLocal = _context.getProperty(LISTEN_ALLOW_LOCAL, "false"); + if (Boolean.valueOf(allowLocal).booleanValue()) { + return true; + } else { + if (_log.shouldLog(Log.ERROR)) + _log.error("External address " + address + " is not publicly routable"); return false; } } else { - //if (_log.shouldLog(Log.WARN)) - // _log.warn("Why do we not have a remoteAddress for " + con, new Exception("hrm")); + return true; } - - long start = _context.clock().now(); - RouterIdentity ident = con.establishConnection(); - long afterEstablish = _context.clock().now(); - long startRunning = 0; - - if (ident == null) { - _context.statManager().updateFrequency("tcp.acceptFailureFrequency"); - con.closeConnection(); - return false; - } - - if (ident.equals(_context.router().getRouterInfo().getIdentity())) { - if (_log.shouldLog(Log.WARN)) - _log.warn("Dropping established connection with *cough* ourselves: listenHost=[" - + _tcpAddress.getHost() + "] listenPort=[" +_tcpAddress.getPort()+ "] remoteHost=[" - + remAddr.getHost() + "] remPort=[" + remAddr.getPort() + "]"); - con.closeConnection(); - return false; - } - - if (_log.shouldLog(Log.INFO)) - _log.info("Connection established with " + ident + " after " + (afterEstablish-start) + "ms"); - if (target != null) { - if (!target.getIdentity().equals(ident)) { - //_context.statManager().updateFrequency("tcp.acceptFailureFrequency"); - if (_log.shouldLog(Log.WARN)) - _log.warn("Target changed identities! was " + target.getIdentity().getHash().toBase64() + ", now is " + ident.getHash().toBase64() + "!"); - // remove the old ref, since they likely just created a new identity - _context.netDb().fail(target.getIdentity().getHash()); - _context.shitlist().shitlistRouter(target.getIdentity().getHash(), "Peer changed identities"); - //return false; - } else { - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Target is the same as who we connected with"); - } - } - if (ident != null) { - Set toClose = new HashSet(4); - List toAdd = new ArrayList(1); - List cons = null; - synchronized (_connections) { - if (!_connections.containsKey(ident)) - _connections.put(ident, new ArrayList(2)); - cons = (List)_connections.get(ident); - if (cons.size() > 0) { - if (_log.shouldLog(Log.WARN)) { - _log.warn("Attempted to open additional connections with " + ident.getHash() + ": closing older connections", new Exception("multiple cons")); + } - StringBuffer buf = new StringBuffer(128); - if (remAddr == null) - remAddr = con.getRemoteAddress(); - buf.append("Connection address: [").append(remAddr.toString()).append(']'); - synchronized (_connectionAddresses) { - if (_connectionAddresses.containsKey(remAddr)) { - buf.append(" NOT KNOWN in: "); - } else { - buf.append(" KNOWN IN: "); - } - for (Iterator iter = _connectionAddresses.keySet().iterator(); iter.hasNext(); ) { - TCPAddress curAddr = (TCPAddress)iter.next(); - buf.append('[').append(curAddr.toString()).append("] "); - } - } - _log.warn(buf.toString()); - } - - while (cons.size() > 0) { - TCPConnection oldCon = (TCPConnection)cons.remove(0); - toAdd.addAll(oldCon.getPendingMessages()); - toClose.add(oldCon); - } - } - cons.add(con); - - Set toRemove = new HashSet(); - for (Iterator iter = _connections.keySet().iterator(); iter.hasNext();) { - RouterIdentity cur = (RouterIdentity)iter.next(); - List all = (List)_connections.get(cur); - if (all.size() <= 0) - toRemove.add(ident); - } - for (Iterator iter = toRemove.iterator(); iter.hasNext(); ) { - _connections.remove(iter.next()); - } - } - - synchronized (_connectionAddresses) { - _connectionAddresses.put(con.getRemoteAddress(), cons); - } - - if (toAdd.size() > 0) { - for (Iterator iter = toAdd.iterator(); iter.hasNext(); ) { - OutNetMessage msg = (OutNetMessage)iter.next(); - con.addMessage(msg); - } - if (_log.shouldLog(Log.INFO)) - _log.info("Transferring " + toAdd.size() + " messages from old cons to the newly established con"); - } - - _context.shitlist().unshitlistRouter(ident.getHash()); - con.runConnection(); - startRunning = _context.clock().now(); - - if (toClose.size() > 0) { - for (Iterator iter = toClose.iterator(); iter.hasNext(); ) { - TCPConnection oldCon = (TCPConnection)iter.next(); - if (_log.shouldLog(Log.INFO)) - _log.info("Closing old duplicate connection " + oldCon.toString(), new Exception("Closing old con")); - oldCon.closeConnection(); - _context.statManager().addRateData("tcp.connectionLifetime", oldCon.getLifetime(), 0); - } - } - long done = _context.clock().now(); - - long diff = done - start; - if ( (diff > 3*1000) && (_log.shouldLog(Log.WARN)) ) { - _log.warn("handleConnection took too long: " + diff + "ms with " + - (afterEstablish-start) + "ms to establish " + - (startRunning-afterEstablish) + "ms to start running " + - (done-startRunning) + "ms to cleanup"); - } - if (_log.shouldLog(Log.DEBUG)) - _log.debug("runConnection called on the con"); - } + /** + * Blocking call to unconditionally update our listening address to the + * one specified, updating the routerInfo, etc. + * + */ + private void updateAddress(TCPAddress addr) { + RouterAddress routerAddr = addr.toRouterAddress(); + _myAddress = addr; + _listener.stopListening(); + _listener.startListening(); - _context.statManager().updateFrequency("tcp.acceptSuccessFrequency"); - return true; - } - - public String getStyle() { return STYLE; } - - public String renderStatusHTML() { - StringBuffer buf = new StringBuffer(); - Map cons = new HashMap(); - synchronized (_connections) { - cons.putAll(_connections); - } - int established = 0; - buf.append("TCP Transport (").append(cons.size()).append(" connections)
\n"); - buf.append("
    "); - for (Iterator iter = cons.keySet().iterator(); iter.hasNext(); ) { - buf.append("
  • "); - RouterIdentity ident = (RouterIdentity)iter.next(); - List curCons = (List)cons.get(ident); - buf.append("Connection to ").append(ident.getHash().toBase64()).append(": "); - String lifetime = null; - for (int i = 0; i < curCons.size(); i++) { - TCPConnection con = (TCPConnection)curCons.get(i); - if (con.getLifetime() > 30*1000) { - established++; - lifetime = DataHelper.formatDuration(con.getLifetime()); - } + Set addresses = getCurrentAddresses(); + List toRemove = null; + for (Iterator iter = addresses.iterator(); iter.hasNext(); ) { + RouterAddress cur = (RouterAddress)iter.next(); + if (STYLE.equals(cur.getTransportStyle())) { + if (toRemove == null) + toRemove = new ArrayList(1); + toRemove.add(cur); } - if (lifetime != null) - buf.append(lifetime); - else - buf.append("[pending]"); - - buf.append("
  • \n"); } - buf.append("
\n"); + if (toRemove != null) { + for (int i = 0; i < toRemove.size(); i++) { + addresses.remove(toRemove.get(i)); + } + } + addresses.add(routerAddr); - if (established == 0) { - buf.append("No TCP connections
    "); - buf.append("
  • Is your publicly reachable IP address / hostname ").append(_listenHost).append("?
  • \n"); - buf.append("
  • Is your firewall / NAT open to receive connections on port ").append(_listenPort).append("?
  • \n"); - buf.append("
  • Do you have any reachable peer references (see down below for \"Routers\", "); - buf.append(" or check your netDb directory - you want at least two routers, since one of them is your own)
  • \n"); - buf.append("
\n"); - } - return buf.toString(); + _context.router().rebuildRouterInfo(); + + _listener.startListening(); } /** - * only establish one connection at a time, and if multiple requests are pooled - * for the same one, once one is established send all the messages through + * Determine whether we should listen to the peer when they give us what they + * say our IP address is. We should allow a peer to specify our IP address + * if and only if we have not configured our own address explicitly and we + * have no fully established connections. * */ - private class ConnEstablisher implements Runnable { - private int _id; + private boolean allowAddressUpdate() { + boolean addressSpecified = (null != _context.getProperty(LISTEN_ADDRESS)); + if (addressSpecified) + return false; + int connectedPeers = countActivePeers(); + return (connectedPeers == 0); + } - public ConnEstablisher(int id) { - _id = id; - } + /** + * What port should we be reachable on? + * + * @return the port number, or -1 if there is no valid port + */ + private int getPort() { + if ( (_myAddress != null) && (_myAddress.getPort() > 0) ) + return _myAddress.getPort(); - public int getId() { return _id; } - - public void run() { - while (_running) { - try { - PendingMessages pending = nextPeer(this); - - long start = _context.clock().now(); - - if (_log.shouldLog(Log.INFO)) - _log.info("Beginning establishment with " + pending.getPeer().toBase64() + " [not error]"); - - TCPConnection con = getConnection(pending.getPeerInfo().getIdentity()); - long conFetched = _context.clock().now(); - long sentPending = 0; - long establishedCon = 0; - long refetchedCon = 0; - long sentRefetched = 0; - long failedPending = 0; - - if (con != null) { - sendPending(con, pending); - sentPending = _context.clock().now(); - } else { - boolean established = establishConnection(pending.getPeerInfo()); - establishedCon = _context.clock().now(); - if (established) { - _context.statManager().updateFrequency("tcp.attemptSuccessFrequency"); - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Connection established"); - con = getConnection(pending.getPeerInfo().getIdentity()); - refetchedCon = _context.clock().now(); - if (con == null) { - if (_log.shouldLog(Log.ERROR)) - _log.error("Connection established to " + pending.getPeer().toBase64() + " but they aren't who we wanted"); - _context.shitlist().shitlistRouter(pending.getPeer(), "Old address of a new peer"); - failPending(pending); - } else { - _context.shitlist().unshitlistRouter(pending.getPeer()); - sendPending(con, pending); - sentRefetched = _context.clock().now(); - } - } else { - _context.statManager().updateFrequency("tcp.attemptFailureFrequency"); - if (_log.shouldLog(Log.INFO)) - _log.info("Unable to establish a connection to " + pending.getPeer()); - failPending(pending); - - // shitlisted by establishConnection with a more detailed reason - //_context.shitlist().shitlistRouter(pending.getPeer(), "Unable to contact host"); - //ProfileManager.getInstance().commErrorOccurred(pending.getPeer()); - failedPending = _context.clock().now(); - } - } - - long end = _context.clock().now(); - long diff = end - start; - - StringBuffer buf = new StringBuffer(128); - buf.append("Time to establish with ").append(pending.getPeer().toBase64()).append(": ").append(diff).append("ms"); - buf.append(" fetched: ").append(conFetched-start).append(" ms"); - if (sentPending != 0) - buf.append(" sendPending: ").append(sentPending - conFetched).append("ms"); - if (establishedCon != 0) { - buf.append(" established: ").append(establishedCon - conFetched).append("ms"); - if (refetchedCon != 0) { - buf.append(" refetched: ").append(refetchedCon - establishedCon).append("ms"); - if (sentRefetched != 0) { - buf.append(" sentRefetched: ").append(sentRefetched - refetchedCon).append("ms"); - } - } else { - buf.append(" failedPending: ").append(failedPending - establishedCon).append("ms"); - } - } - if (diff > 6000) { - if (_log.shouldLog(Log.WARN)) - _log.warn(buf.toString()); - } else { - if (_log.shouldLog(Log.INFO)) - _log.info(buf.toString()); - } - } catch (Throwable t) { - if (_log.shouldLog(Log.CRIT)) - _log.log(Log.CRIT, "Error in connection establisher thread - NO MORE CONNECTIONS", t); - } + String port = _context.getProperty(LISTEN_PORT); + if (port != null) { + try { + int portNum = Integer.parseInt(port); + if ( (portNum >= 1) && (portNum < 65535) ) + return portNum; + } catch (NumberFormatException nfe) { + // fallthrough } + } + + return -1; + } + + + /** + * How many peers can we talk to right now? + * + */ + public int countActivePeers() { + synchronized (_connectionLock) { + return _connectionsByIdent.size(); } } /** - * Add a new message to the outbound pool to be established asap (may be sent - * along existing connections if they appear later) + * The transport is done sending this message. This exposes the + * superclass's protected method to the current package. * + * @param msg message in question + * @param sendSuccessful true if the peer received it + * @param msToSend how long it took to transfer the data to the peer + * @param allowRequeue true if we should try other transports if available */ - public void addPending(OutNetMessage msg) { - synchronized (_msgs) { - Hash target = msg.getTarget().getIdentity().getHash(); - PendingMessages msgs = (PendingMessages)_msgs.get(target); - if (msgs == null) { - msgs = new PendingMessages(msg.getTarget()); - msgs.addPending(msg); - _msgs.put(target, msgs); - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Adding a pending to new " + target.toBase64()); - } else { - msgs.addPending(msg); - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Adding a pending to existing " + target.toBase64()); - } - int level = Log.INFO; - if (msgs.getMessageCount() > 2) - level = Log.WARN; - if (_log.shouldLog(level)) - _log.log(level, "Add message to " + target.toBase64() + ", making a total of " + msgs.getMessageCount() + " for them, with another " + (_msgs.size() -1) + " peers pending establishment"); - _msgs.notifyAll(); - } - msg.timestamp("TCPTransport.addPending finished and notified"); + public void afterSend(OutNetMessage msg, boolean sendSuccessful, boolean allowRequeue, long msToSend) { + super.afterSend(msg, sendSuccessful, allowRequeue, msToSend); } /** - * blocking call to claim the next available targeted peer. does a wait on - * the _msgs pool which should be notified from addPending. + * Blocking call to retrieve the next peer that we want to establish a + * connection with. * */ - private PendingMessages nextPeer(ConnEstablisher establisher) { - PendingMessages rv = null; + RouterInfo getNextPeer() { while (true) { - synchronized (_msgs) { - if (_msgs.size() <= 0) { - try { _msgs.wait(); } catch (InterruptedException ie) {} - } - if (_msgs.size() > 0) { - for (Iterator iter = _msgs.keySet().iterator(); iter.hasNext(); ) { - Object key = iter.next(); - rv = (PendingMessages)_msgs.get(key); - if (!rv.setEstablisher(establisher)) { - // unable to claim this peer - if (_log.shouldLog(Log.INFO)) - _log.info("Peer is still in process: " + rv.getPeer() + " on establisher " + rv.getEstablisher().getId()); - rv = null; - } else { - if (_log.shouldLog(Log.INFO)) - _log.info("Returning next peer " + rv.getPeer().toBase64()); - return rv; - } - } - // all of the messages refer to a connection being established - try { _msgs.wait(); } catch (InterruptedException ie) {} + synchronized (_connectionLock) { + for (Iterator iter = _pendingMessages.keySet().iterator(); iter.hasNext(); ) { + Hash peer = (Hash)iter.next(); + List msgs = (List)_pendingMessages.get(peer); + if (_pendingConnectionsByIdent.contains(peer)) + continue; // we're already trying to talk to them + + if (msgs.size() <= 0) + continue; // uh... + OutNetMessage msg = (OutNetMessage)msgs.get(0); + RouterAddress addr = msg.getTarget().getTargetAddress(STYLE); + TCPAddress tcpAddr = new TCPAddress(addr); + if (tcpAddr.getPort() <= 0) + continue; // invalid + if (_pendingConnectionsByAddress.contains(tcpAddr.toString())) + continue; // we're already trying to talk to someone at their address + + // ok, this is someone we can try to contact. mark it as ours. + _pendingConnectionsByIdent.add(peer); + _pendingConnectionsByAddress.add(tcpAddr.toString()); + return msg.getTarget(); } + + try { + _connectionLock.wait(); + } catch (InterruptedException ie) {} } } + } + + /** Called after an establisher finished (or failed) connecting to the peer */ + void establishmentComplete(RouterInfo info) { + TCPAddress addr = new TCPAddress(info.getTargetAddress(STYLE)); + Hash peer = info.getIdentity().calculateHash(); + List msgs = null; + synchronized (_connectionLock) { + _pendingConnectionsByAddress.remove(addr.toString()); + _pendingConnectionsByIdent.remove(peer); + + msgs = (List)_pendingMessages.remove(peer); + } - } - - /** - * Send all the messages targetting the given location - * over the established connection - * - */ - private void sendPending(TCPConnection con, PendingMessages pending) { - if (con == null) { - if (_log.shouldLog(Log.ERROR)) - _log.error("Send pending to null con?", new Exception("Hmm")); - return; - } - if (pending == null) { - if (_log.shouldLog(Log.ERROR)) - _log.error("Null pending, 'eh?", new Exception("Hmm..")); - return; - } - if (_log.shouldLog(Log.INFO)) - _log.info("Connection established, now queueing up " + pending.getMessageCount() + " messages to be sent"); - synchronized (_msgs) { - _msgs.remove(pending.getPeer()); - } - if (pending.getPeer().equals(con.getRemoteRouterIdentity().calculateHash())) { - OutNetMessage msg = null; - while ( (msg = pending.getNextMessage()) != null) { - msg.timestamp("TCPTransport.sendPending to con.addMessage"); - con.addMessage(msg); - } - } else { - // we connected to someone we didn't try to connect to... - OutNetMessage msg = null; - while ( (msg = pending.getNextMessage()) != null) { - msg.timestamp("TCPTransport.sendPending connected to a different peer"); - afterSend(msg, false, false); - } - } - } - - /** - * Fail out all messages pending to the specified peer - */ - private void failPending(PendingMessages pending) { - if (pending != null) { - synchronized (_msgs) { - _msgs.remove(pending.getPeer()); - } - - OutNetMessage msg = null; - while ( (msg = pending.getNextMessage()) != null) { + if (msgs != null) { + // messages are only available if the connection failed (since + // connectionEstablished clears them otherwise) + for (int i = 0; i < msgs.size(); i++) { + OutNetMessage msg = (OutNetMessage)msgs.get(i); afterSend(msg, false); } } } - /** - * Coordinate messages for a particular peer that hasn't been established yet - * - */ - private static class PendingMessages { - private List _messages; - private Hash _peer; - private RouterInfo _peerInfo; - private ConnEstablisher _establisher; - - public PendingMessages(RouterInfo peer) { - _messages = new ArrayList(2); - _peerInfo = peer; - _peer = peer.getIdentity().getHash(); - _establisher = null; + /** Make this stuff pretty (only used in the old console) */ + public String renderStatusHTML() { + StringBuffer buf = new StringBuffer(1024); + buf.append("Connections:
    \n"); + synchronized (_connectionLock) { + for (Iterator iter = _connectionsByIdent.values().iterator(); iter.hasNext(); ) { + TCPConnection con = (TCPConnection)iter.next(); + buf.append("
  • "); + buf.append(con.getRemoteRouterIdentity().getHash().toBase64().substring(0,6)); + buf.append(": up for ").append(DataHelper.formatDuration(con.getLifetime())); + buf.append("
  • \n"); + } + buf.append("
\n"); + + buf.append("Connections being built:
    \n"); + for (Iterator iter = _pendingConnectionsByIdent.iterator(); iter.hasNext(); ) { + Hash peer = (Hash)iter.next(); + buf.append("
  • "); + buf.append(peer.toBase64().substring(0,6)); + buf.append("
  • \n"); + } + buf.append("
\n"); } - /** - * Claim a peer for a specific establisher - * - * @return true if the claim was successful, false if someone beat us to it - */ - public boolean setEstablisher(ConnEstablisher establisher) { - synchronized (PendingMessages.this) { - if (_establisher == null) { - _establisher = establisher; - return true; - } else { - return false; - } + buf.append("Most recent connection errors:
    "); + synchronized (_lastConnectionErrors) { + for (int i = _lastConnectionErrors.size()-1; i >= 0; i--) { + String msg = (String)_lastConnectionErrors.get(i); + buf.append("
  • ").append(msg).append("
  • \n"); } } - public ConnEstablisher getEstablisher() { - return _establisher; - } + buf.append("
"); - /** - * Add a new message to this to-be-established connection - */ - public void addPending(OutNetMessage msg) { - synchronized (_messages) { - _messages.add(msg); - } - } - - /** - * Get the next message queued up for delivery on this connection being established - * - */ - public OutNetMessage getNextMessage() { - synchronized (_messages) { - if (_messages.size() <= 0) - return null; - else - return (OutNetMessage)_messages.remove(0); - } - } - - /** - * Get the number of messages queued up for this to be established connection - * - */ - public int getMessageCount() { - synchronized (_messages) { - return _messages.size(); - } - } - - /** who are we going to establish with? */ - public Hash getPeer() { return _peer; } - /** who are we going to establish with? */ - public RouterInfo getPeerInfo() { return _peerInfo; } + return buf.toString(); } + } diff --git a/router/java/src/net/i2p/router/transport/tcp/package.html b/router/java/src/net/i2p/router/transport/tcp/package.html new file mode 100644 index 0000000000..b2249087bc --- /dev/null +++ b/router/java/src/net/i2p/router/transport/tcp/package.html @@ -0,0 +1,140 @@ + +

Implements the transport for communicating with other routers via TCP/IP.

+ +

Connection protocol

+ +

The protocol used to establish the connection between the peers is +implemented in the {@link net.i2p.router.transport.tcp.ConnectionBuilder} +for "Alice", the initiator, and in +{@link net.i2p.router.transport.tcp.ConnectionHandler} for "Bob", the +receiving peer. (+ implies concatenation)

+ +

Common case:

+

1) Alice to Bob:
+ #bytesFollowing + #versions + v1 [+ v2 [etc]] + tag? + tagData + properties

+

2) Bob to Alice:
+ #bytesFollowing + versionOk + #bytesIP + IP + tagOk? + nonce + properties

+ +
    +
  • #bytesFollowing is a 2 byte unsigned integer specifying how many + bytes there are (after the current pair) in the line sent. 0xFFFF is reserved
  • +
  • #versions is a 1 byte unsigned integer specifying how many + acceptable 1 byte version numbers follow (preferred value first).
  • +
  • v1 (etc) is a 1 byte unsigned integer specifying a protocol + version. The value 0x0 is not allowed.
  • +
  • tag? is a 1 byte value specifying whether a tag follows - 0x0 means + no tag follows, 0x1 means a 32 byte tag follows.
  • +
  • tagData is a 32 byte tag, if necessary
  • +
  • properties is a name=value mapping, formatted as the other I2P + mappings (via {@link net.i2p.data.DataHelper#readProperties})
  • +
  • versionOk is a 1 byte value specifying the protocol version + that is agreed upon, or 0x0 if no compatible protocol versions are available.
  • +
  • #bytesIP is a 2 byte unsigned integer specifying how many bytes + following make up the IP address
  • +
  • IP is made up of #bytesIP bytes formatting the + peer who established the connection's IP address as a string (e.g. "192.168.1.1")
  • +
  • tagOk? is a 1 byte value specifying whether the tag provided + is available for use - 0x0 means no, 0x1 means yes.
  • +
  • nonce is a 4 byte random value
  • +
+ +

Whether or not the tagData is specified by Alice and is accepted +by Bob determines which of the scenarios below are used. In addition, the IP +address provided by Bob gives Alice the opportunity to fire up a socket listener +on that interface and include it in her list of reachable addresses. The +properties mappings are left for future expansion.

+ +

Connection establishment with a valid tag:

+

With a valid tag and nonce received, both Alice and +Bob load up the previously negotiated sessionKey and set the +iv to the first 16 bytes of H(tag + nonce). The +remainder of the communication is AES256 encrypted per +{@link net.i2p.crypto.AESInputStream} and {@link net.i2p.crypto.AESOutputStream}

+ +

3) Alice to Bob:
+ H(nonce)

+

4) Bob to Alice:
+ H(tag)

+

5) If the hashes are not correct, disconnect immediately and do not + consume the tag

+

6) Alice to Bob:
+ routerInfo + currentTime + H(routerInfo + currentTime + nonce + tag)

+

7) Bob should now verify that he can establish a connection to her through one of the + routerAddresses specified in her RouterInfo. The testing process is described below.

+

8) Bob to Alice:
+ routerInfo + status + properties + H(routerInfo + status + properties + nonce + tag)

+

9) If the status is ok, both Alice and Bob consume the + tagData, updating the next tag to be H(E(nonce + tag, sessionKey)). + Otherwise, both sides disconnect and do not consume the tag. In addition, on error the + properties mapping has a more detailed reason under the key "MESSAGE".

+ +
    +
  • H(x) is the SHA256 hash of x, formatted per {@link net.i2p.data.Hash#writeBytes}.
  • +
  • routerInfo is the serialization of the local router's info + per {@link net.i2p.data.RouterInfo#writeBytes}.
  • +
  • currentTime is what the local router thinks the current network time + is, formatted per {@link net.i2p.data.DataHelper#writeDate}.
  • +
  • status is a 1 byte value:
      +
    • 0x0 means OK
    • +
    • 0x1 means Alice was not reachable
    • +
    • 0x2 means the clock was skewed (Bob's current time may be available + in the properties mapping under "SKEW", formatted as "yyyyMMddhhmmssSSS", + per {@link java.text.SimpleDateFormat}).
    • +
    • 0x3 means the signature is invalid (only used by steps 9 and 11 below)
    • +
    • Other values are currently undefined (yet fatal) errors
    • +
  • +
+ +

Connection establishment without a vald tag:

+ +

3) Alice to Bob
+ X

+

4) Bob to Alice
+ Y

+

5) Both sides complete the Diffie-Hellman exchange, setting the + sessionKey to the first 32 bytes of the result (e.g. (X^y mod p)), + iv to the next 16 bytes, and the nextTag to the 32 + bytes after that. The rest of the data is AES256 encrypted with those settings per + {@link net.i2p.crypto.AESInputStream} and {@link net.i2p.crypto.AESOutputStream}

+

6) Alice to Bob
+ H(nonce)

+

7) Bob to Alice
+ H(nextTag)

+

8) If they disagree, disconnect immediately and do not persist the tags or keys

+

9) Alice to Bob
+ routerInfo + currentTime + + S(routerInfo + currentTime + nonce + nextTag, routerIdent.signingKey)

+

10) Bob should now verify that he can establish a connection to her through one of the + routerAddresses specified in her RouterInfo. The testing process is described below.

+

11) Bob to Alice
+ routerInfo + status + properties + + S(routerInfo + status + properties + nonce + nextTag, routerIdent.signingKey)

+

12) If the signature matches on both sides and status is ok, both sides + save the sessionKey negotiated as well as the nextTag. + Otherwise, the keys and tags are discarded and both sides drop the connection.

+ +
    +
  • X is a 256 byte unsigned integer in 2s complement, representing + g^x mod p (where g and p are defined + in {@link net.i2p.crypto.CryptoConstants} and x is a randomly chosen value
  • +
  • Y is a 256 byte unsigned integer in 2s complement, representing + g^y mod p (where g and p are defined + in {@link net.i2p.crypto.CryptoConstants} and y is a randomly chosen value
  • +
  • S(val, key) is the DSA signature of the val using the + given signing key (in this case, the router's signing keys to provide + authentication that they are who they say they are). The signature is formatted + per {@link net.i2p.data.Signature}.
  • +
+ +

Peer testing

+

As mentioned in steps 7 and 10 above, Bob should verify that Alice is reachable +to prevent a restricted route from being formed (he may decide not to do this once +I2P supports restricted routes)

+ +

1) Bob to Alice
+ 0xFFFF + #versions + v1 [+ v2 [etc]] + properties

+

2) Alice to Bob
+ 0xFFFF + versionOk + #bytesIP + IP + currentTime + properties

+

3) Both sides close the socket

+ +