propagate from branch 'i2p.i2p.zzz.test' (head fa1d7d3151cb0b03dde308766d3d350afda8f14a)

to branch 'i2p.i2p' (head 8cb6295e6a3492fd3b93366bfb0ebf231115fa85)
This commit is contained in:
zzz
2008-11-20 15:53:30 +00:00
38 changed files with 244 additions and 4732 deletions

View File

@ -19,7 +19,10 @@
<target name="distclean" depends="clean" /> <target name="distclean" depends="clean" />
<target name="depend"> <condition property="depend.available">
<typefound name="depend" />
</condition>
<target name="depend" if="depend.available">
<depend <depend
cache="../../build" cache="../../build"
srcdir="${src}" srcdir="${src}"

View File

@ -7,7 +7,10 @@
<ant dir="../../streaming/java/" target="build" /> <ant dir="../../streaming/java/" target="build" />
<!-- streaming will build ministreaming and core --> <!-- streaming will build ministreaming and core -->
</target> </target>
<target name="depend"> <condition property="depend.available">
<typefound name="depend" />
</condition>
<target name="depend" if="depend.available">
<depend <depend
cache="../../../build" cache="../../../build"
srcdir="./src" srcdir="./src"

View File

@ -7,7 +7,10 @@
<ant dir="../../jetty/" target="build" /> <ant dir="../../jetty/" target="build" />
<!-- ministreaming will build core --> <!-- ministreaming will build core -->
</target> </target>
<target name="depend"> <condition property="depend.available">
<typefound name="depend" />
</condition>
<target name="depend" if="depend.available">
<depend <depend
cache="../../../build" cache="../../../build"
srcdir="./src" srcdir="./src"

View File

@ -41,11 +41,11 @@ public class EditBean extends IndexBean {
if (tun != null) if (tun != null)
return tun.getTargetHost(); return tun.getTargetHost();
else else
return ""; return "127.0.0.1";
} }
public String getTargetPort(int tunnel) { public String getTargetPort(int tunnel) {
TunnelController tun = getController(tunnel); TunnelController tun = getController(tunnel);
if (tun != null) if (tun != null && tun.getTargetPort() != null)
return tun.getTargetPort(); return tun.getTargetPort();
else else
return ""; return "";
@ -59,7 +59,7 @@ public class EditBean extends IndexBean {
} }
public String getPrivateKeyFile(int tunnel) { public String getPrivateKeyFile(int tunnel) {
TunnelController tun = getController(tunnel); TunnelController tun = getController(tunnel);
if (tun != null) if (tun != null && tun.getPrivKeyFile() != null)
return tun.getPrivKeyFile(); return tun.getPrivKeyFile();
else else
return ""; return "";
@ -266,4 +266,4 @@ public class EditBean extends IndexBean {
} }
return props; return props;
} }
} }

View File

@ -96,6 +96,7 @@ public class IndexBean {
_curNonce = -1; _curNonce = -1;
} }
} }
public void setPassphrase(String phrase) { public void setPassphrase(String phrase) {
_passphrase = phrase; _passphrase = phrase;
} }
@ -332,15 +333,15 @@ public class IndexBean {
public String getTunnelName(int tunnel) { public String getTunnelName(int tunnel) {
TunnelController tun = getController(tunnel); TunnelController tun = getController(tunnel);
if (tun != null) if (tun != null && tun.getName() != null)
return tun.getName(); return tun.getName();
else else
return ""; return "New Tunnel";
} }
public String getClientPort(int tunnel) { public String getClientPort(int tunnel) {
TunnelController tun = getController(tunnel); TunnelController tun = getController(tunnel);
if (tun != null) if (tun != null && tun.getListenPort() != null)
return tun.getListenPort(); return tun.getListenPort();
else else
return ""; return "";
@ -389,7 +390,7 @@ public class IndexBean {
public String getTunnelDescription(int tunnel) { public String getTunnelDescription(int tunnel) {
TunnelController tun = getController(tunnel); TunnelController tun = getController(tunnel);
if (tun != null) if (tun != null && tun.getDescription() != null)
return tun.getDescription(); return tun.getDescription();
else else
return ""; return "";
@ -406,7 +407,12 @@ public class IndexBean {
public String getClientDestination(int tunnel) { public String getClientDestination(int tunnel) {
TunnelController tun = getController(tunnel); TunnelController tun = getController(tunnel);
if (tun == null) return ""; if (tun == null) return "";
if ("client".equals(tun.getType())||"ircclient".equals(tun.getType())) return tun.getTargetDestination(); if ("client".equals(tun.getType())||"ircclient".equals(tun.getType())) {
if (tun.getTargetDestination() != null)
return tun.getTargetDestination();
else
return "";
}
else return tun.getProxyList(); else return tun.getProxyList();
} }

View File

@ -80,6 +80,10 @@
<div id="portField" class="rowItem"> <div id="portField" class="rowItem">
<label for="port" accesskey="P"> <label for="port" accesskey="P">
<span class="accessKey">P</span>ort: <span class="accessKey">P</span>ort:
<% String value = editBean.getClientPort(curTunnel);
if (value == null || "".equals(value.trim()))
out.write(" <font color=\"red\">(required)</font>");
%>
</label> </label>
<input type="text" size="6" maxlength="5" id="port" name="port" title="Access Port Number" value="<%=editBean.getClientPort(curTunnel)%>" class="freetext" /> <input type="text" size="6" maxlength="5" id="port" name="port" title="Access Port Number" value="<%=editBean.getClientPort(curTunnel)%>" class="freetext" />
</div> </div>
@ -123,6 +127,10 @@
%><div id="destinationField" class="rowItem"> %><div id="destinationField" class="rowItem">
<label for="targetDestination" accesskey="T"> <label for="targetDestination" accesskey="T">
<span class="accessKey">T</span>unnel Destination: <span class="accessKey">T</span>unnel Destination:
<% String value2 = editBean.getClientDestination(curTunnel);
if (value2 == null || "".equals(value2.trim()))
out.write(" <font color=\"red\">(required)</font>");
%>
</label> </label>
<input type="text" size="30" id="targetDestination" name="targetDestination" title="Destination of the Tunnel" value="<%=editBean.getClientDestination(curTunnel)%>" class="freetext" /> <input type="text" size="30" id="targetDestination" name="targetDestination" title="Destination of the Tunnel" value="<%=editBean.getClientDestination(curTunnel)%>" class="freetext" />
<span class="comment">(name or destination)</span> <span class="comment">(name or destination)</span>

View File

@ -93,6 +93,10 @@
<div id="portField" class="rowItem"> <div id="portField" class="rowItem">
<label for="targetPort" accesskey="P"> <label for="targetPort" accesskey="P">
<span class="accessKey">P</span>ort: <span class="accessKey">P</span>ort:
<% String value = editBean.getTargetPort(curTunnel);
if (value == null || "".equals(value.trim()))
out.write(" <font color=\"red\">(required)</font>");
%>
</label> </label>
<input type="text" size="6" maxlength="5" id="targetPort" name="targetPort" title="Target Port Number" value="<%=editBean.getTargetPort(curTunnel)%>" class="freetext" /> <input type="text" size="6" maxlength="5" id="targetPort" name="targetPort" title="Target Port Number" value="<%=editBean.getTargetPort(curTunnel)%>" class="freetext" />
</div> </div>
@ -112,6 +116,10 @@
%><div id="privKeyField" class="rowItem"> %><div id="privKeyField" class="rowItem">
<label for="privKeyFile" accesskey="k"> <label for="privKeyFile" accesskey="k">
Private <span class="accessKey">k</span>ey file: Private <span class="accessKey">k</span>ey file:
<% String value2 = editBean.getPrivateKeyFile(curTunnel);
if (value2 == null || "".equals(value2.trim()))
out.write(" <font color=\"red\">(required)</font>");
%>
</label> </label>
<input type="text" size="30" id="privKeyFile" name="privKeyFile" title="Path to Private Key File" value="<%=editBean.getPrivateKeyFile(curTunnel)%>" class="freetext" /> <input type="text" size="30" id="privKeyFile" name="privKeyFile" title="Path to Private Key File" value="<%=editBean.getPrivateKeyFile(curTunnel)%>" class="freetext" />
</div> </div>

View File

@ -5,6 +5,7 @@
<property name="jetty.md5" value="a61adc832be6baf2678935506743cfc3" /> <property name="jetty.md5" value="a61adc832be6baf2678935506743cfc3" />
<property name="jetty.url" value="http://dist.codehaus.org/jetty/jetty-5.1.x/jetty-5.1.12.zip" /> <property name="jetty.url" value="http://dist.codehaus.org/jetty/jetty-5.1.x/jetty-5.1.12.zip" />
<property name="jetty.filename" value="jetty-5.1.12.zip" /> <property name="jetty.filename" value="jetty-5.1.12.zip" />
<property name="verified.filename" value="verified.txt" />
<target name="all" depends="build" /> <target name="all" depends="build" />
@ -41,7 +42,9 @@
<get src="${jetty.url}" verbose="true" dest="${jetty.filename}" /> <get src="${jetty.url}" verbose="true" dest="${jetty.filename}" />
</target> </target>
<target name="verifyJettylib" > <uptodate property="verified.already" srcfile="${jetty.filename}" targetfile="${verified.filename}" />
<target name="verifyJettylib" unless="verified.already" >
<condition property="jetty.zip.verified" > <condition property="jetty.zip.verified" >
<and> <and>
<checksum file="${jetty.filename}" algorithm="SHA" property="${jetty.sha1}" /> <checksum file="${jetty.filename}" algorithm="SHA" property="${jetty.sha1}" />
@ -55,6 +58,7 @@
</not> </not>
</condition> </condition>
</fail> </fail>
<touch file="${verified.filename}" />
</target> </target>
<target name="extractJettylib" unless="jetty.zip.extracted" > <target name="extractJettylib" unless="jetty.zip.extracted" >
@ -97,6 +101,7 @@
</target> </target>
<target name="clean" > <target name="clean" >
<delete dir="./build" /> <delete dir="./build" />
<delete file="${verified.filename}" />
</target> </target>
<target name="cleandep" depends="clean" /> <target name="cleandep" depends="clean" />
<target name="distclean" depends="clean"> <target name="distclean" depends="clean">

View File

@ -5,7 +5,10 @@
<target name="builddep"> <target name="builddep">
<ant dir="../../../core/java/" target="build" /> <ant dir="../../../core/java/" target="build" />
</target> </target>
<target name="depend"> <condition property="depend.available">
<typefound name="depend" />
</condition>
<target name="depend" if="depend.available">
<depend <depend
cache="../../../build" cache="../../../build"
srcdir="./src" srcdir="./src"

View File

@ -10,7 +10,10 @@
<target name="prepare"> <target name="prepare">
<ant dir="../../jetty/" target="build" /> <ant dir="../../jetty/" target="build" />
</target> </target>
<target name="depend"> <condition property="depend.available">
<typefound name="depend" />
</condition>
<target name="depend" if="depend.available">
<depend <depend
cache="../../../build" cache="../../../build"
srcdir="./src" srcdir="./src"
@ -27,6 +30,8 @@
<pathelement location="../../jrobin/jrobin-1.4.0.jar" /> <pathelement location="../../jrobin/jrobin-1.4.0.jar" />
</classpath> </classpath>
</depend> </depend>
</target>
<target name="dependVersion">
<!-- Force the dependency on the RouterVersion as depend doesn't recognize constant changes --> <!-- Force the dependency on the RouterVersion as depend doesn't recognize constant changes -->
<dependset> <dependset>
<srcfilelist dir="." files="../../../router/java/build/obj/net/i2p/router/RouterVersion.class" /> <srcfilelist dir="." files="../../../router/java/build/obj/net/i2p/router/RouterVersion.class" />
@ -35,7 +40,7 @@
<targetfilelist dir="." files="build/obj/net/i2p/router/web/UpdateHandler.class" /> <targetfilelist dir="." files="build/obj/net/i2p/router/web/UpdateHandler.class" />
</dependset> </dependset>
</target> </target>
<target name="compile" depends="prepare, depend"> <target name="compile" depends="prepare, depend, dependVersion">
<mkdir dir="./build" /> <mkdir dir="./build" />
<mkdir dir="./build/obj" /> <mkdir dir="./build/obj" />
<javac <javac

View File

@ -6,7 +6,10 @@
<ant dir="../../ministreaming/java/" target="build" /> <ant dir="../../ministreaming/java/" target="build" />
<!-- ministreaming will build core --> <!-- ministreaming will build core -->
</target> </target>
<target name="depend"> <condition property="depend.available">
<typefound name="depend" />
</condition>
<target name="depend" if="depend.available">
<depend <depend
cache="../../../build" cache="../../../build"
srcdir="./src" srcdir="./src"

View File

@ -6,10 +6,13 @@
<ant dir="../../ministreaming/java/" target="build" /> <ant dir="../../ministreaming/java/" target="build" />
<!-- ministreaming will build core --> <!-- ministreaming will build core -->
</target> </target>
<target name="depend"> <condition property="depend.available">
<typefound name="depend" />
</condition>
<target name="depend" if="depend.available">
<depend <depend
cache="../../../build" cache="../../../build"
srcdir="./src" srcdir="./src:./test"
destdir="./build/obj" > destdir="./build/obj" >
<!-- Depend on classes instead of jars where available --> <!-- Depend on classes instead of jars where available -->
<classpath> <classpath>

View File

@ -5,7 +5,10 @@
<target name="builddep"> <target name="builddep">
<!-- noop, since the core doesnt depend on anything --> <!-- noop, since the core doesnt depend on anything -->
</target> </target>
<target name="depend"> <condition property="depend.available">
<typefound name="depend" />
</condition>
<target name="depend" if="depend.available">
<depend <depend
cache="../../build" cache="../../build"
srcdir="./src" srcdir="./src"

View File

@ -39,6 +39,7 @@ class I2PSessionImpl2 extends I2PSessionImpl {
private final static long SEND_TIMEOUT = 60 * 1000; // 60 seconds to send private final static long SEND_TIMEOUT = 60 * 1000; // 60 seconds to send
/** should we gzip each payload prior to sending it? */ /** should we gzip each payload prior to sending it? */
private final static boolean SHOULD_COMPRESS = true; private final static boolean SHOULD_COMPRESS = true;
private final static boolean SHOULD_DECOMPRESS = true;
/** /**
* Create a new session, reading the Destination, PrivateKey, and SigningPrivateKey * Create a new session, reading the Destination, PrivateKey, and SigningPrivateKey
@ -64,6 +65,8 @@ class I2PSessionImpl2 extends I2PSessionImpl {
_context.statManager().createRateStat("i2cp.receiveStatusTime.4", "How long it took to get status=4 back", "i2cp", new long[] { 60*1000, 10*60*1000 }); _context.statManager().createRateStat("i2cp.receiveStatusTime.4", "How long it took to get status=4 back", "i2cp", new long[] { 60*1000, 10*60*1000 });
_context.statManager().createRateStat("i2cp.receiveStatusTime.5", "How long it took to get status=5 back", "i2cp", new long[] { 60*1000, 10*60*1000 }); _context.statManager().createRateStat("i2cp.receiveStatusTime.5", "How long it took to get status=5 back", "i2cp", new long[] { 60*1000, 10*60*1000 });
_context.statManager().createRateStat("i2cp.receiveStatusTime", "How long it took to get any status", "i2cp", new long[] { 60*1000, 10*60*1000 }); _context.statManager().createRateStat("i2cp.receiveStatusTime", "How long it took to get any status", "i2cp", new long[] { 60*1000, 10*60*1000 });
_context.statManager().createRateStat("i2cp.tx.msgCompressed", "compressed size transferred", "i2cp", new long[] { 60*1000, 30*60*1000 });
_context.statManager().createRateStat("i2cp.tx.msgExpanded", "size before compression", "i2cp", new long[] { 60*1000, 30*60*1000 });
} }
protected long getTimeout() { protected long getTimeout() {
@ -75,6 +78,27 @@ class I2PSessionImpl2 extends I2PSessionImpl {
clearStates(); clearStates();
super.destroySession(sendDisconnect); super.destroySession(sendDisconnect);
} }
/** Don't bother if really small.
* Three 66-byte messages will fit in one tunnel message.
* Four messages don't fit no matter how small. So below 66 it isn't worth it.
* See ConnectionOptions.java in the streaming lib for similar calculations.
* Since we still have to pass it through gzip -0 the CPU savings
* is trivial but it's the best we can do for now. See below.
* i2cp.gzip defaults to SHOULD_COMPRESS = true.
* Perhaps the http server (which does its own compression)
* and P2P apps (with generally uncompressible data) should
* set to false.
*/
private static final int DONT_COMPRESS_SIZE = 66;
private boolean shouldCompress(int size) {
if (size <= DONT_COMPRESS_SIZE)
return false;
String p = getOptions().getProperty("i2cp.gzip");
if (p != null)
return Boolean.valueOf(p).booleanValue();
return SHOULD_COMPRESS;
}
@Override @Override
public boolean sendMessage(Destination dest, byte[] payload) throws I2PSessionException { public boolean sendMessage(Destination dest, byte[] payload) throws I2PSessionException {
@ -92,8 +116,30 @@ class I2PSessionImpl2 extends I2PSessionImpl {
throws I2PSessionException { throws I2PSessionException {
if (_log.shouldLog(Log.DEBUG)) _log.debug("sending message"); if (_log.shouldLog(Log.DEBUG)) _log.debug("sending message");
if (isClosed()) throw new I2PSessionException("Already closed"); if (isClosed()) throw new I2PSessionException("Already closed");
if (SHOULD_COMPRESS) payload = DataHelper.compress(payload, offset, size);
else throw new IllegalStateException("we need to update sendGuaranteed to support partial send"); // Sadly there is no way to send something completely uncompressed in a backward-compatible way,
// so we have to still send it in a gzip format, which adds 23 bytes (2.4% for a 960-byte msg)
// (10 byte header + 5 byte block header + 8 byte trailer)
// In the future we can add a one-byte magic number != 0x1F to signal an uncompressed msg
// (Gzip streams start with 0x1F 0x8B 0x08)
// assuming we don't need the CRC-32 that comes with gzip (do we?)
// Maybe implement this soon in receiveMessage() below so we are ready
// in case we ever make an incompatible network change.
// This would save 22 of the 23 bytes and a little CPU.
boolean sc = shouldCompress(size);
if (sc)
payload = DataHelper.compress(payload, offset, size);
else
payload = DataHelper.compress(payload, offset, size, DataHelper.NO_COMPRESSION);
//else throw new IllegalStateException("we need to update sendGuaranteed to support partial send");
int compressed = payload.length;
if (_log.shouldLog(Log.INFO)) {
String d = dest.calculateHash().toBase64().substring(0,4);
_log.info("sending message to: " + d + " compress? " + sc + " sizeIn=" + size + " sizeOut=" + compressed);
}
_context.statManager().addRateData("i2cp.tx.msgCompressed", compressed, 0);
_context.statManager().addRateData("i2cp.tx.msgExpanded", size, 0);
return sendBestEffort(dest, payload, keyUsed, tagsSent); return sendBestEffort(dest, payload, keyUsed, tagsSent);
} }
@ -107,7 +153,8 @@ class I2PSessionImpl2 extends I2PSessionImpl {
_log.error("Error: message " + msgId + " already received!"); _log.error("Error: message " + msgId + " already received!");
return null; return null;
} }
if (SHOULD_COMPRESS) { // future - check magic number to see whether to decompress
if (SHOULD_DECOMPRESS) {
try { try {
return DataHelper.decompress(compressed); return DataHelper.decompress(compressed);
} catch (IOException ioe) { } catch (IOException ioe) {

View File

@ -33,6 +33,7 @@ import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Properties; import java.util.Properties;
import java.util.TreeMap; import java.util.TreeMap;
import java.util.zip.Deflater;
import net.i2p.util.ByteCache; import net.i2p.util.ByteCache;
import net.i2p.util.OrderedProperties; import net.i2p.util.OrderedProperties;
@ -852,15 +853,21 @@ public class DataHelper {
} }
private static final int MAX_UNCOMPRESSED = 40*1024; private static final int MAX_UNCOMPRESSED = 40*1024;
public static final int MAX_COMPRESSION = Deflater.BEST_COMPRESSION;
public static final int NO_COMPRESSION = Deflater.NO_COMPRESSION;
/** compress the data and return a new GZIP compressed array */ /** compress the data and return a new GZIP compressed array */
public static byte[] compress(byte orig[]) { public static byte[] compress(byte orig[]) {
return compress(orig, 0, orig.length); return compress(orig, 0, orig.length);
} }
public static byte[] compress(byte orig[], int offset, int size) { public static byte[] compress(byte orig[], int offset, int size) {
return compress(orig, offset, size, MAX_COMPRESSION);
}
public static byte[] compress(byte orig[], int offset, int size, int level) {
if ((orig == null) || (orig.length <= 0)) return orig; if ((orig == null) || (orig.length <= 0)) return orig;
if (size >= MAX_UNCOMPRESSED) if (size >= MAX_UNCOMPRESSED)
throw new IllegalArgumentException("tell jrandom size=" + size); throw new IllegalArgumentException("tell jrandom size=" + size);
ReusableGZIPOutputStream out = ReusableGZIPOutputStream.acquire(); ReusableGZIPOutputStream out = ReusableGZIPOutputStream.acquire();
out.setLevel(level);
try { try {
out.write(orig, offset, size); out.write(orig, offset, size);
out.finish(); out.finish();

View File

@ -132,6 +132,7 @@ public class EepGet {
int numRetries = 5; int numRetries = 5;
int markSize = 1024; int markSize = 1024;
int lineLen = 40; int lineLen = 40;
int inactivityTimeout = 60*1000;
String etag = null; String etag = null;
String saveAs = null; String saveAs = null;
String url = null; String url = null;
@ -145,6 +146,9 @@ public class EepGet {
} else if (args[i].equals("-n")) { } else if (args[i].equals("-n")) {
numRetries = Integer.parseInt(args[i+1]); numRetries = Integer.parseInt(args[i+1]);
i++; i++;
} else if (args[i].equals("-t")) {
inactivityTimeout = 1000 * Integer.parseInt(args[i+1]);
i++;
} else if (args[i].equals("-e")) { } else if (args[i].equals("-e")) {
etag = "\"" + args[i+1] + "\""; etag = "\"" + args[i+1] + "\"";
i++; i++;
@ -174,7 +178,7 @@ public class EepGet {
EepGet get = new EepGet(I2PAppContext.getGlobalContext(), true, proxyHost, proxyPort, numRetries, saveAs, url, true, etag); EepGet get = new EepGet(I2PAppContext.getGlobalContext(), true, proxyHost, proxyPort, numRetries, saveAs, url, true, etag);
get.addStatusListener(get.new CLIStatusListener(markSize, lineLen)); get.addStatusListener(get.new CLIStatusListener(markSize, lineLen));
get.fetch(); get.fetch(45*1000, -1, inactivityTimeout);
} }
public static String suggestName(String url) { public static String suggestName(String url) {
@ -208,7 +212,7 @@ public class EepGet {
} }
private static void usage() { private static void usage() {
System.err.println("EepGet [-p localhost:4444] [-n #retries] [-o outputFile] [-m markSize lineLen] url"); System.err.println("EepGet [-p localhost:4444] [-n #retries] [-o outputFile] [-m markSize lineLen] [-t timeout] url");
} }
public static interface StatusListener { public static interface StatusListener {
@ -416,7 +420,7 @@ public class EepGet {
SocketTimeout timeout = null; SocketTimeout timeout = null;
if (_fetchHeaderTimeout > 0) if (_fetchHeaderTimeout > 0)
timeout = new SocketTimeout(_fetchHeaderTimeout); timeout = new SocketTimeout(_fetchHeaderTimeout);
final SocketTimeout stimeout = timeout; // ugly final SocketTimeout stimeout = timeout; // ugly - why not use sotimeout?
timeout.setTimeoutCommand(new Runnable() { timeout.setTimeoutCommand(new Runnable() {
public void run() { public void run() {
if (_log.shouldLog(Log.DEBUG)) if (_log.shouldLog(Log.DEBUG))
@ -457,7 +461,7 @@ public class EepGet {
} }
_currentAttempt++; _currentAttempt++;
if (_currentAttempt > _numRetries) if (_currentAttempt > _numRetries || !_keepFetching)
break; break;
try { try {
long delay = _context.random().nextInt(60*1000); long delay = _context.random().nextInt(60*1000);
@ -629,8 +633,6 @@ public class EepGet {
if (_log.shouldLog(Log.DEBUG)) if (_log.shouldLog(Log.DEBUG))
_log.debug("rc: " + responseCode + " for " + _actualURL); _log.debug("rc: " + responseCode + " for " + _actualURL);
if(_transferFailed)
_log.error("Already failed for " + _actualURL);
boolean rcOk = false; boolean rcOk = false;
switch (responseCode) { switch (responseCode) {
case 200: // full case 200: // full
@ -661,16 +663,24 @@ public class EepGet {
_keepFetching = false; _keepFetching = false;
_notModified = true; _notModified = true;
return; return;
case 403: // bad req
case 404: // not found case 404: // not found
case 409: // bad addr helper
case 503: // no outproxy
_keepFetching = false; _keepFetching = false;
_transferFailed = true; _transferFailed = true;
// maybe we should throw instead of return to get the return code back to the user
return; return;
case 416: // completed (or range out of reach) case 416: // completed (or range out of reach)
_bytesRemaining = 0; _bytesRemaining = 0;
_keepFetching = false; _keepFetching = false;
return; return;
case 504: // gateway timeout
// throw out of doFetch() to fetch() and try again
throw new IOException("HTTP Proxy timeout");
default: default:
rcOk = false; rcOk = false;
_keepFetching = false;
_transferFailed = true; _transferFailed = true;
} }

View File

@ -3,6 +3,7 @@ package net.i2p.util;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.zip.Deflater;
import java.util.zip.GZIPInputStream; import java.util.zip.GZIPInputStream;
import net.i2p.data.DataHelper; import net.i2p.data.DataHelper;
@ -51,6 +52,10 @@ public class ReusableGZIPOutputStream extends ResettableGZIPOutputStream {
public void reset() { public void reset() {
super.reset(); super.reset();
_buffer.reset(); _buffer.reset();
def.setLevel(Deflater.BEST_COMPRESSION);
}
public void setLevel(int level) {
def.setLevel(level);
} }
/** pull the contents of the stream written */ /** pull the contents of the stream written */
public byte[] getData() { return _buffer.toByteArray(); } public byte[] getData() { return _buffer.toByteArray(); }

View File

@ -1,3 +1,27 @@
2008-11-20 zzz
* I2PTunnel: Handle missing fields in edit pages better
* Move DummyNetworkDatabaseFacade to his own file
to help the build dependencies
* Drop old tcp transport and old tunnel build sources
* EepGet:
- Better handling of 504 gateway timeout
(keep going up to limit of retry count rather
than just one more partial fetch)
- Add -t cmd line option for timeout
- Better handling of 403, 409, 503 errors
- Don't keep going after unknown return code
- Don't delay before exiting after a failure
2008-11-15 zzz
* Build files:
- Don't die if depend not available
- Only verify Jetty hash once
- Add streaming lib tests to depend task
* I2CP Compression:
- Add i2cp.gzip option (default true)
- Add compression stats
- Don't bother compressing if really small
2008-11-13 zzz 2008-11-13 zzz
* Streaming: * Streaming:
- Add more info to Connection.toString() for debugging - Add more info to Connection.toString() for debugging

View File

@ -8,7 +8,10 @@
<target name="builddeptest"> <target name="builddeptest">
<ant dir="../../core/java/" target="jarTest" /> <ant dir="../../core/java/" target="jarTest" />
</target> </target>
<target name="depend"> <condition property="depend.available">
<typefound name="depend" />
</condition>
<target name="depend" if="depend.available">
<depend <depend
cache="../../build" cache="../../build"
srcdir="./src" srcdir="./src"

View File

@ -0,0 +1,64 @@
package net.i2p.router;
/*
* 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.IOException;
import java.io.Writer;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import net.i2p.data.Hash;
import net.i2p.data.LeaseSet;
import net.i2p.data.RouterInfo;
class DummyNetworkDatabaseFacade extends NetworkDatabaseFacade {
private Map _routers;
private RouterContext _context;
public DummyNetworkDatabaseFacade(RouterContext ctx) {
_routers = Collections.synchronizedMap(new HashMap());
_context = ctx;
}
public void restart() {}
public void shutdown() {}
public void startup() {
RouterInfo info = _context.router().getRouterInfo();
_routers.put(info.getIdentity().getHash(), info);
}
public void lookupLeaseSet(Hash key, Job onFindJob, Job onFailedLookupJob, long timeoutMs) {}
public LeaseSet lookupLeaseSetLocally(Hash key) { return null; }
public void lookupRouterInfo(Hash key, Job onFindJob, Job onFailedLookupJob, long timeoutMs) {
RouterInfo info = lookupRouterInfoLocally(key);
if (info == null)
_context.jobQueue().addJob(onFailedLookupJob);
else
_context.jobQueue().addJob(onFindJob);
}
public RouterInfo lookupRouterInfoLocally(Hash key) { return (RouterInfo)_routers.get(key); }
public void publish(LeaseSet localLeaseSet) {}
public void publish(RouterInfo localRouterInfo) {}
public LeaseSet store(Hash key, LeaseSet leaseSet) { return leaseSet; }
public RouterInfo store(Hash key, RouterInfo routerInfo) {
RouterInfo rv = (RouterInfo)_routers.put(key, routerInfo);
return rv;
}
public void unpublish(LeaseSet localLeaseSet) {}
public void fail(Hash dbEntry) {
_routers.remove(dbEntry);
}
public Set findNearestRouters(Hash key, int maxNumRouters, Set peersToIgnore) { return new HashSet(_routers.values()); }
public void renderStatusHTML(Writer out) throws IOException {}
}

View File

@ -63,47 +63,3 @@ public abstract class NetworkDatabaseFacade implements Service {
public int getKnownLeaseSets() { return 0; } public int getKnownLeaseSets() { return 0; }
public void renderRouterInfoHTML(Writer out, String s) throws IOException {} public void renderRouterInfoHTML(Writer out, String s) throws IOException {}
} }
class DummyNetworkDatabaseFacade extends NetworkDatabaseFacade {
private Map _routers;
private RouterContext _context;
public DummyNetworkDatabaseFacade(RouterContext ctx) {
_routers = Collections.synchronizedMap(new HashMap());
_context = ctx;
}
public void restart() {}
public void shutdown() {}
public void startup() {
RouterInfo info = _context.router().getRouterInfo();
_routers.put(info.getIdentity().getHash(), info);
}
public void lookupLeaseSet(Hash key, Job onFindJob, Job onFailedLookupJob, long timeoutMs) {}
public LeaseSet lookupLeaseSetLocally(Hash key) { return null; }
public void lookupRouterInfo(Hash key, Job onFindJob, Job onFailedLookupJob, long timeoutMs) {
RouterInfo info = lookupRouterInfoLocally(key);
if (info == null)
_context.jobQueue().addJob(onFailedLookupJob);
else
_context.jobQueue().addJob(onFindJob);
}
public RouterInfo lookupRouterInfoLocally(Hash key) { return (RouterInfo)_routers.get(key); }
public void publish(LeaseSet localLeaseSet) {}
public void publish(RouterInfo localRouterInfo) {}
public LeaseSet store(Hash key, LeaseSet leaseSet) { return leaseSet; }
public RouterInfo store(Hash key, RouterInfo routerInfo) {
RouterInfo rv = (RouterInfo)_routers.put(key, routerInfo);
return rv;
}
public void unpublish(LeaseSet localLeaseSet) {}
public void fail(Hash dbEntry) {
_routers.remove(dbEntry);
}
public Set findNearestRouters(Hash key, int maxNumRouters, Set peersToIgnore) { return new HashSet(_routers.values()); }
public void renderStatusHTML(Writer out) throws IOException {}
}

View File

@ -17,7 +17,7 @@ import net.i2p.CoreVersion;
public class RouterVersion { public class RouterVersion {
public final static String ID = "$Revision: 1.548 $ $Date: 2008-06-07 23:00:00 $"; public final static String ID = "$Revision: 1.548 $ $Date: 2008-06-07 23:00:00 $";
public final static String VERSION = "0.6.4"; public final static String VERSION = "0.6.4";
public final static long BUILD = 10; public final static long BUILD = 11;
public static void main(String args[]) { public static void main(String args[]) {
System.out.println("I2P Router version: " + VERSION + "-" + BUILD); System.out.println("I2P Router version: " + VERSION + "-" + BUILD);
System.out.println("Router ID: " + RouterVersion.ID); System.out.println("Router ID: " + RouterVersion.ID);

View File

@ -26,7 +26,6 @@ import net.i2p.router.OutNetMessage;
import net.i2p.router.RouterContext; import net.i2p.router.RouterContext;
import net.i2p.router.transport.ntcp.NTCPAddress; import net.i2p.router.transport.ntcp.NTCPAddress;
import net.i2p.router.transport.ntcp.NTCPTransport; import net.i2p.router.transport.ntcp.NTCPTransport;
import net.i2p.router.transport.tcp.TCPTransport;
import net.i2p.router.transport.udp.UDPAddress; import net.i2p.router.transport.udp.UDPAddress;
import net.i2p.util.Log; import net.i2p.util.Log;
@ -159,11 +158,6 @@ public class CommSystemFacadeImpl extends CommSystemFacade {
newCreated = true; newCreated = true;
} }
if (!addresses.containsKey(TCPTransport.STYLE)) {
RouterAddress addr = createTCPAddress();
if (addr != null)
addresses.put(TCPTransport.STYLE, addr);
}
if (!addresses.containsKey(NTCPTransport.STYLE)) { if (!addresses.containsKey(NTCPTransport.STYLE)) {
RouterAddress addr = createNTCPAddress(_context); RouterAddress addr = createNTCPAddress(_context);
if (_log.shouldLog(Log.INFO)) if (_log.shouldLog(Log.INFO))
@ -177,35 +171,6 @@ public class CommSystemFacadeImpl extends CommSystemFacade {
return new HashSet(addresses.values()); return new HashSet(addresses.values());
} }
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_TCP_DISABLED = "i2np.tcp.disable";
private RouterAddress createTCPAddress() {
if (!TransportManager.ALLOW_TCP) return null;
RouterAddress addr = new RouterAddress();
addr.setCost(10);
addr.setExpiration(null);
Properties props = new Properties();
String name = _context.router().getConfigSetting(PROP_I2NP_TCP_HOSTNAME);
String port = _context.router().getConfigSetting(PROP_I2NP_TCP_PORT);
String disabledStr = _context.router().getConfigSetting(PROP_I2NP_TCP_DISABLED);
boolean disabled = false;
if ( (disabledStr == null) || ("true".equalsIgnoreCase(disabledStr)) )
return null;
if ( (name == null) || (port == null) ) {
//_log.info("TCP Host/Port not specified in config file - skipping TCP transport");
return null;
} else {
_log.info("Creating TCP address on " + name + ":" + port);
}
props.setProperty("host", name);
props.setProperty("port", port);
addr.setOptions(props);
addr.setTransportStyle(TCPTransport.STYLE);
return addr;
}
public final static String PROP_I2NP_NTCP_HOSTNAME = "i2np.ntcp.hostname"; public final static String PROP_I2NP_NTCP_HOSTNAME = "i2np.ntcp.hostname";
public final static String PROP_I2NP_NTCP_PORT = "i2np.ntcp.port"; public final static String PROP_I2NP_NTCP_PORT = "i2np.ntcp.port";
public final static String PROP_I2NP_NTCP_AUTO_PORT = "i2np.ntcp.autoip"; public final static String PROP_I2NP_NTCP_AUTO_PORT = "i2np.ntcp.autoip";

View File

@ -28,7 +28,6 @@ import net.i2p.router.CommSystemFacade;
import net.i2p.router.OutNetMessage; import net.i2p.router.OutNetMessage;
import net.i2p.router.RouterContext; import net.i2p.router.RouterContext;
import net.i2p.router.transport.ntcp.NTCPTransport; import net.i2p.router.transport.ntcp.NTCPTransport;
import net.i2p.router.transport.tcp.TCPTransport;
import net.i2p.router.transport.udp.UDPTransport; import net.i2p.router.transport.udp.UDPTransport;
import net.i2p.util.Log; import net.i2p.util.Log;
@ -70,15 +69,6 @@ public class TransportManager implements TransportEventListener {
static final boolean ALLOW_TCP = false; static final boolean ALLOW_TCP = false;
private void configTransports() { private void configTransports() {
String disableTCP = _context.router().getConfigSetting(PROP_DISABLE_TCP);
// Unless overridden by constant or explicit config property, start TCP tranport
if ( !ALLOW_TCP || ((disableTCP != null) && (Boolean.TRUE.toString().equalsIgnoreCase(disableTCP))) ) {
_log.info("Explicitly disabling the TCP transport!");
} else {
Transport t = new TCPTransport(_context);
t.setListener(this);
_transports.add(t);
}
String enableUDP = _context.router().getConfigSetting(PROP_ENABLE_UDP); String enableUDP = _context.router().getConfigSetting(PROP_ENABLE_UDP);
if (enableUDP == null) if (enableUDP == null)
enableUDP = DEFAULT_ENABLE_UDP; enableUDP = DEFAULT_ENABLE_UDP;

View File

@ -1,795 +0,0 @@
package net.i2p.router.transport.tcp;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.SocketException;
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.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.router.transport.BandwidthLimitedInputStream;
import net.i2p.router.transport.BandwidthLimitedOutputStream;
import net.i2p.util.I2PThread;
import net.i2p.util.Log;
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 30 seconds, give up */
public static final int CONNECTION_TIMEOUT = 20*1000;
public static final int WRITE_BUFFER_SIZE = 2*1024;
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
if (_socket != null)
fail("Error connecting", e);
return null;
}
}
private TCPConnection doEstablishConnection() {
createSocket();
if ( (_socket == null) || (_error != null) )
return null;
try { _socket.setSoTimeout(CONNECTION_TIMEOUT); } catch (SocketException se) {}
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();
try { _socket.setSoTimeout(0); } catch (SocketException se) {}
TCPConnection con = new TCPConnection(_context);
con.setInputStream(_connectionIn);
con.setOutputStream(_connectionOut);
con.setSocket(_socket);
con.setRemoteRouterIdentity(_actualPeer.getIdentity());
con.setRemoteAddress(_remoteAddress);
con.setAttemptedPeer(_target.getIdentity().getHash());
con.setShownAddress(_localIP);
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 <code>#bytesFollowing + #versions + v1 [+ v2 [etc]] +
* tag? + tagData + properties</code>
*/
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(ConnectionHandler.FLAG_TAG_FOLLOWING);
baos.write(_connectionTag.getData());
} else {
baos.write(ConnectionHandler.FLAG_TAG_NOT_FOLLOWING);
}
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 <code>#bytesFollowing + versionOk + #bytesIP + IP + tagOk? + nonce + properties</code>
*
*/
private boolean receiveAgreedProtocol() {
try {
// #bytesFollowing + versionOk + #bytesIP + IP + tagOk? + nonce + properties
int numBytes = (int)DataHelper.readLong(_rawIn, 2);
if ( (numBytes <= 0) || (numBytes >= ConnectionHandler.FLAG_TEST) )
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 == ConnectionHandler.FLAG_PROTOCOL_NONE) {
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 == ConnectionHandler.FLAG_TAG_OK) && (_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 <code>H(E(nonce + tag, sessionKey))</code> */
private void updateNextTagExisting() {
byte pre[] = new byte[48];
System.arraycopy(_nonce.getData(), 0, pre, 0, 4);
System.arraycopy(_connectionTag.getData(), 0, pre, 4, 32);
_context.aes().encrypt(pre, 0, pre, 0, _key, _iv, pre.length);
Hash h = _context.sha().calculateHash(pre);
_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 BufferedOutputStream(_rawOut, ConnectionBuilder.WRITE_BUFFER_SIZE);
_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;
Properties props = DataHelper.readProperties(_rawIn);
// ignore these now
boolean ok = validateStatus(status, props);
if (!ok) return false;
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;
try {
_context.netDb().store(peer.getIdentity().getHash(), peer);
return true;
} catch (IllegalArgumentException iae) {
fail("Peer sent us bad info - " + _target.getIdentity().getHash().toBase64().substring(0,6)
+ ": " + iae.getMessage());
return false;
}
} 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 BufferedOutputStream(_rawOut, ConnectionBuilder.WRITE_BUFFER_SIZE);
_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 (" + read + ") 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;
}
// our public == X, since we are establishing the connection
byte X[] = builder.getMyPublicValueBytes();
byte Y[] = builder.getPeerPublicValueBytes();
// send: routerInfo + currentTime
// + S(routerInfo + currentTime + nonce + nextTag + X + Y, 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());
baos.write(X);
baos.write(Y);
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 + X + Y, routerIdent.signingKey)
try {
RouterInfo peer = new RouterInfo();
peer.readBytes(_rawIn);
int status = (int)_rawIn.read() & 0xFF;
Properties props = DataHelper.readProperties(_rawIn);
// ignore these now
boolean ok = validateStatus(status, props);
if (!ok) return false;
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());
baos.write(X);
baos.write(Y);
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;
try {
_context.netDb().store(peer.getIdentity().getHash(), peer);
return true;
} catch (IllegalArgumentException iae) {
fail("Peer sent us bad info - " + _target.getIdentity().getHash().toBase64().substring(0,6)
+ ": " + iae.getMessage());
return false;
}
} 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, Properties props) {
switch (status) {
case -1: // EOF
fail("Error reading the status from "
+ _target.getIdentity().calculateHash().toBase64().substring(0,6));
return false;
case ConnectionHandler.STATUS_OK:
return true;
case ConnectionHandler.STATUS_UNREACHABLE:
fail("According to "
+ _target.getIdentity().calculateHash().toBase64().substring(0,6)
+ ", we are not reachable on " + _localIP + ":" + _transport.getPort());
return false;
case ConnectionHandler.STATUS_SKEWED:
fail("According to "
+ _target.getIdentity().calculateHash().toBase64().substring(0,6)
+ ", our clock is off (they think it is " + props.getProperty("SKEW") + ")");
return false;
case ConnectionHandler.STATUS_SIGNATURE_FAILED: // (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() {
_connectionIn = new BandwidthLimitedInputStream(_context, _rawIn, _actualPeer.getIdentity());
OutputStream blos = new BandwidthLimitedOutputStream(_context, _rawOut, _actualPeer.getIdentity());
_connectionOut = blos;
//_connectionIn = _rawIn;
//_connectionOut = _rawOut;
Hash peer = _actualPeer.getIdentity().getHash();
_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() {
if ( (_target == null) || (_transport == null) ) {
fail("Internal error - null while running");
_log.log(Log.CRIT, "Internal error - target = " + _target + " _transport = " + _transport);
return;
}
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) {
Hash peer = _target.getIdentity().calculateHash();
String peerName = null;
if (peer == null)
peerName = "unknown";
else
peerName = peer.toBase64().substring(0,6);
fail("Error contacting "
+ peerName
+ " 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));
}
}
}
}

View File

@ -1,913 +0,0 @@
package net.i2p.router.transport.tcp;
import java.io.BufferedOutputStream;
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.net.SocketException;
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.DataFormatException;
import net.i2p.data.DataHelper;
import net.i2p.data.Hash;
import net.i2p.data.RouterInfo;
import net.i2p.data.SessionKey;
import net.i2p.data.Signature;
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.Log;
/**
* 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;
/** for reading/comparing, this is the #bytes sent if we are being tested */
public static final int FLAG_TEST = 0xFFFF;
/** protocol version sent if no protocols are ok */
public static final byte FLAG_PROTOCOL_NONE = 0x0;
/** alice is sending a tag to bob */
public static final byte FLAG_TAG_FOLLOWING = 0x1;
/** alice is not sending a tag to bob */
public static final byte FLAG_TAG_NOT_FOLLOWING = 0x0;
/** the connection tag is ok (we have an available key for it) */
public static final byte FLAG_TAG_OK = 0x1;
/** the connection tag is not ok (must go with a full DH) */
public static final byte FLAG_TAG_NOT_OK = 0x0;
/** dunno why the peer is bad */
public static final int STATUS_UNKNOWN = -1;
/** the peer's public addresses could not be verified */
public static final int STATUS_UNREACHABLE = 1;
/** the peer's clock is too far skewed */
public static final int STATUS_SKEWED = 2;
/** the peer's signature failed (either some crazy corruption or MITM) */
public static final int STATUS_SIGNATURE_FAILED = 3;
/** the peer is fine */
public static final int STATUS_OK = 0;
private static final int MAX_VERSIONS = 255;
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();
try { _socket.setSoTimeout(TCPListener.HANDLE_TIMEOUT); } catch (SocketException se) {}
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 (true)
try { _socket.setSoTimeout(ConnectionRunner.DISCONNECT_INACTIVITY_PERIOD); } catch (SocketException se) {}
else
try { _socket.setSoTimeout(0); } catch (SocketException se) {}
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");
con.setIsOutbound(false);
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 <code>#bytesFollowing + #versions + v1 [+ v2 [etc]] + tag? + tagData + properties</code>
*
*/
private boolean readPreferredProtocol() {
try {
int numBytes = (int)DataHelper.readLong(_rawIn, 2);
if (numBytes <= 0)
throw new IOException("Invalid number of bytes in connection");
// reachability test
if (numBytes == FLAG_TEST) {
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 > MAX_VERSIONS) ) {
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 == FLAG_TAG_FOLLOWING) {
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 <code>#bytesFollowing + versionOk + #bytesIP + IP + tagOk? + nonce + properties</code>
*/
private void sendAgreedProtocol() {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream(128);
if (_agreedProtocol <= 0)
baos.write(FLAG_PROTOCOL_NONE);
else
baos.write(_agreedProtocol);
byte ip[] = _from.getBytes();
baos.write(ip.length);
baos.write(ip);
if (_key != null)
baos.write(FLAG_TAG_OK);
else
baos.write(FLAG_TAG_NOT_OK);
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 <code>H(E(nonce + tag, sessionKey))</code> */
private void updateNextTagExisting() {
byte pre[] = new byte[48];
System.arraycopy(_nonce.getData(), 0, pre, 0, 4);
System.arraycopy(_connectionTag.getData(), 0, pre, 4, 32);
_context.aes().encrypt(pre, 0, pre, 0, _key, _iv, pre.length);
Hash h = _context.sha().calculateHash(pre);
_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 = H(tag+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 BufferedOutputStream(_rawOut, ConnectionBuilder.WRITE_BUFFER_SIZE);
_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 = STATUS_UNKNOWN;
if (!reachable) {
status = STATUS_UNREACHABLE;
} else if ( (clockSkew > Router.CLOCK_FUDGE_FACTOR)
|| (clockSkew < 0 - Router.CLOCK_FUDGE_FACTOR) ) {
status = STATUS_SKEWED;
SimpleDateFormat fmt = new SimpleDateFormat("yyyyMMddHHmmssSSS");
props.setProperty("SKEW", fmt.format(new Date(_context.clock().now())));
} else {
try {
_context.netDb().store(_actualPeer.getIdentity().getHash(), _actualPeer);
status = STATUS_OK;
} catch (IllegalArgumentException iae) {
// bad peer info
status = STATUS_UNKNOWN;
props.setProperty("REASON", "RouterInfoFailed");
}
}
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);
if (builder == null) {
fail("Error exchanging the keys with " + _from);
return false;
}
} 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 BufferedOutputStream(_rawOut, ConnectionBuilder.WRITE_BUFFER_SIZE);
_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;
// our public == Y, since we are receiving the connection
byte X[] = builder.getPeerPublicValueBytes();
byte Y[] = builder.getMyPublicValueBytes();
// read: routerInfo + currentTime
// + S(routerInfo + currentTime + nonce + nextTag + X + Y, 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());
baos.write(X);
baos.write(Y);
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 + X + Y, routerIdent.signingKey)
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream(512);
_context.router().getRouterInfo().writeBytes(baos);
Properties props = new Properties();
int status = STATUS_UNKNOWN;
if (!reachable) {
status = STATUS_UNREACHABLE;
} else if ( (clockSkew > Router.CLOCK_FUDGE_FACTOR)
|| (clockSkew < 0 - Router.CLOCK_FUDGE_FACTOR) ) {
status = STATUS_SKEWED;
SimpleDateFormat fmt = new SimpleDateFormat("yyyyMMddHHmmssSSS");
props.setProperty("SKEW", fmt.format(new Date(_context.clock().now())));
} else if (!sigOk) {
status = STATUS_SIGNATURE_FAILED;
} else {
try {
_context.netDb().store(_actualPeer.getIdentity().getHash(), _actualPeer);
status = STATUS_OK;
} catch (IllegalArgumentException iae) {
// bad peer info
status = STATUS_UNKNOWN;
props.setProperty("REASON", "RouterInfoFailed");
}
}
if (_actualPeer.getIdentity().getHash().equals(_context.routerHash())) {
status = STATUS_UNKNOWN;
props.setProperty("REASON", "wtf, talking to myself?");
}
baos.write(status);
DataHelper.writeProperties(baos, props);
byte beginning[] = baos.toByteArray();
baos.write(_nonce.getData());
baos.write(_nextConnectionTag.getData());
baos.write(X);
baos.write(Y);
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 STATUS_OK:
return true;
case STATUS_UNREACHABLE:
fail("Peer " + _actualPeer.getIdentity().calculateHash().toBase64().substring(0,6)
+ " at " + _from + " is unreachable");
return false;
case STATUS_SKEWED:
fail("Peer " + _actualPeer.getIdentity().calculateHash().toBase64().substring(0,6)
+ " was skewed by " + DataHelper.formatDuration(clockSkew));
return false;
case STATUS_SIGNATURE_FAILED:
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 ( (_remoteAddress.getPort() <= 0) || (_remoteAddress.getPort() > 65535) )
return false;
TCPAddress testAddress = _remoteAddress;
// if it is a LAN address, test with that address and not the public one
if (!TCPAddress.isPubliclyRoutable(_from)) {
testAddress = new TCPAddress(_from, _remoteAddress.getPort());
}
try {
return verifyReachability(testAddress);
} catch (IOException ioe) {
if (_log.shouldLog(Log.WARN))
_log.warn("Error verifying "
+ _actualPeer.getIdentity().calculateHash().toBase64().substring(0,6)
+ "at " + testAddress, ioe);
return false;
} catch (DataFormatException dfe) {
if (_log.shouldLog(Log.WARN))
_log.warn("Error verifying "
+ _actualPeer.getIdentity().calculateHash().toBase64().substring(0,6)
+ "at " + testAddress, dfe);
return false;
}
}
private static boolean verifyReachability(TCPAddress address) throws IOException, DataFormatException {
//if (true) return true;
Socket s = new Socket(address.getAddress(), address.getPort());
OutputStream out = s.getOutputStream();
InputStream in = s.getInputStream();
try { s.setSoTimeout(TCPListener.HANDLE_TIMEOUT); } catch (SocketException se) {}
//if (_log.shouldLog(Log.DEBUG))
// _log.debug("Beginning verification of reachability");
// send: 0xFFFF + #versions + v1 [+ v2 [etc]] + properties
DataHelper.writeLong(out, 2, FLAG_TEST);
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 flag = (int)DataHelper.readLong(in, 2);
if (flag != FLAG_TEST)
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 == FLAG_PROTOCOL_NONE)
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;
}
/**
* 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 0xFFFF.
*
*/
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 > MAX_VERSIONS) 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: 0xFFFF + versionOk + #bytesIP + IP + currentTime + properties
DataHelper.writeLong(_rawOut, 2, FLAG_TEST);
_rawOut.write(version);
byte ip[] = _from.getBytes();
_rawOut.write(ip.length);
_rawOut.write(ip);
DataHelper.writeLong(_rawOut, DataHelper.DATE_LENGTH, _context.clock().now());
//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;
}
}
/**
* Finish up the establishment (wrapping the streams, storing the netDb,
* persisting the connection tags, etc)
*
*/
private void establishComplete() {
_connectionIn = new BandwidthLimitedInputStream(_context, _rawIn, _actualPeer.getIdentity());
OutputStream blos = new BandwidthLimitedOutputStream(_context, _rawOut, _actualPeer.getIdentity());
_connectionOut = blos;
//_connectionIn = _rawIn;
//_connectionOut = _rawOut;
Hash peer = _actualPeer.getIdentity().getHash();
_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);
}
/**
* Verify the reachability of a peer.
* Usage: <code>ConnectionHandler hostname portNum</code>
*/
public static void main(String args[]) {
if (false) args = new String[] { "dev.i2p.net", "4108" };
if ( (args == null) || (args.length != 2) ) {
System.out.println("Usage: ConnectionHandler hostname portNum");
System.exit(0);
}
try {
int port = Integer.parseInt(args[1]);
TCPAddress addr = new TCPAddress(args[0], port);
boolean ok = verifyReachability(addr);
if (ok)
System.out.println("Peer is reachable: " + addr.toString());
else
System.out.println("Peer is not reachable: " + addr.toString());
} catch (Exception e) {
System.out.println("Peer is not reachable: " + args[0] + ":" + args[1]);
e.printStackTrace();
}
}
}

View File

@ -1,218 +0,0 @@
package net.i2p.router.transport.tcp;
import java.io.IOException;
import java.io.OutputStream;
import net.i2p.data.DataHelper;
import net.i2p.data.RouterInfo;
import net.i2p.data.i2np.DateMessage;
import net.i2p.data.i2np.I2NPMessage;
import net.i2p.router.OutNetMessage;
import net.i2p.router.RouterContext;
import net.i2p.util.I2PThread;
import net.i2p.util.Log;
import net.i2p.util.SimpleTimer;
/**
* Push out I2NPMessages across the wire
*
*/
class ConnectionRunner implements Runnable {
private Log _log;
private RouterContext _context;
private TCPConnection _con;
private boolean _keepRunning;
private byte _writeBuffer[];
private long _lastTimeSend;
private long _lastWriteEnd;
private long _lastWriteBegin;
private long _lastRealActivity;
private static final long TIME_SEND_FREQUENCY = 60*1000;
/** if we don't send them any real data in a 10 minute period, drop 'em */
static final int DISCONNECT_INACTIVITY_PERIOD = 10*60*1000;
public ConnectionRunner(RouterContext ctx, TCPConnection con) {
_context = ctx;
_log = ctx.logManager().getLog(ConnectionRunner.class);
_con = con;
_keepRunning = false;
_lastWriteBegin = ctx.clock().now();
_lastWriteEnd = _lastWriteBegin;
_lastRealActivity = _lastWriteBegin;
}
public void startRunning() {
_keepRunning = true;
_writeBuffer = new byte[38*1024]; // expansion factor
_lastTimeSend = -1;
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();
long delay = TIME_SEND_FREQUENCY + _context.random().nextInt(60*1000);
SimpleTimer.getInstance().addEvent(new KeepaliveEvent(), delay);
}
public void stopRunning() {
_keepRunning = false;
}
public void run() {
while (_keepRunning && !_con.getIsClosed()) {
OutNetMessage msg = _con.getNextMessage();
if (msg == null) {
if (_keepRunning && !_con.getIsClosed())
_log.error("next message is null but we should keep running?");
_con.closeConnection();
return;
} else {
sendMessage(msg);
}
}
}
private void sendMessage(OutNetMessage msg) {
byte buf[] = _writeBuffer;
int written = 0;
try {
written = msg.getMessageData(_writeBuffer);
} catch (ArrayIndexOutOfBoundsException aioobe) {
I2NPMessage m = msg.getMessage();
if (m != null) {
buf = m.toByteArray();
written = buf.length;
}
} catch (Exception e) {
_log.log(Log.CRIT, "getting the message data", e);
_con.closeConnection();
return;
}
if (written <= 0) {
if (_log.shouldLog(Log.WARN))
_log.warn("message " + msg.getMessageType() + "/" + msg.getMessageId()
+ " expired before it could be sent");
msg.timestamp("ConnectionRunner.sendMessage noData");
_con.sent(msg, false, 0);
return;
}
msg.timestamp("ConnectionRunner.sendMessage data");
boolean sendTime = false;
if (_lastTimeSend < _context.clock().now() - TIME_SEND_FREQUENCY)
sendTime = true;
OutputStream out = _con.getOutputStream();
boolean ok = false;
if (!DateMessage.class.getName().equals(msg.getMessageType()))
_lastRealActivity = _context.clock().now();
try {
synchronized (out) {
_lastWriteBegin = _context.clock().now();
out.write(buf, 0, written);
if (sendTime) {
out.write(buildTimeMessage().toByteArray());
_lastTimeSend = _context.clock().now();
}
out.flush();
_lastWriteEnd = _context.clock().now();
}
if (_log.shouldLog(Log.DEBUG))
_log.debug("Just sent message " + msg.getMessageId() + " to "
+ msg.getTarget().getIdentity().getHash().toBase64().substring(0,6)
+ " writeTime = " + (_lastWriteEnd - _lastWriteBegin) +"ms"
+ " lifetime = " + msg.getLifetime() + "ms");
ok = true;
} catch (IOException ioe) {
if (_log.shouldLog(Log.WARN))
_log.warn("Error writing out the message", ioe);
_con.closeConnection();
}
_con.sent(msg, ok, _lastWriteEnd - _lastWriteBegin);
}
/**
* Build up a new message to be sent with the current router's time
*
*/
private I2NPMessage buildTimeMessage() {
DateMessage dm = new DateMessage(_context);
dm.setNow(_context.clock().now());
return dm;
}
/**
* Every few minutes, send a (tiny) message to the peer if we haven't
* spoken with them recently. This will help kill off any hung
* connections (due to IP address changes, etc). If we don't get any
* messages through in 5 minutes, kill the connection as well.
*
*/
private class KeepaliveEvent implements SimpleTimer.TimedEvent {
public void timeReached() {
if (!_keepRunning) return;
if (_con.getIsClosed()) return;
long now = _context.clock().now();
long timeSinceWrite = now - _lastWriteEnd;
long timeSinceWriteBegin = now - _lastWriteBegin;
long timeSinceWriteReal = now - _lastRealActivity;
if (timeSinceWrite > 5*TIME_SEND_FREQUENCY) {
TCPTransport t = _con.getTransport();
String msg = "Connection closed with "
+ _con.getRemoteRouterIdentity().getHash().toBase64().substring(0,6)
+ " due to " + DataHelper.formatDuration(timeSinceWrite)
+ " of inactivity after "
+ DataHelper.formatDuration(_con.getLifetime());
if (_lastWriteBegin > _lastWriteEnd)
msg = msg + " with a message being written for " +
DataHelper.formatDuration(timeSinceWriteBegin);
t.addConnectionErrorMessage(msg);
if (_log.shouldLog(Log.INFO))
_log.info(msg);
_con.closeConnection(false);
return;
}
if (timeSinceWriteReal > DISCONNECT_INACTIVITY_PERIOD) {
TCPTransport t = _con.getTransport();
String msg = "Connection closed with "
+ _con.getRemoteRouterIdentity().getHash().toBase64().substring(0,6)
+ " due to " + DataHelper.formatDuration(timeSinceWriteReal)
+ " of inactivity after "
+ DataHelper.formatDuration(_con.getLifetime());
if (_lastWriteBegin > _lastWriteEnd)
msg = msg + " with a message being written for " +
DataHelper.formatDuration(timeSinceWriteBegin);
t.addConnectionErrorMessage(msg);
if (_log.shouldLog(Log.INFO))
_log.info(msg);
_con.closeConnection(false);
return;
}
if (_lastTimeSend < _context.clock().now() - 2*TIME_SEND_FREQUENCY)
enqueueTimeMessage();
long delay = 2*TIME_SEND_FREQUENCY + _context.random().nextInt((int)TIME_SEND_FREQUENCY);
SimpleTimer.getInstance().addEvent(KeepaliveEvent.this, delay);
}
}
private void enqueueTimeMessage() {
OutNetMessage msg = new OutNetMessage(_context);
RouterInfo ri = _context.netDb().lookupRouterInfoLocally(_con.getRemoteRouterIdentity().getHash());
if (ri == null) return;
msg.setTarget(ri);
msg.setExpiration(_context.clock().now() + TIME_SEND_FREQUENCY);
msg.setMessage(buildTimeMessage());
msg.setPriority(100);
_con.addMessage(msg);
if (_log.shouldLog(Log.INFO))
_log.info("Enqueueing time message to " + _con.getRemoteRouterIdentity().getHash().toBase64().substring(0,6));
}
}

View File

@ -1,107 +0,0 @@
package net.i2p.router.transport.tcp;
import java.util.HashMap;
import java.util.Map;
import net.i2p.data.ByteArray;
import net.i2p.data.Hash;
import net.i2p.data.SessionKey;
import net.i2p.router.RouterContext;
import net.i2p.util.Log;
/**
* Organize the tags used to connect with peers.
*
*/
public class ConnectionTagManager {
protected Log _log;
private RouterContext _context;
/** H(routerIdentity) to ByteArray */
private Map _tagByPeer;
/** ByteArray to H(routerIdentity) */
private Map _peerByTag;
/** H(routerIdentity) to SessionKey */
private Map _keyByPeer;
/** synchronize against this when dealing with the data */
private Object _lock;
/**
* Only keep the keys and tags for up to *cough* 10,000 peers (everyone
* else will need to use a full DH rekey). Ok, yeah, 10,000 is absurd for
* the TCP transport anyway, but we need a limit, and this eats up at most
* 1MB (96 bytes per peer). Later we may add another mapping to drop the
* oldest ones first, but who cares for now.
*
*/
public static final int MAX_CONNECTION_TAGS = 10*1000;
public ConnectionTagManager(RouterContext context) {
_context = context;
_log = context.logManager().getLog(getClass());
initialize();
_lock = new Object();
}
protected void initialize() {
initializeData(new HashMap(128), new HashMap(128), new HashMap(128));
}
protected void initializeData(Map keyByPeer, Map tagByPeer, Map peerByTag) {
_keyByPeer = keyByPeer;
_tagByPeer = tagByPeer;
_peerByTag = peerByTag;
}
/** Retrieve the associated tag (but do not consume it) */
public ByteArray getTag(Hash peer) {
synchronized (_lock) {
return (ByteArray)_tagByPeer.get(peer);
}
}
public SessionKey getKey(Hash peer) {
synchronized (_lock) { //
return (SessionKey)_keyByPeer.get(peer);
}
}
public SessionKey getKey(ByteArray tag) {
synchronized (_lock) { //
Hash peer = (Hash)_peerByTag.get(tag);
if (peer == null) return null;
return (SessionKey)_keyByPeer.get(peer);
}
}
/** Update the tag associated with a peer, dropping the old one */
public void replaceTag(Hash peer, ByteArray newTag, SessionKey key) {
synchronized (_lock) {
while (_keyByPeer.size() > MAX_CONNECTION_TAGS) {
Hash rmPeer = (Hash)_keyByPeer.keySet().iterator().next();
ByteArray tag = (ByteArray)_tagByPeer.remove(peer);
SessionKey oldKey = (SessionKey)_keyByPeer.remove(peer);
rmPeer = (Hash)_peerByTag.remove(tag);
if (_log.shouldLog(Log.INFO))
_log.info("Too many tags, dropping the one for " + rmPeer.toBase64().substring(0,6));
}
_keyByPeer.put(peer, key);
_peerByTag.put(newTag, peer);
_tagByPeer.put(peer, newTag);
saveTags(_keyByPeer, _tagByPeer);
}
}
/**
* Save the tags/keys associated with the peer.
*
* @param keyByPeer H(routerIdentity) to SessionKey
* @param tagByPeer H(routerIdentity) to ByteArray
*/
protected void saveTags(Map keyByPeer, Map tagByPeer) {
// noop, in memory only
}
protected RouterContext getContext() { return _context; }
}

View File

@ -1,78 +0,0 @@
package net.i2p.router.transport.tcp;
import net.i2p.data.DataHelper;
import net.i2p.data.Hash;
import net.i2p.data.RouterIdentity;
import net.i2p.data.i2np.DateMessage;
import net.i2p.data.i2np.I2NPMessage;
import net.i2p.data.i2np.I2NPMessageReader;
import net.i2p.router.Router;
import net.i2p.util.Log;
/**
* Receive messages from a message reader and bounce them off to the transport
* for further enqueueing.
*/
public class MessageHandler implements I2NPMessageReader.I2NPMessageEventListener {
private Log _log;
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();
_log = con.getRouterContext().logManager().getLog(MessageHandler.class);
transport.getContext().statManager().createRateStat("tcp.disconnectAfterSkew", "How skewed a connection became before we killed it?", "TCP", new long[] { 10*60*1000l, 60*60*1000l, 24*60*60*1000l } );
}
public void disconnected(I2NPMessageReader reader) {
_con.closeConnection();
}
public void messageReceived(I2NPMessageReader reader, I2NPMessage message, long msToRead, int size) {
_con.messageReceived();
if (_log.shouldLog(Log.DEBUG))
_log.debug("Just received message " + message.getUniqueId() + " from "
+ _identHash.toBase64().substring(0,6)
+ " readTime = " + msToRead + "ms type = " + message.getClass().getName());
if (message instanceof DateMessage) {
DateMessage msg = (DateMessage)message;
timeMessageReceived(msg.getNow());
// dont propogate the message, its just a fake
return;
}
_transport.messageReceived(message, _ident, _identHash, msToRead, size);
}
private void timeMessageReceived(long remoteTime) {
long delta = _con.getRouterContext().clock().now() - remoteTime;
if ( (delta > Router.CLOCK_FUDGE_FACTOR) || (delta < 0 - Router.CLOCK_FUDGE_FACTOR) ) {
_con.closeConnection();
String msg = "Peer " + _identHash.toBase64().substring(0,6) + " is too far skewed ("
+ DataHelper.formatDuration(delta) + ") after uptime of "
+ DataHelper.formatDuration(_con.getLifetime());
if (_log.shouldLog(Log.WARN))
_log.warn(msg);
_transport.addConnectionErrorMessage(msg);
_transport.getContext().statManager().addRateData("tcp.disconnectAfterSkew", delta, _con.getLifetime());
} else {
int level = Log.DEBUG;
if ( (delta > Router.CLOCK_FUDGE_FACTOR/2) || (delta < 0 - Router.CLOCK_FUDGE_FACTOR/2) )
level = Log.WARN;
if (_log.shouldLog(level))
_log.log(level, "Peer " + _identHash.toBase64().substring(0,6) + " is only skewed by ("
+ DataHelper.formatDuration(delta) + ") after uptime of "
+ DataHelper.formatDuration(_con.getLifetime()) );
_con.setOffsetReceived(delta);
}
}
public void readError(I2NPMessageReader reader, Exception error) {
_con.closeConnection();
}
}

View File

@ -1,200 +0,0 @@
package net.i2p.router.transport.tcp;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import net.i2p.data.ByteArray;
import net.i2p.data.DataFormatException;
import net.i2p.data.DataHelper;
import net.i2p.data.Hash;
import net.i2p.data.SessionKey;
import net.i2p.router.RouterContext;
import net.i2p.util.Log;
/**
* Simple persistent impl writing the connection tags to connectionTag.keys
* (or another file specified via "i2np.tcp.tagFile")
*
*/
public class PersistentConnectionTagManager extends ConnectionTagManager {
private Object _ioLock;
public PersistentConnectionTagManager(RouterContext context) {
super(context);
_ioLock = new Object();
}
public static final String PROP_TAG_FILE = "i2np.tcp.tagFile";
public static final String DEFAULT_TAG_FILE = "connectionTag.keys";
protected void initialize() {
loadTags();
}
/**
* Save the tags/keys associated with the peer.
*
* @param keyByPeer H(routerIdentity) to SessionKey
* @param tagByPeer H(routerIdentity) to ByteArray
*/
protected void saveTags(Map keyByPeer, Map tagByPeer) {
byte data[] = prepareData(keyByPeer, tagByPeer);
if (data == null) return;
synchronized (_ioLock) {
File tagFile = getFile();
if (tagFile == null) return;
FileOutputStream fos = null;
try {
fos = new FileOutputStream(tagFile);
fos.write(data);
fos.flush();
if (_log.shouldLog(Log.INFO))
_log.info("Wrote connection tags for " + keyByPeer.size() + " peers");
} catch (IOException ioe) {
if (_log.shouldLog(Log.WARN))
_log.warn("Error writing out the tags", ioe);
} finally {
if (fos != null) try { fos.close(); } catch (IOException ioe) {}
}
}
}
/**
* Get the raw data to be written to disk.
*
* @param keyByPeer H(routerIdentity) to SessionKey
* @param tagByPeer H(routerIdentity) to ByteArray
*/
private byte[] prepareData(Map keyByPeer, Map tagByPeer) {
ByteArrayOutputStream baos = new ByteArrayOutputStream(keyByPeer.size() * 32 * 3 + 32);
try {
for (Iterator iter = keyByPeer.keySet().iterator(); iter.hasNext(); ) {
Hash peer = (Hash)iter.next();
SessionKey key = (SessionKey)keyByPeer.get(peer);
ByteArray tag = (ByteArray)tagByPeer.get(peer);
if ( (key == null) || (tag == null) ) continue;
baos.write(peer.getData());
baos.write(key.getData());
baos.write(tag.getData());
if (_log.shouldLog(Log.DEBUG))
_log.debug("Wrote connection tag for " + peer.toBase64().substring(0,6));
}
byte pre[] = baos.toByteArray();
Hash check = getContext().sha().calculateHash(pre);
baos.write(check.getData());
return baos.toByteArray();
} catch (IOException ioe) {
if (_log.shouldLog(Log.WARN))
_log.warn("Error preparing the tags", ioe);
return null;
}
}
private void loadTags() {
File tagFile = getFile();
if ( (tagFile == null) || (tagFile.length() <= 31) ) {
initializeData(new HashMap(), new HashMap(), new HashMap());
return;
}
FileInputStream fin = null;
try {
fin = new FileInputStream(tagFile);
byte data[] = getData(tagFile, fin);
if (data == null) {
initializeData(new HashMap(), new HashMap(), new HashMap());
return;
}
int entries = data.length / (32 * 3);
Map keyByPeer = new HashMap(entries);
Map tagByPeer = new HashMap(entries);
Map peerByTag = new HashMap(entries);
for (int i = 0; i < data.length; i += 32*3) {
byte peer[] = new byte[32];
byte key[] = new byte[32];
byte tag[] = new byte[32];
System.arraycopy(data, i, peer, 0, 32);
System.arraycopy(data, i + 32, key, 0, 32);
System.arraycopy(data, i + 64, tag, 0, 32);
Hash peerData = new Hash(peer);
SessionKey keyData = new SessionKey(key);
ByteArray tagData = new ByteArray(tag);
keyByPeer.put(peerData, keyData);
tagByPeer.put(peerData, tagData);
peerByTag.put(tagData, peerData);
if (_log.shouldLog(Log.DEBUG))
_log.debug("Loaded connection tag for " + peerData.toBase64().substring(0,6));
if (keyByPeer.size() > ConnectionTagManager.MAX_CONNECTION_TAGS)
break;
}
if (_log.shouldLog(Log.INFO))
_log.info("Loaded connection tags for " + keyByPeer.size() + " peers");
initializeData(keyByPeer, tagByPeer, peerByTag);
} catch (IOException ioe) {
if (_log.shouldLog(Log.ERROR))
_log.error("Connection tag file is corrupt, removing it");
try { fin.close(); } catch (IOException ioe2) {}
tagFile.delete(); // ignore rv
fin = null;
initializeData(new HashMap(), new HashMap(), new HashMap());
return;
} finally {
if (fin != null) try { fin.close(); } catch (IOException ioe) {}
}
}
private byte[] getData(File tagFile, FileInputStream fin) throws IOException {
byte data[] = new byte[(int)tagFile.length() - 32];
int read = DataHelper.read(fin, data);
if (read != data.length) {
if (_log.shouldLog(Log.ERROR))
_log.error("Connection tag file is corrupt (too short), removing it");
try { fin.close(); } catch (IOException ioe) {}
tagFile.delete(); // ignore rv
fin = null;
return null;
}
Hash readHash = new Hash();
try {
readHash.readBytes(fin);
} catch (DataFormatException dfe) {
readHash = null;
}
Hash calcHash = getContext().sha().calculateHash(data);
if ( (readHash == null) || (!calcHash.equals(readHash)) ) {
if (_log.shouldLog(Log.ERROR))
_log.error("Connection tag file is corrupt, removing it");
try { fin.close(); } catch (IOException ioe) {}
tagFile.delete(); // ignore rv
fin = null;
return null;
}
return data;
}
private File getFile() {
return new File(getContext().getProperty(PROP_TAG_FILE, DEFAULT_TAG_FILE));
}
}

View File

@ -1,177 +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.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Properties;
import net.i2p.data.DataHelper;
import net.i2p.data.RouterAddress;
import net.i2p.router.transport.TransportImpl;
import net.i2p.util.Log;
/**
* Wrap up an address
*/
public class TCPAddress {
private final static Log _log = new Log(TCPAddress.class);
private int _port;
private String _host;
private InetAddress _addr;
/** Port number used in RouterAddress definitions */
public final static String PROP_PORT = "port";
/** Host name used in RouterAddress definitions */
public final static String PROP_HOST = "host";
public TCPAddress(String host, int port) {
try {
if (host != null) {
InetAddress iaddr = InetAddress.getByName(host);
_host = iaddr.getHostAddress();
_addr = iaddr;
}
_port = port;
} catch (UnknownHostException uhe) {
_host = null;
_port = -1;
_addr = null;
if (_log.shouldLog(Log.WARN))
_log.warn("Unknown host [" + host + "] for port [" + port + "]", uhe);
}
}
public TCPAddress() {
_host = null;
_port = -1;
_addr = null;
}
public TCPAddress(InetAddress addr, int port) {
if (addr != null)
_host = addr.getHostAddress();
_addr = addr;
_port = port;
}
public TCPAddress(RouterAddress addr) {
if (addr == null) {
_host = null;
_port = -1;
return;
}
String host = addr.getOptions().getProperty(PROP_HOST);
if (host == null) {
_host = null;
_port = -1;
} else {
try {
InetAddress iaddr = InetAddress.getByName(host.trim());
_host = iaddr.getHostAddress();
_addr = iaddr;
String port = addr.getOptions().getProperty(PROP_PORT);
if ( (port != null) && (port.trim().length() > 0) ) {
try {
_port = Integer.parseInt(port.trim());
} catch (NumberFormatException nfe) {
_log.error("Invalid port [" + port + "]", nfe);
_port = -1;
}
} else {
_port = -1;
}
} catch (UnknownHostException uhe) {
_host = null;
_port = -1;
}
}
}
public RouterAddress toRouterAddress() {
if ( (_host == null) || (_port <= 0) )
return null;
RouterAddress addr = new RouterAddress();
addr.setCost(10);
addr.setExpiration(null);
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; }
public void setAddress(InetAddress addr) { _addr = addr; }
public int getPort() { return _port; }
public void setPort(int port) { _port = port; }
public boolean isPubliclyRoutable() {
return isPubliclyRoutable(_host);
}
public static boolean isPubliclyRoutable(String host) {
if (host == null) return false;
try {
InetAddress addr = InetAddress.getByName(host);
byte quad[] = addr.getAddress();
if (quad.length != 4) {
if (_log.shouldLog(Log.ERROR))
_log.error("Refusing IPv6 address (" + host + " / " + addr.getHostAddress() + ") "
+ " since not all peers support it, and we don't support restricted routes");
return false;
}
return TransportImpl.isPubliclyRoutable(quad);
} 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.trim().hashCode();
return rv;
}
public boolean equals(Object val) {
if ( (val != null) && (val instanceof TCPAddress) ) {
TCPAddress addr = (TCPAddress)val;
String hostname = null;
if (addr.getHost() != null)
hostname = addr.getHost().trim();
String ourHost = getHost();
if (ourHost != null)
ourHost = ourHost.trim();
return DataHelper.eq(hostname, ourHost) && getPort() == addr.getPort();
}
return false;
}
public boolean equals(RouterAddress addr) {
if (addr == null) return false;
Properties opts = addr.getOptions();
if (opts == null) return false;
return ( (_host.equals(opts.getProperty(PROP_HOST))) &&
(Integer.toString(_port).equals(opts.getProperty(PROP_PORT))) );
}
}

View File

@ -1,450 +0,0 @@
package net.i2p.router.transport.tcp;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import net.i2p.data.DataHelper;
import net.i2p.data.Hash;
import net.i2p.data.RouterIdentity;
import net.i2p.data.i2np.I2NPMessageReader;
import net.i2p.router.OutNetMessage;
import net.i2p.router.RouterContext;
import net.i2p.stat.Rate;
import net.i2p.stat.RateStat;
import net.i2p.util.Log;
/**
* Central choke point for a single TCP connection to a single peer.
*
*/
public class TCPConnection {
private Log _log;
private RouterContext _context;
private RouterIdentity _ident;
private Hash _attemptedPeer;
private TCPAddress _remoteAddress;
private String _shownAddress;
private List _pendingMessages;
private InputStream _in;
private OutputStream _out;
private Socket _socket;
private TCPTransport _transport;
private ConnectionRunner _runner;
private I2NPMessageReader _reader;
private RateStat _sendRate;
private long _started;
private boolean _closed;
private long _lastRead;
private long _lastWrite;
private long _offsetReceived;
private boolean _isOutbound;
public TCPConnection(RouterContext ctx) {
_context = ctx;
_log = ctx.logManager().getLog(TCPConnection.class);
_pendingMessages = new ArrayList(4);
_ident = null;
_remoteAddress = null;
_shownAddress = null;
_in = null;
_out = null;
_socket = null;
_transport = null;
_started = -1;
_closed = false;
_lastRead = 0;
_lastWrite = 0;
_offsetReceived = 0;
_isOutbound = false;
_runner = new ConnectionRunner(_context, this);
_context.statManager().createRateStat("tcp.probabalisticDropQueueSize", "How many bytes were queued to be sent when a message as dropped probabalistically?", "TCP", new long[] { 60*1000l, 10*60*1000l, 60*60*1000l, 24*60*60*1000l } );
_context.statManager().createRateStat("tcp.queueSize", "How many bytes were queued on a connection?", "TCP", new long[] { 60*1000l, 10*60*1000l, 60*60*1000l, 24*60*60*1000l } );
_context.statManager().createRateStat("tcp.sendBps", "How fast are we sending data to a peer?", "TCP", new long[] { 60*1000l, 10*60*1000l, 60*60*1000l, 24*60*60*1000l } );
}
/** 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 we initially were trying to contact */
public Hash getAttemptedPeer() { return _attemptedPeer; }
/** 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; }
/** Who we initially were trying to contact */
public void setAttemptedPeer(Hash peer) { _attemptedPeer = peer; }
/** What address the peer said we are reachable on */
public void setShownAddress(String ip) { _shownAddress = ip; }
/** What address the peer said we are reachable on */
public String getShownAddress() { return _shownAddress; }
/** skew that the other peer has from our clock */
public long getOffsetReceived() { return _offsetReceived; }
public void setOffsetReceived(long ms) { _offsetReceived = ms; }
public TCPTransport getTransport() { return _transport; }
public boolean getIsOutbound() { return _isOutbound; }
public void setIsOutbound(boolean outbound) { _isOutbound = outbound; }
/**
* Actually start processing the messages on the connection (and reading
* from the peer, of course). This call should not block.
*
*/
public void runConnection() {
String peer = _ident.calculateHash().toBase64().substring(0,6);
String name = "TCP Read [" + peer + "]";
_sendRate = new RateStat("tcp.sendRatePeer", "How many bytes are in the messages sent to " + peer, peer, new long[] { 60*1000, 5*60*1000, 60*60*1000 });
_reader = new I2NPMessageReader(_context, _in, new MessageHandler(_transport, this), name);
_reader.startReading();
_runner.startRunning();
_started = _context.clock().now();
}
/**
* 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() { closeConnection(true); }
public synchronized void closeConnection(boolean wasError) {
if (_log.shouldLog(Log.INFO)) {
if (_ident != null)
_log.info("Connection between " + _ident.getHash().toBase64().substring(0,6)
+ " and " + _context.routerHash().toBase64().substring(0,6)
+ " closed", new Exception("Closed by"));
else
_log.info("Connection between " + _remoteAddress
+ " and " + _context.routerHash().toBase64().substring(0,6)
+ " closed", new Exception("Closed by"));
}
if (_closed) return;
_closed = true;
synchronized (_pendingMessages) {
_pendingMessages.notifyAll();
}
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) {}
List msgs = clearPendingMessages();
for (int i = 0; i < msgs.size(); i++) {
OutNetMessage msg = (OutNetMessage)msgs.get(i);
msg.timestamp("closeConnection");
_transport.afterSend(msg, false, true, -1);
}
if (wasError) {
_context.profileManager().commErrorOccurred(_ident.getHash());
_transport.addConnectionErrorMessage("Connection closed with "
+ _ident.getHash().toBase64().substring(0,6)
+ " after " + DataHelper.formatDuration(getLifetime()));
}
_transport.connectionClosed(this);
}
/**
* Pull off any unsent OutNetMessages from the queue
*
*/
public List clearPendingMessages() {
List rv = null;
synchronized (_pendingMessages) {
rv = new ArrayList(_pendingMessages);
_pendingMessages.clear();
_pendingMessages.notifyAll();
}
return rv;
}
/**
* Add the given message to the outbound queue, notifying our
* runners that we want to send it.
*
*/
public void addMessage(OutNetMessage msg) {
msg.timestamp("TCPConnection.addMessage");
List expired = null;
int remaining = 0;
long remainingSize = 0;
long curSize = msg.getMessageSize(); // so we don't serialize while locked
synchronized (_pendingMessages) {
_pendingMessages.add(msg);
expired = locked_expireOld();
List throttled = locked_throttle();
if (expired == null)
expired = throttled;
else if (throttled != null)
expired.addAll(throttled);
for (int i = 0; i < _pendingMessages.size(); i++) {
OutNetMessage cur = (OutNetMessage)_pendingMessages.get(i);
remaining++;
remainingSize += cur.getMessageSize();
}
remaining = _pendingMessages.size();
_pendingMessages.notifyAll();
}
if (expired != null) {
for (int i = 0; i < expired.size(); i++) {
OutNetMessage cur = (OutNetMessage)expired.get(i);
cur.timestamp("TCPConnection.addMessage expired");
if (_log.shouldLog(Log.WARN))
_log.warn("Message " + cur.getMessageId() + " expired on the queue to "
+ _ident.getHash().toBase64().substring(0,6)
+ " (queue size " + remaining + "/" + remainingSize + ") with lifetime "
+ cur.getLifetime() + " and size " + cur.getMessageSize());
sent(cur, false, 0);
}
}
}
private boolean shouldDropProbabalistically() {
return Boolean.valueOf(_context.getProperty("tcp.dropProbabalistically", "false")).booleanValue();
}
/**
* Implement a probabalistic dropping of messages on the queue to the
* peer along the lines of RFC2309.
*
* @return list of OutNetMessages that were expired, or null
*/
private List locked_throttle() {
if (!shouldDropProbabalistically()) return null;
int bytesQueued = 0;
long earliestExpiration = -1;
for (int i = 0; i < _pendingMessages.size(); i++) {
OutNetMessage msg = (OutNetMessage)_pendingMessages.get(i);
bytesQueued += (int)msg.getMessageSize();
if ( (earliestExpiration < 0) || (msg.getExpiration() < earliestExpiration) )
earliestExpiration = msg.getExpiration();
}
if (bytesQueued > 0)
_context.statManager().addRateData("tcp.queueSize", bytesQueued, _pendingMessages.size());
long sendRate = getSendRate();
long bytesSendableUntilFirstExpire = sendRate * (earliestExpiration - _context.clock().now()) / 1000;
// pretend that instead of being able to push bytesSendableUntilFirstExpire,
// that we can only push a fraction of that amount, causing us to probabalistically
// drop more than is necessary (leaving a fraction of the queue 'free' for bursts)
long excessQueued = (long)(bytesQueued - ((double)bytesSendableUntilFirstExpire * (1.0-getQueueFreeFactor())));
if ( (excessQueued > 0) && (_pendingMessages.size() > 1) && (_transport != null) )
return locked_probabalisticDrop(excessQueued);
else
return null;
}
/**
* by default, try to keep the queue completely full, but this can be overridden
* with the property 'tcp.queueFreeFactor'
*
*/
public static final double DEFAULT_QUEUE_FREE_FACTOR = 0.0;
private double getQueueFreeFactor() {
String factor = _context.getProperty("tcp.queueFreeFactor");
if (factor != null) {
try {
return Double.parseDouble(factor);
} catch (NumberFormatException nfe) {
if (_log.shouldLog(Log.WARN))
_log.warn("Invalid tcp.queueFreeFactor [" + factor + "]", nfe);
}
}
return DEFAULT_QUEUE_FREE_FACTOR;
}
/** how many Bps we are sending data to the peer (or 2KBps if we don't know) */
public long getSendRate() {
if (_sendRate == null) return 2*1024;
_sendRate.coalesceStats();
Rate r = _sendRate.getRate(60*1000);
if (r == null) {
return 2*1024;
} else if (r.getLastEventCount() <= 2) {
r = _sendRate.getRate(5*60*1000);
if (r.getLastEventCount() <= 2)
r = _sendRate.getRate(60*60*1000);
}
if (r.getLastEventCount() <= 2) {
return 2*1024;
} else {
long bps = (long)(r.getLastTotalValue() * 1000 / r.getLastTotalEventTime());
_context.statManager().addRateData("tcp.sendBps", bps, 0);
return bps;
}
}
/**
* Probabalistically drop messages in relation to their size vs how much
* we've exceeded our target queue usage.
*/
private List locked_probabalisticDrop(long excessBytesQueued) {
List rv = null;
for (int i = 0; i < _pendingMessages.size() && excessBytesQueued > 0; i++) {
OutNetMessage msg = (OutNetMessage)_pendingMessages.get(i);
int p = getDropProbability(msg.getMessageSize(), excessBytesQueued);
if (_context.random().nextInt(100) < p) {
_pendingMessages.remove(i);
i--;
msg.timestamp("Probabalistically dropped due to queue size " + excessBytesQueued);
if (rv == null)
rv = new ArrayList(1);
rv.add(msg);
//sent(msg, false, -1);
_context.statManager().addRateData("tcp.probabalisticDropQueueSize", excessBytesQueued, msg.getLifetime());
// since we've already dropped down this amount, lets reduce the
// number of additional messages dropped
excessBytesQueued -= msg.getMessageSize();
}
}
return rv;
}
private int getDropProbability(long msgSize, long excessBytesQueued) {
if (msgSize > excessBytesQueued)
return 100;
return (int)(100.0*(msgSize/excessBytesQueued));
}
private List locked_expireOld() {
long now = _context.clock().now();
List expired = null;
for (int i = 0; i < _pendingMessages.size(); i++) {
OutNetMessage cur = (OutNetMessage)_pendingMessages.get(i);
if (cur.getExpiration() < now) {
_pendingMessages.remove(i);
if (expired == null)
expired = new ArrayList(1);
expired.add(cur);
i--;
}
}
return expired;
}
/**
* 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();
int queueSize = 0;
synchronized (_pendingMessages) {
queueSize = _pendingMessages.size();
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--;
}
}
if (_pendingMessages.size() > 0) {
msg = (OutNetMessage)_pendingMessages.remove(0);
} else {
if (expired == null) {
try {
_pendingMessages.wait();
} catch (InterruptedException ie) {}
}
}
}
if (expired != null) {
for (int i = 0; i < expired.size(); i++) {
OutNetMessage cur = (OutNetMessage)expired.get(i);
cur.timestamp("TCPConnection.getNextMessage expired");
if (_log.shouldLog(Log.WARN))
_log.warn("Message " + cur.getMessageId() + " expired on the queue to "
+ _ident.getHash().toBase64().substring(0,6)
+ " (queue size " + queueSize + ") with lifetime "
+ cur.getLifetime());
sent(cur, false, 0);
}
}
}
if (msg != null)
msg.timestamp("TCPConnection.getNextMessage retrieved");
return msg;
}
/** How long has this connection been active for? */
public long getLifetime() { return (_started <= 0 ? -1 : _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; }
RouterContext getRouterContext() { return _context; }
boolean getIsActive() {
if ( (_lastRead <= 0) || (_lastWrite <= 0) ) return false;
long recent = (_lastRead > _lastWrite ? _lastRead : _lastWrite);
long howLongAgo = _context.clock().now() - recent;
if (howLongAgo < 1*60*1000)
return true;
else
return false;
}
void messageReceived() {
_lastRead = _context.clock().now();
}
/**
* 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);
if (ok)
_sendRate.addData(msg.getMessageSize(), msg.getLifetime());
if (ok)
_lastWrite = _context.clock().now();
}
}

View File

@ -1,69 +0,0 @@
package net.i2p.router.transport.tcp;
import net.i2p.data.Hash;
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) {
try {
loop();
} catch (Exception e) {
_log.log(Log.CRIT, "wtf, establisher b0rked. send this stack trace to jrandom", e);
}
}
}
private void loop() {
RouterInfo info = _transport.getNextPeer();
if (info == null) {
try { Thread.sleep(5*1000); } catch (InterruptedException ie) {}
return;
}
ConnectionBuilder cb = new ConnectionBuilder(_context, _transport, info);
TCPConnection con = null;
try {
con = cb.establishConnection();
} catch (Exception e) {
_log.log(Log.CRIT, "Unhandled exception establishing a connection to "
+ info.getIdentity().getHash().toBase64(), e);
}
if (con != null) {
con.setIsOutbound(true);
_transport.connectionEstablished(con);
} else {
if (!_context.router().isAlive()) return;
_transport.addConnectionErrorMessage(cb.getError());
Hash peer = info.getIdentity().getHash();
_context.profileManager().commErrorOccurred(peer);
// disabling in preparation for dropping tcp, since other transports may work, and
// hence shitlisting is not appropriate
//_context.shitlist().shitlistRouter(peer, "Unable to contact");
//_context.netDb().fail(peer);
}
// 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);
}
}

View File

@ -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.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
import net.i2p.router.RouterContext;
import net.i2p.util.I2PThread;
import net.i2p.util.Log;
import net.i2p.util.SimpleTimer;
/**
* Listen for TCP connections with a listener thread
*
*/
class TCPListener {
private Log _log;
private TCPTransport _transport;
private ServerSocket _socket;
private ListenerRunner _listener;
private RouterContext _context;
/** Client Sockets that have been received but not yet handled (oldest first) */
private List _pendingSockets;
/** List of SocketHandler runners if we're listening (else an empty list) */
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 */
final static int HANDLE_TIMEOUT = 30*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);
_transport = transport;
_pendingSockets = new ArrayList(10);
_handlers = new ArrayList(CONCURRENT_HANDLERS);
_context.statManager().createRateStat("tcp.conReceiveOK", "How long does it take to receive a valid connection", "TCP", new long[] { 60*1000, 5*60*1000, 10*60*1000 });
_context.statManager().createRateStat("tcp.conReceiveFail", "How long does it take to receive a failed connection", "TCP", new long[] { 60*1000, 5*60*1000, 10*60*1000 });
_context.statManager().createRateStat("tcp.conUnhandled", "How often do we receive a connection but take too long on other ones to handle it", "TCP", new long[] { 60*1000, 5*60*1000, 10*60*1000 });
}
/** Make sure we are listening per the transport's config */
public void startListening() {
TCPAddress addr = new TCPAddress(_transport.getMyHost(), _transport.getPort());
if (addr.getPort() > 0) {
if (_listener != null) {
if ( (_listener.getMyAddress().getPort() == addr.getPort()) &&
(_listener.getMyAddress().getHost() == null) ) {
_listener.getMyAddress().setHost(addr.getHost());
}
if (_log.shouldLog(Log.WARN))
_log.warn("Not starting another listener on " + addr
+ " while already listening on " + _listener.getMyAddress());
return;
}
_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();
}
}
}
public void stopListening() {
if (_listener != null)
_listener.stopListening();
for (int i = 0; i < _handlers.size(); i++) {
SocketHandler h = (SocketHandler)_handlers.get(i);
h.stopHandling();
}
_handlers.clear();
if (_socket != null) {
try {
_socket.close();
_socket = null;
} catch (IOException ioe) {}
}
_listener = null;
}
private InetAddress getInetAddress(String host) {
try {
return InetAddress.getByName(host);
} catch (UnknownHostException uhe) {
_log.warn("Listen host " + host + " unknown", uhe);
try {
return InetAddress.getLocalHost();
} catch (UnknownHostException uhe2) {
_log.error("Local host is not reachable", uhe2);
return null;
}
}
}
class ListenerRunner implements Runnable {
private boolean _isRunning;
private int _nextFailDelay = 1000;
private TCPAddress _myAddress;
public ListenerRunner(TCPAddress address) {
_isRunning = true;
_myAddress = address;
}
public void stopListening() { _isRunning = false; }
public TCPAddress getMyAddress() { return _myAddress; }
public void run() {
if (_log.shouldLog(Log.INFO))
_log.info("Beginning TCP listener on " + _myAddress);
int curDelay = 0;
while (_isRunning) {
try {
if ( (_transport.shouldListenToAllInterfaces()) || (_myAddress.getHost() == null) ) {
_socket = new ServerSocket(_myAddress.getPort());
} else {
InetAddress listenAddr = getInetAddress(_myAddress.getHost());
_socket = new ServerSocket(_myAddress.getPort(), 5, listenAddr);
}
String host = (null == _myAddress.getHost() ? "0.0.0.0" : _myAddress.getHost());
if (_log.shouldLog(Log.INFO))
_log.info("Begin looping for host " + host + ":" + _myAddress.getPort());
curDelay = 0;
loop();
} catch (IOException ioe) {
if (_isRunning && _context.router().isAlive())
if (_log.shouldLog(Log.ERROR))
_log.error("Error listening to tcp connection " + _myAddress.getHost() + ":"
+ _myAddress.getPort(), ioe);
}
if (_socket != null) {
try { _socket.close(); } catch (IOException ioe) {}
_socket = null;
}
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 (_isRunning && _context.router().isAlive())
if (_log.shouldLog(Log.ERROR))
_log.error("CANCELING TCP LISTEN. delay = " + curDelay);
_isRunning = false;
}
private void loop() {
while (_isRunning && _context.router().isAlive()) {
try {
if (_log.shouldLog(Log.INFO))
_log.info("Waiting for a connection on " + _myAddress.getHost() + ":" + _myAddress.getPort());
Socket s = _socket.accept();
if (_log.shouldLog(Log.INFO))
_log.info("Connection handled on " + _myAddress.getHost() + ":" + _myAddress.getPort() + " with " + s.getInetAddress().toString() + ":" + s.getPort());
handle(s);
} catch (SocketException se) {
if (_log.shouldLog(Log.ERROR))
_log.error("Error handling a connection - closed?", se);
return;
} catch (Throwable t) {
if (_log.shouldLog(Log.ERROR))
_log.error("Error handling a connection", t);
}
}
}
}
/**
* Just toss it on a queue for our pool of handlers to deal with (but also
* queue up a timeout event in case they're swamped)
*
*/
private void handle(Socket s) {
SimpleTimer.getInstance().addEvent(new CloseUnhandled(s), HANDLE_TIMEOUT);
synchronized (_pendingSockets) {
_pendingSockets.add(s);
_pendingSockets.notifyAll();
}
}
/** callback to close an unhandled socket (if the handlers are overwhelmed) */
private class CloseUnhandled implements SimpleTimer.TimedEvent {
private Socket _cur;
public CloseUnhandled(Socket socket) {
_cur = socket;
}
public void timeReached() {
boolean removed;
synchronized (_pendingSockets) {
removed = _pendingSockets.remove(_cur);
}
if (removed) {
_context.statManager().addRateData("tcp.conUnhandled", 1, 0);
// handlers hadn't taken it yet, so close it
if (_log.shouldLog(Log.WARN))
_log.warn("Closing unhandled socket " + _cur);
try { _cur.close(); } catch (IOException ioe) {}
}
}
}
/**
* Implement a runner for the pool of handlers, pulling sockets out of the
* _pendingSockets queue and synchronously pumping them through a
* TimedHandler.
*
*/
private class SocketHandler implements Runnable {
private boolean _handle;
public SocketHandler() {
_handle = true;
}
public void run () {
while (_handle) {
Socket cur = null;
try {
synchronized (_pendingSockets) {
if (_pendingSockets.size() <= 0)
_pendingSockets.wait();
else
cur = (Socket)_pendingSockets.remove(0);
}
} catch (InterruptedException ie) {}
if (cur != null)
handleSocket(cur);
cur = null;
}
}
public void stopHandling() { _handle = false; }
/**
* blocking call to establish the basic connection, but with a timeout
* in the TimedHandler
*/
private void handleSocket(Socket s) {
TimedHandler h = new TimedHandler(s);
h.handle();
}
}
private class TimedHandler implements SimpleTimer.TimedEvent {
private int _handlerId;
private Socket _socket;
private boolean _wasSuccessful;
public TimedHandler(Socket socket) {
_socket = socket;
_wasSuccessful = false;
_handlerId = ++__handlerId;
}
public int getHandlerId() { return _handlerId; }
public void handle() {
SimpleTimer.getInstance().addEvent(TimedHandler.this, HANDLE_TIMEOUT);
ConnectionHandler ch = new ConnectionHandler(_context, _transport, _socket);
TCPConnection con = null;
try {
long before = System.currentTimeMillis();
con = ch.receiveConnection();
long duration = System.currentTimeMillis() - before;
if (con != null)
_context.statManager().addRateData("tcp.conReceiveOK", duration, duration);
else
_context.statManager().addRateData("tcp.conReceiveFail", duration, duration);
} catch (Exception e) {
_log.log(Log.CRIT, "Unhandled exception receiving a connection on " + _socket, e);
}
if (con != null) {
_wasSuccessful = true;
_transport.connectionEstablished(con);
} else if (ch.getTestComplete()) {
// not a connection, but we verified the test
_wasSuccessful = true;
}
if (!_wasSuccessful)
_transport.addConnectionErrorMessage(ch.getError());
}
public boolean wasSuccessful() { return _wasSuccessful; }
/**
* Called after a timeout period - if we haven't already established the
* connection, close the socket (interrupting any blocking ops)
*
*/
public void timeReached() {
if (wasSuccessful()) {
//if (_log.shouldLog(Log.DEBUG))
// _log.debug("Handle successful");
} else {
if (_log.shouldLog(Log.WARN))
_log.warn("Unable to handle in the time allotted");
try { _socket.close(); } catch (IOException ioe) {}
}
}
}
}

View File

@ -1,851 +0,0 @@
package net.i2p.router.transport.tcp;
import java.io.IOException;
import java.io.Writer;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
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.router.OutNetMessage;
import net.i2p.router.RouterContext;
import net.i2p.router.transport.Transport;
import net.i2p.router.transport.TransportBid;
import net.i2p.router.transport.TransportImpl;
import net.i2p.util.I2PThread;
import net.i2p.util.Log;
/**
* TCP Transport implementation, coordinating the connections
* between peers and the transmission of messages across those
* connections.
*
*/
public class TCPTransport extends TransportImpl {
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;
/** H(RouterIdentity) to TCPConnection for fully established connections */
private Map _connectionsByIdent;
/** TCPAddress::toString() to TCPConnection for fully established connections */
private Map _connectionsByAddress;
/** H(RouterIdentity) for not yet established connections */
private Set _pendingConnectionsByIdent;
/** TCPAddress::toString() for not yet established connections */
private Set _pendingConnectionsByAddress;
/**
* H(RouterIdentity) to List of OutNetMessage for messages targetting
* not yet established connections
*/
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;
private TransportBid _fastBid;
private TransportBid _slowBid;
/** 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[] { 6 }; // drop < 0.6.1.11
/** blah, people shouldnt use defaults... */
public static final int DEFAULT_LISTEN_PORT = 8887;
/** Creates a new instance of TCPTransport */
public TCPTransport(RouterContext context) {
super(context);
_log = context.logManager().getLog(TCPTransport.class);
_listener = new TCPListener(context, this);
_myAddress = null;
_tagManager = new PersistentConnectionTagManager(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();
_fastBid = new SharedBid(200);
_slowBid = new SharedBid(5000);
String str = _context.getProperty(PROP_ESTABLISHERS);
int establishers = 0;
if (str != null) {
try {
establishers = Integer.parseInt(str);
} catch (NumberFormatException nfe) {
if (_log.shouldLog(Log.ERROR))
_log.error("Invalid number of connection establishers [" + str + "]");
establishers = DEFAULT_ESTABLISHERS;
}
} else {
establishers = DEFAULT_ESTABLISHERS;
}
_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) {
if (false) return null;
RouterAddress addr = toAddress.getTargetAddress(STYLE);
if ( (_myAddress != null) && (_myAddress.equals(addr)) )
return null; // dont talk to yourself
if (getIsConnected(toAddress.getIdentity())) {
return _fastBid;
} else {
if (addr == null)
return null;
return _slowBid;
}
}
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);
if (msg != null) {
if (msg.getTarget() == null)
throw new IllegalStateException("Null target for a ready message?");
msg.timestamp("TCPTransport.outboundMessageReady");
TCPConnection con = null;
boolean newPeer = false;
Hash peer = msg.getTarget().getIdentity().calculateHash();
synchronized (_connectionLock) {
con = (TCPConnection)_connectionsByIdent.get(peer);
if (con == null) {
if (_log.shouldLog(Log.DEBUG)) {
StringBuffer buf = new StringBuffer(128);
buf.append("No connections to ");
buf.append(peer.toBase64().substring(0,6));
buf.append(", but we are connected to ");
for (Iterator iter = _connectionsByIdent.keySet().iterator(); iter.hasNext(); ) {
Hash cur = (Hash)iter.next();
buf.append(cur.toBase64().substring(0,6)).append(", ");
}
_log.debug(buf.toString());
}
List msgs = (List)_pendingMessages.get(peer);
if (msgs == null) {
msgs = new ArrayList(4);
_pendingMessages.put(peer, msgs);
newPeer = true;
}
msgs.add(msg);
msg.timestamp("TCPTransport.outboundMessageReady queued behind " +(msgs.size()-1));
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 changedMsgs = null;
boolean alreadyConnected = false;
boolean changedIdents = false;
synchronized (_connectionLock) {
if (_connectionsByAddress.containsKey(remAddr.toString())) {
alreadyConnected = true;
} else {
_connectionsByAddress.put(remAddr.toString(), con);
}
if (_connectionsByIdent.containsKey(ident.calculateHash())) {
alreadyConnected = true;
} else {
_connectionsByIdent.put(ident.calculateHash(), con);
}
// just drop the _pending connections - the establisher should fail
// them accordingly.
_pendingConnectionsByAddress.remove(remAddr.toString());
_pendingConnectionsByIdent.remove(ident.calculateHash());
if ( (con.getAttemptedPeer() != null) && (!ident.getHash().equals(con.getAttemptedPeer())) ) {
changedIdents = true;
_pendingConnectionsByIdent.remove(con.getAttemptedPeer());
changedMsgs = (List)_pendingMessages.remove(con.getAttemptedPeer());
}
if (!alreadyConnected)
waitingMsgs = (List)_pendingMessages.remove(ident.calculateHash());
if (_log.shouldLog(Log.DEBUG)) {
StringBuffer buf = new StringBuffer(256);
buf.append("\nConnection to ").append(ident.getHash().toBase64().substring(0,6));
buf.append(" built. Already connected? ");
buf.append(alreadyConnected);
buf.append("\nconnectionsByAddress: (cur=").append(remAddr.toString()).append(") ");
for (Iterator iter = _connectionsByAddress.keySet().iterator(); iter.hasNext(); ) {
String addr = (String)iter.next();
buf.append(addr).append(" ");
}
buf.append("\nconnectionsByIdent: ");
for (Iterator iter = _connectionsByIdent.keySet().iterator(); iter.hasNext(); ) {
Hash h = (Hash)iter.next();
buf.append(h.toBase64().substring(0,6)).append(" ");
}
_log.debug(buf.toString());
}
}
if (changedIdents) {
_context.shitlist().shitlistRouter(con.getAttemptedPeer(), "Changed identities", STYLE);
if (changedMsgs != null) {
for (int i = 0; i < changedMsgs.size(); i++) {
OutNetMessage cur = (OutNetMessage)changedMsgs.get(i);
cur.timestamp("changedIdents");
afterSend(cur, false, false, 0);
}
}
}
if (alreadyConnected) {
if (_log.shouldLog(Log.DEBUG))
_log.debug("Closing new duplicate");
con.setTransport(this);
con.closeConnection();
} else {
con.setTransport(this);
if (waitingMsgs != null) {
for (int i = 0; i < waitingMsgs.size(); i++) {
con.addMessage((OutNetMessage)waitingMsgs.get(i));
}
}
_context.shitlist().unshitlistRouter(ident.calculateHash(), STYLE);
con.runConnection();
if (_log.shouldLog(Log.DEBUG))
_log.debug("Connection set to run");
}
}
void connectionClosed(TCPConnection con) {
Hash remotePeer = null;
if (con == null) return;
RouterIdentity peer = con.getRemoteRouterIdentity();
if (peer == null) return;
remotePeer = peer.getHash();
synchronized (_connectionLock) {
TCPConnection cur = (TCPConnection)_connectionsByIdent.remove(remotePeer);
if ( (cur != null) && (cur != con) )
_connectionsByIdent.put(cur.getRemoteRouterIdentity().getHash(), cur);
cur = (TCPConnection)_connectionsByAddress.remove(con.getRemoteAddress().toString());
if ( (cur != null) && (cur != con) )
_connectionsByAddress.put(cur.getRemoteAddress().toString(), cur);
if (_log.shouldLog(Log.DEBUG)) {
StringBuffer buf = new StringBuffer(256);
buf.append("\nCLOSING ").append(con.getRemoteRouterIdentity().getHash().toBase64().substring(0,6));
buf.append(".");
if (cur != null)
buf.append("\nconnectionsByAddress: (cur=").append(con.getRemoteAddress().toString()).append(") ");
for (Iterator iter = _connectionsByAddress.keySet().iterator(); iter.hasNext(); ) {
String addr = (String)iter.next();
buf.append(addr).append(" ");
}
buf.append("\nconnectionsByIdent: ");
for (Iterator iter = _connectionsByIdent.keySet().iterator(); iter.hasNext(); ) {
Hash h = (Hash)iter.next();
buf.append(h.toBase64().substring(0,6)).append(" ");
}
_log.debug(buf.toString(), new Exception("Closed by"));
}
}
}
/**
* 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 (_log.shouldLog(Log.DEBUG))
_log.debug("Address received [" + address + "] our address: [" + _myAddress + "]");
synchronized (_listener) { // no need to lock on the whole TCPTransport
if (allowAddressUpdate(address)) {
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
if (_log.shouldLog(Log.INFO))
_log.info("Not updating our local address, as it hasnt changed from " + address);
return;
}
}
if (_log.shouldLog(Log.INFO))
_log.info("Update our local address to " + address);
updateAddress(addr);
} else {
if (_log.shouldLog(Log.WARN))
_log.warn("Address received is NOT a valid address! [" + addr + "]");
}
} else {
if (_log.shouldLog(Log.ERROR))
_log.error("Address specified is not valid [" + address + ":" + port + "]");
}
} else {
// either we have explicitly specified our IP address, or
// we are already connected to some people.
if (_log.shouldLog(Log.DEBUG))
_log.debug("Not allowing address update");
}
}
}
public RouterAddress startListening() {
configureLocalAddress();
_listener.startListening();
if (_myAddress != null) {
RouterAddress rv = _myAddress.toRouterAddress();
if (rv != null)
replaceAddress(rv);
return rv;
} else {
return null;
}
}
public void 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. A timestamp is prefixed to it before
* being rendered on the router console.
*
*/
void addConnectionErrorMessage(String msg) {
synchronized (_fmt) {
msg = _fmt.format(new Date(_context.clock().now())) + ": " + msg;
}
synchronized (_lastConnectionErrors) {
while (_lastConnectionErrors.size() >= MAX_ERR_MESSAGES)
_lastConnectionErrors.remove(0);
_lastConnectionErrors.add(msg);
}
}
String getMyHost() {
if (_myAddress != null)
return _myAddress.getHost();
else
return null;
}
public String getStyle() { return STYLE; }
ConnectionTagManager getTagManager() { return _tagManager; }
/**
* Initialize the _myAddress var with our local address (if possible)
*
*/
private void configureLocalAddress() {
String addr = _context.getProperty(LISTEN_ADDRESS);
int port = getPort();
if ( (addr == null) || (addr.trim().length() <= 0) ) {
if (_log.shouldLog(Log.ERROR))
_log.error("External address is not specified - autodetecting IP (be sure to forward port " + port + ")");
return;
}
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");
}
} else {
if (_log.shouldLog(Log.ERROR))
_log.error("External port is not valid");
}
}
/**
* Is the given address a valid one that we could listen to or contact?
*
*/
boolean allowAddress(TCPAddress address) {
if (address == null) {
if (_log.shouldLog(Log.DEBUG))
_log.debug("Address is null?!");
return false;
}
if ( (address.getPort() <= 0) || (address.getPort() > 65535) ) {
if (_log.shouldLog(Log.DEBUG))
_log.debug("Port is invalid? " + address.getPort());
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.WARN))
_log.warn("External address " + address + " is not publicly routable");
return false;
}
} else {
return true;
}
}
/**
* Blocking call to unconditionally update our listening address to the
* one specified, updating the routerInfo, etc.
*
*/
private void updateAddress(TCPAddress addr) {
boolean restartListener = true;
if ( (addr.getPort() == getPort()) && (shouldListenToAllInterfaces()) )
restartListener = false;
RouterAddress routerAddr = addr.toRouterAddress();
_myAddress = addr;
if (restartListener)
_listener.stopListening();
replaceAddress(routerAddr);
_context.router().rebuildRouterInfo();
if (_log.shouldLog(Log.INFO))
_log.info("Updating our local address to include " + addr.toString()
+ " and modified our routerInfo to have: "
+ _context.router().getRouterInfo().getAddresses());
// safe to do multiple times
_listener.startListening();
}
/**
* 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 boolean allowAddressUpdate(String proposedAddress) {
int connectedPeers = countActivePeers();
boolean addressSpecified = (null != _context.getProperty(LISTEN_ADDRESS));
if (addressSpecified) {
if (_log.shouldLog(Log.DEBUG))
_log.debug("Not allowing address update, sicne we have one specified (#cons=" + connectedPeers + ")");
return false;
}
if (connectedPeers < 3) {
if (_log.shouldLog(Log.DEBUG))
_log.debug("Allowing address update, since the # of connected peers is " + connectedPeers);
return true;
} else if (connectedPeers == 3) {
// ok, now comes the vote:
// if we agree with the majority, allow the update
// otherwise, reject the update
int agreed = countActiveAgreeingPeers(proposedAddress);
if (agreed > 1) {
if (_log.shouldLog(Log.DEBUG))
_log.debug("Most common address selected, allowing address update w/ # of connected peers is " + connectedPeers);
return true;
} else {
if (_log.shouldLog(Log.DEBUG))
_log.debug("Proposed address [" + proposedAddress + "] is only used by " + agreed
+ ", rejecting address update w/ # of connected peers is "
+ connectedPeers);
return false;
}
} else {
if (_log.shouldLog(Log.DEBUG))
_log.debug("Not allowing address update, since the # of connected peers is " + connectedPeers);
return false;
}
}
/**
* What port should we be reachable on?
*
* @return the port number, or -1 if there is no valid port
*/
int getPort() {
if ( (_myAddress != null) && (_myAddress.getPort() > 0) )
return _myAddress.getPort();
String port = _context.getProperty(LISTEN_PORT, DEFAULT_LISTEN_PORT+"");
if (port != null) {
try {
int portNum = Integer.parseInt(port.trim());
if ( (portNum >= 1) && (portNum < 65535) )
return portNum;
} catch (NumberFormatException nfe) {
// fallthrough
}
}
return -1;
}
public List getMostRecentErrorMessages() {
return _lastConnectionErrors;
}
/**
* How many peers can we talk to right now?
*
*/
public int countActivePeers() {
int numActive = 0;
int numInactive = 0;
synchronized (_connectionLock) {
if (_connectionsByIdent.size() <= 0) return 0;
for (Iterator iter = _connectionsByIdent.values().iterator(); iter.hasNext(); ) {
TCPConnection con = (TCPConnection)iter.next();
if (con.getIsActive())
numActive++;
else
numInactive++;
}
}
if ( (numInactive > 0) && (_log.shouldLog(Log.DEBUG)) )
_log.debug("Inactive peers: " + numInactive + " active: " + numActive);
return numActive;
}
/**
* How many peers that we are connected to think we are reachable at the given
* address?
*
*/
public int countActiveAgreeingPeers(String address) {
int agreed = 0;
synchronized (_connectionLock) {
if (_connectionsByIdent.size() <= 0) return 0;
for (Iterator iter = _connectionsByIdent.values().iterator(); iter.hasNext(); ) {
TCPConnection con = (TCPConnection)iter.next();
if (con.getIsActive()) {
String shown = con.getShownAddress();
if ( (shown != null) && (shown.equals(address)) )
agreed++;
}
}
}
return agreed;
}
/**
* 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 afterSend(OutNetMessage msg, boolean sendSuccessful, boolean allowRequeue, long msToSend) {
super.afterSend(msg, sendSuccessful, allowRequeue, msToSend);
}
/**
* Blocking call to retrieve the next peer that we want to establish a
* connection with.
*
*/
RouterInfo getNextPeer() {
while (_context.router().isAlive()) {
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);
if (addr == null) {
_log.error("Message target has no TCP addresses! " + msg.getTarget());
iter.remove();
_context.shitlist().shitlistRouter(peer, "Peer "
+ msg.getTarget().getIdentity().calculateHash().toBase64().substring(0,6)
+ " has no addresses", STYLE);
_context.netDb().fail(peer);
for (int i = 0; i < msgs.size(); i++) {
OutNetMessage cur = (OutNetMessage)msgs.get(i);
cur.timestamp("no TCP addresses");
afterSend(cur, false, false, 0);
}
continue;
}
TCPAddress tcpAddr = new TCPAddress(addr);
if (tcpAddr.getPort() <= 0) {
iter.remove();
_context.shitlist().shitlistRouter(peer, "Peer "
+ msg.getTarget().getIdentity().calculateHash().toBase64().substring(0,6)
+ " has only invalid addresses", STYLE);
_context.netDb().fail(peer);
for (int i = 0; i < msgs.size(); i++) {
OutNetMessage cur = (OutNetMessage)msgs.get(i);
cur.timestamp("invalid addresses");
afterSend(cur, false, false, 0);
}
continue; // invalid
}
if (_pendingConnectionsByAddress.contains(tcpAddr.toString()))
continue; // we're already trying to talk to someone at their address
if (_context.routerHash().equals(peer)) {
_log.error("Message points at us! " + msg.getTarget());
iter.remove();
_context.netDb().fail(peer);
for (int i = 0; i < msgs.size(); i++) {
OutNetMessage cur = (OutNetMessage)msgs.get(i);
cur.timestamp("points at us");
afterSend(cur, false, false, 0);
}
continue;
}
if ( (_myAddress != null) && (_myAddress.equals(tcpAddr)) ) {
_log.error("Message points at our old TCP addresses! " + msg.getTarget());
iter.remove();
_context.shitlist().shitlistRouter(peer, "This is our old address...", STYLE);
_context.netDb().fail(peer);
for (int i = 0; i < msgs.size(); i++) {
OutNetMessage cur = (OutNetMessage)msgs.get(i);
cur.timestamp("points at our ip");
afterSend(cur, false, false, 0);
}
continue;
}
if (!allowAddress(tcpAddr)) {
_log.error("Message points at illegal address! router "
+ msg.getTarget().getIdentity().calculateHash().toBase64().substring(0,6)
+ " address " + tcpAddr.toString());
iter.remove();
_context.shitlist().shitlistRouter(peer, "Invalid TCP address...", STYLE);
_context.netDb().fail(peer);
for (int i = 0; i < msgs.size(); i++) {
OutNetMessage cur = (OutNetMessage)msgs.get(i);
cur.timestamp("points at an illegal address");
afterSend(cur, false, false, 0);
}
continue;
}
// ok, this is someone we can try to contact. mark it as ours.
_pendingConnectionsByIdent.add(peer);
_pendingConnectionsByAddress.add(tcpAddr.toString());
if (_log.shouldLog(Log.DEBUG))
_log.debug("Add pending connection to: " + peer.toBase64().substring(0,6));
return msg.getTarget();
}
try {
_connectionLock.wait();
} catch (InterruptedException ie) {}
}
}
return null;
}
/** 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);
}
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);
msg.timestamp("establishmentComplete(failed)");
afterSend(msg, false);
}
}
}
/** Make this stuff pretty (only used in the old console) */
public void renderStatusHTML(Writer out) throws IOException {
StringBuffer buf = new StringBuffer(1024);
int outbound = 0;
int inbound = 0;
synchronized (_connectionLock) {
long offsetTotal = 0;
buf.append("<b>Connections (").append(_connectionsByIdent.size()).append("):</b><ul>\n");
for (Iterator iter = _connectionsByIdent.values().iterator(); iter.hasNext(); ) {
TCPConnection con = (TCPConnection)iter.next();
buf.append("<li>");
if (con.getIsOutbound()) {
outbound++;
buf.append("Outbound to ");
} else {
inbound++;
buf.append("Inbound from ");
}
buf.append(con.getRemoteRouterIdentity().getHash().toBase64().substring(0,6));
buf.append(": up for ").append(DataHelper.formatDuration(con.getLifetime()));
buf.append(" transferring at ");
long bps = con.getSendRate();
if (bps < 1024)
buf.append(bps).append("Bps");
else
buf.append((int)(bps/1024)).append("KBps");
buf.append(" with a ").append(con.getOffsetReceived()).append("ms clock offset");
buf.append("</li>\n");
offsetTotal += con.getOffsetReceived();
}
buf.append("</ul>\n");
buf.append("<b>Average clock skew, TCP peers: ");
if (_connectionsByIdent.size() > 0)
buf.append(offsetTotal / _connectionsByIdent.size()).append("ms</b><br />\n");
else
buf.append("n/a</b><br />\n");
buf.append("<b>Connections being built:</b><ul>\n");
for (Iterator iter = _pendingConnectionsByIdent.iterator(); iter.hasNext(); ) {
Hash peer = (Hash)iter.next();
buf.append("<li>");
buf.append(peer.toBase64().substring(0,6));
buf.append("</li>\n");
}
buf.append("</ul>\n");
buf.append("<b>Inbound: ").append(inbound).append(", Outbound: ").append(outbound).append("</b><br />\n");
}
buf.append("<b>Most recent connection errors:</b><ul>");
synchronized (_lastConnectionErrors) {
for (int i = _lastConnectionErrors.size()-1; i >= 0; i--) {
String msg = (String)_lastConnectionErrors.get(i);
buf.append("<li>").append(msg).append("</li>\n");
}
}
buf.append("</ul>");
out.write(buf.toString());
}
/**
* Cache the bid to reduce object churn
*/
private class SharedBid extends TransportBid {
public SharedBid(int ms) { super(); setLatencyMs(ms); }
public Transport getTransport() { return TCPTransport.this; }
public String toString() { return "TCP bid @ " + getLatencyMs(); }
}
}

View File

@ -1,144 +0,0 @@
<html><body>
<p>
OBSOLETE - see NTCP.
Implements the transport for communicating with other routers via TCP/IP.
</p>
<h1>Connection protocol</h1>
<p>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. <i>(+ implies concatenation)</i></p>
<h2>Common case:</h2>
<p><b>1) </b> <i>Alice to Bob</i>: <br />
<code>#bytesFollowing + #versions + v1 [+ v2 [etc]] + tag? + tagData + properties</code></p>
<p><b>2) </b> <i>Bob to Alice</i>: <br />
<code>#bytesFollowing + versionOk + #bytesIP + IP + tagOk? + nonce + properties</code></p>
<ul>
<li><code>#bytesFollowing</code> is a 2 byte unsigned integer specifying how many
bytes there are (after the current pair) in the line sent. 0xFFFF is reserved</li>
<li><code>#versions</code> is a 1 byte unsigned integer specifying how many
acceptable 1 byte version numbers follow (preferred value first).</li>
<li><code>v1</code> (etc) is a 1 byte unsigned integer specifying a protocol
version. The value 0x0 is not allowed.</li>
<li><code>tag?</code> is a 1 byte value specifying whether a tag follows - 0x0 means
no tag follows, 0x1 means a 32 byte tag follows.</li>
<li><code>tagData</code> is a 32 byte tag, if necessary</li>
<li><code>properties</code> is a name=value mapping, formatted as the other I2P
mappings (via {@link net.i2p.data.DataHelper#readProperties})</li>
<li><code>versionOk</code> is a 1 byte value specifying the protocol version
that is agreed upon, or 0x0 if no compatible protocol versions are available.</li>
<li><code>#bytesIP</code> is a 2 byte unsigned integer specifying how many bytes
following make up the IP address</li>
<li><code>IP</code> is made up of <code>#bytesIP</code> bytes formatting the
peer who established the connection's IP address as a string (e.g. "192.168.1.1")</li>
<li><code>tagOk?</code> is a 1 byte value specifying whether the tag provided
is available for use - 0x0 means no, 0x1 means yes.</li>
<li><code>nonce</code> is a 4 byte random value</li>
</ul>
<p>Whether or not the <code>tagData</code> 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
<code>properties</code> mappings are left for future expansion.</p>
<h2>Connection establishment with a valid tag:</h2>
<p>With a valid <code>tag</code> and <code>nonce</code> received, both Alice and
Bob load up the previously negotiated <code>sessionKey</code> and set the
<code>iv</code> to the first 16 bytes of <code>H(tag + nonce)</code>. The
remainder of the communication is AES256 encrypted per
{@link net.i2p.crypto.AESInputStream} and {@link net.i2p.crypto.AESOutputStream}</p>
<p><b>3) </b> <i>Alice to Bob</i>: <br />
<code>H(nonce)</code></p>
<p><b>4) </b> <i>Bob to Alice</i>: <br />
<code>H(tag)</code></p>
<p><b>5) </b> If the hashes are not correct, disconnect immediately and do not
consume the tag</p>
<p><b>6) </b> <i>Alice to Bob</i>: <br />
<code>routerInfo + currentTime + H(routerInfo + currentTime + nonce + tag)</code></p>
<p><b>7) </b> 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.</p>
<p><b>8) </b> <i>Bob to Alice</i>: <br />
<code>routerInfo + status + properties + H(routerInfo + status + properties + nonce + tag)</code></p>
<p><b>9) </b> If the <code>status</code> is ok, both Alice and Bob consume the
<code>tagData</code>, updating the next tag to be <code>H(E(nonce + tag, sessionKey))</code>
(with nonce+tag padded with 12 bytes of 0x0 at the end).
Otherwise, both sides disconnect and do not consume the tag. In addition, on error the
<code>properties</code> mapping has a more detailed reason under the key "MESSAGE".</p>
<ul>
<li><code>H(x)</code> is the SHA256 hash of x, formatted per {@link net.i2p.data.Hash#writeBytes}.</li>
<li><code>routerInfo</code> is the serialization of the local router's info
per {@link net.i2p.data.RouterInfo#writeBytes}.</li>
<li><code>currentTime</code> is what the local router thinks the current network time
is, formatted per {@link net.i2p.data.DataHelper#writeDate}.</li>
<li><code>status</code> is a 1 byte value:<ul>
<li><b>0x0</b> means OK</li>
<li><b>0x1</b> means Alice was not reachable</li>
<li><b>0x2</b> 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}).</li>
<li><b>0x3</b> means the signature is invalid (only used by steps 9 and 11 below)</li>
<li>Other values are currently undefined (yet fatal) errors</li>
</ul></li>
</ul>
<h2>Connection establishment without a vald tag:</h2>
<p><b>3) </b> <i>Alice to Bob</i> <br />
X</p>
<p><b>4) </b> <i>Bob to Alice</i> <br />
Y</p>
<p><b>5) </b> Both sides complete the Diffie-Hellman exchange, setting the
<code>sessionKey</code> to the first 32 bytes of the result (e.g. (X^y mod p)),
<code>iv</code> to the next 16 bytes, and the <code>nextTag</code> 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}</p>
<p><b>6) </b> <i>Alice to Bob</i> <br />
<code>H(nonce)</code></p>
<p><b>7) </b> <i>Bob to Alice</i> <br />
<code>H(nextTag)</code></p>
<p><b>8) </b> If they disagree, disconnect immediately and do not persist the tags or keys</p>
<p><b>9) </b> <i>Alice to Bob</i> <br />
<code>routerInfo + currentTime
+ S(routerInfo + currentTime + nonce + nextTag, routerIdent.signingKey)</code></p>
<p><b>10) </b> 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.</p>
<p><b>11) </b> <i>Bob to Alice</i> <br />
<code>routerInfo + status + properties
+ S(routerInfo + status + properties + nonce + nextTag, routerIdent.signingKey)</code></p>
<p><b>12) </b> If the signature matches on both sides and <code>status</code> is ok, both sides
save the <code>sessionKey</code> negotiated as well as the <code>nextTag</code>.
Otherwise, the keys and tags are discarded and both sides drop the connection.</p>
<ul>
<li><code>X</code> is a 256 byte unsigned integer in 2s complement, representing
</code>g^x mod p</code> (where <code>g</code> and <code>p</code> are defined
in {@link net.i2p.crypto.CryptoConstants} and x is a randomly chosen value</li>
<li><code>Y</code> is a 256 byte unsigned integer in 2s complement, representing
</code>g^y mod p</code> (where <code>g</code> and <code>p</code> are defined
in {@link net.i2p.crypto.CryptoConstants} and y is a randomly chosen value</li>
<li><code>S(val, key)</code> is the DSA signature of the <code>val</code> using the
given signing <code>key</code> (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}.</li>
</ul>
<h2>Peer testing</h2>
<p>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)</p>
<p><b>1) </b> <i>Bob to Alice</i> <br />
<code>0xFFFF + #versions + v1 [+ v2 [etc]] + properties</p>
<p><b>2) </b> <i>Alice to Bob</i> <br />
<code>0xFFFF + versionOk + #bytesIP + IP + currentTime + properties</code></p>
<p><b>3) </b> Both sides close the socket</p>
</body></html>

View File

@ -1,224 +0,0 @@
package net.i2p.router.tunnel.pool;
import net.i2p.data.Certificate;
import net.i2p.data.DataHelper;
import net.i2p.data.Hash;
import net.i2p.data.RouterIdentity;
import net.i2p.data.RouterInfo;
import net.i2p.data.TunnelId;
import net.i2p.data.i2np.DeliveryInstructions;
import net.i2p.data.i2np.GarlicMessage;
import net.i2p.data.i2np.I2NPMessage;
import net.i2p.data.i2np.TunnelCreateMessage;
import net.i2p.data.i2np.TunnelCreateStatusMessage;
import net.i2p.data.i2np.TunnelGatewayMessage;
import net.i2p.router.HandlerJobBuilder;
import net.i2p.router.Job;
import net.i2p.router.JobImpl;
import net.i2p.router.Router;
import net.i2p.router.RouterContext;
import net.i2p.router.message.GarlicMessageBuilder;
import net.i2p.router.message.PayloadGarlicConfig;
import net.i2p.router.message.SendMessageDirectJob;
import net.i2p.router.peermanager.TunnelHistory;
import net.i2p.router.tunnel.HopConfig;
import net.i2p.util.Log;
/**
* Receive a request to join a tunnel, and if we aren't overloaded (per the
* throttle), join it (updating the tunnelDispatcher), then send back the
* agreement. Even if we are overloaded, send back a reply stating how
* overloaded we are.
*
*/
public class HandleTunnelCreateMessageJob extends JobImpl {
private Log _log;
private TunnelCreateMessage _request;
private boolean _alreadySearched;
/** job builder to redirect all tunnelCreateMessages through this job type */
static class Builder implements HandlerJobBuilder {
private RouterContext _ctx;
public Builder(RouterContext ctx) { _ctx = ctx; }
public Job createJob(I2NPMessage receivedMessage, RouterIdentity from, Hash fromHash) {
return new HandleTunnelCreateMessageJob(_ctx, (TunnelCreateMessage)receivedMessage);
}
}
public HandleTunnelCreateMessageJob(RouterContext ctx, TunnelCreateMessage msg) {
super(ctx);
_log = ctx.logManager().getLog(HandleTunnelCreateMessageJob.class);
_request = msg;
_alreadySearched = false;
}
private static final int STATUS_DEFERRED = 10000;
public String getName() { return "Handle tunnel join request"; }
public void runJob() {
if (_log.shouldLog(Log.DEBUG))
_log.debug("handle join request: " + _request);
int status = shouldAccept();
if (status == STATUS_DEFERRED) {
return;
} else if (status > 0) {
if (_log.shouldLog(Log.WARN))
_log.warn("reject(" + status + ") join request: " + _request);
sendRejection(status);
} else {
if (_log.shouldLog(Log.DEBUG))
_log.debug("accept join request: " + _request);
accept();
}
}
/** don't accept requests to join for 15 minutes or more */
public static final int MAX_DURATION_SECONDS = 15*60;
private int shouldAccept() {
// Should not see any initiation requests in hidden mode
if ("true".equalsIgnoreCase(getContext().getProperty(Router.PROP_HIDDEN, "false"))) {
return TunnelHistory.TUNNEL_REJECT_CRIT;
}
if (_request.getDurationSeconds() >= MAX_DURATION_SECONDS)
return TunnelHistory.TUNNEL_REJECT_CRIT;
Hash nextRouter = _request.getNextRouter();
if (nextRouter != null) {
RouterInfo ri = getContext().netDb().lookupRouterInfoLocally(nextRouter);
if (ri == null) {
if (_alreadySearched) // only search once
return TunnelHistory.TUNNEL_REJECT_TRANSIENT_OVERLOAD;
getContext().netDb().lookupRouterInfo(nextRouter, new DeferredAccept(getContext(), true), new DeferredAccept(getContext(), false), 5*1000);
_alreadySearched = true;
return STATUS_DEFERRED;
}
}
return getContext().throttle().acceptTunnelRequest();
}
private class DeferredAccept extends JobImpl {
private boolean _shouldAccept;
public DeferredAccept(RouterContext ctx, boolean shouldAccept) {
super(ctx);
_shouldAccept = shouldAccept;
}
public void runJob() {
HandleTunnelCreateMessageJob.this.runJob();
}
private static final String NAME_OK = "Deferred tunnel accept";
private static final String NAME_REJECT = "Deferred tunnel reject";
public String getName() { return _shouldAccept ? NAME_OK : NAME_REJECT; }
}
private void accept() {
byte recvId[] = new byte[4];
getContext().random().nextBytes(recvId);
HopConfig cfg = new HopConfig();
long expiration = _request.getDurationSeconds()*1000 + getContext().clock().now();
cfg.setCreation(getContext().clock().now());
cfg.setExpiration(expiration);
cfg.setIVKey(_request.getIVKey());
cfg.setLayerKey(_request.getLayerKey());
cfg.setOptions(_request.getOptions());
cfg.setReceiveTunnelId(recvId);
if (_request.getIsGateway()) {
if (_log.shouldLog(Log.INFO))
_log.info("join as inbound tunnel gateway pointing at "
+ _request.getNextRouter().toBase64().substring(0,4) + ":"
+ _request.getNextTunnelId()
+ " (nonce=" + _request.getNonce() + ")");
// serve as the inbound tunnel gateway
cfg.setSendTo(_request.getNextRouter());
TunnelId id = _request.getNextTunnelId();
if (id == null) {
sendRejection(TunnelHistory.TUNNEL_REJECT_CRIT);
return;
}
cfg.setSendTunnelId(DataHelper.toLong(4, id.getTunnelId()));
getContext().tunnelDispatcher().joinInboundGateway(cfg);
} else if (_request.getNextRouter() == null) {
if (_log.shouldLog(Log.INFO))
_log.info("join as outbound tunnel endpoint (nonce=" + _request.getNonce() + ")");
// serve as the outbound tunnel endpoint
getContext().tunnelDispatcher().joinOutboundEndpoint(cfg);
} else {
if (_log.shouldLog(Log.INFO))
_log.info("join as tunnel participant pointing at "
+ _request.getNextRouter().toBase64().substring(0,4) + ":"
+ _request.getNextTunnelId()
+ " (nonce=" + _request.getNonce() + ")");
// serve as a general participant
cfg.setSendTo(_request.getNextRouter());
TunnelId id = _request.getNextTunnelId();
if (id == null) {
sendRejection(TunnelHistory.TUNNEL_REJECT_CRIT);
return;
}
cfg.setSendTunnelId(DataHelper.toLong(4, id.getTunnelId()));
getContext().tunnelDispatcher().joinParticipant(cfg);
}
sendAcceptance(recvId);
}
private static final byte[] REJECTION_TUNNEL_ID = new byte[] { (byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF };
private static final int REPLY_TIMEOUT = 30*1000;
private static final int REPLY_PRIORITY = 500;
private void sendAcceptance(byte recvId[]) {
sendReply(recvId, TunnelCreateStatusMessage.STATUS_SUCCESS);
}
private void sendRejection(int severity) {
sendReply(REJECTION_TUNNEL_ID, severity);
}
private void sendReply(byte recvId[], int status) {
TunnelCreateStatusMessage reply = new TunnelCreateStatusMessage(getContext());
reply.setNonce(_request.getNonce());
reply.setReceiveTunnelId(new TunnelId(DataHelper.fromLong(recvId, 0, 4)));
reply.setStatus(status);
GarlicMessage msg = createReply(reply);
if (msg == null)
throw new RuntimeException("wtf, couldn't create reply? to " + _request);
TunnelGatewayMessage gw = new TunnelGatewayMessage(getContext());
gw.setMessage(msg);
gw.setTunnelId(_request.getReplyTunnel());
gw.setMessageExpiration(msg.getMessageExpiration());
if (_log.shouldLog(Log.DEBUG))
_log.debug("sending (" + status + ") to the tunnel "
+ _request.getReplyGateway().toBase64().substring(0,4) + ":"
+ _request.getReplyTunnel() + " wrt " + _request);
SendMessageDirectJob job = new SendMessageDirectJob(getContext(), gw, _request.getReplyGateway(),
REPLY_TIMEOUT, REPLY_PRIORITY);
// run it inline (adds to the outNetPool if it has the router info, otherwise queue a lookup)
job.runJob();
//getContext().jobQueue().addJob(job);
}
private GarlicMessage createReply(TunnelCreateStatusMessage reply) {
DeliveryInstructions instructions = new DeliveryInstructions();
instructions.setDeliveryMode(DeliveryInstructions.DELIVERY_MODE_LOCAL);
PayloadGarlicConfig cfg = new PayloadGarlicConfig();
cfg.setPayload(reply);
cfg.setCertificate(new Certificate(Certificate.CERTIFICATE_TYPE_NULL, null));
cfg.setDeliveryInstructions(instructions);
cfg.setRequestAck(false);
cfg.setExpiration(getContext().clock().now() + REPLY_TIMEOUT);
cfg.setId(getContext().random().nextLong(I2NPMessage.MAX_ID_VALUE));
GarlicMessage msg = GarlicMessageBuilder.buildMessage(getContext(), cfg,
null, // we dont care about the tags
null, // or keys sent
null, // and we don't know what public key to use
_request.getReplyKey(), _request.getReplyTag());
return msg;
}
}

View File

@ -1,48 +0,0 @@
package net.i2p.router.tunnel.pool;
import net.i2p.data.Hash;
import net.i2p.data.RouterIdentity;
import net.i2p.data.i2np.I2NPMessage;
import net.i2p.data.i2np.TunnelDataMessage;
import net.i2p.data.i2np.TunnelGatewayMessage;
import net.i2p.router.HandlerJobBuilder;
import net.i2p.router.Job;
import net.i2p.router.JobImpl;
import net.i2p.router.RouterContext;
/**
*
*/
public class TunnelMessageHandlerBuilder implements HandlerJobBuilder {
private RouterContext _context;
public TunnelMessageHandlerBuilder(RouterContext ctx) {
_context = ctx;
}
public Job createJob(I2NPMessage receivedMessage, RouterIdentity from, Hash fromHash) {
if ( (fromHash == null) && (from != null) )
fromHash = from.calculateHash();
return new HandleJob(_context, receivedMessage, fromHash);
}
private class HandleJob extends JobImpl {
private I2NPMessage _msg;
private Hash _from;
public HandleJob(RouterContext ctx, I2NPMessage msg, Hash from) {
super(ctx);
_msg = msg;
_from = from;
}
public void runJob() {
if (_msg instanceof TunnelGatewayMessage) {
getContext().tunnelDispatcher().dispatch((TunnelGatewayMessage)_msg);
} else if (_msg instanceof TunnelDataMessage) {
getContext().tunnelDispatcher().dispatch((TunnelDataMessage)_msg, _from);
}
}
public String getName() { return "Dispatch tunnel message"; }
}
}