diff --git a/apps/routerconsole/java/src/net/i2p/router/web/NetDbHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/NetDbHelper.java
index 808297fb53..ba977a6e80 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/NetDbHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/NetDbHelper.java
@@ -19,7 +19,8 @@ public class NetDbHelper extends HelperBase {
_x("All Routers"), // 3
_x("All Routers with Full Stats"), // 4
"LeaseSet Debug", // 5
- _x("LeaseSets") }; // 6
+ _x("LeaseSets"), // 6
+ "Sybil" }; // 7
private static final String links[] =
{"", // 0
@@ -28,7 +29,8 @@ public class NetDbHelper extends HelperBase {
"?f=2", // 3
"?f=1", // 4
"?l=2", // 5
- "?l=1" }; // 6
+ "?l=1", // 6
+ "?f=3" }; // 7
public void setRouter(String r) {
if (r != null)
@@ -77,6 +79,8 @@ public class NetDbHelper extends HelperBase {
renderer.renderRouterInfoHTML(_out, _routerPrefix, _version, _country);
else if (_lease)
renderer.renderLeaseSetHTML(_out, _debug);
+ else if (_full == 3)
+ (new SybilRenderer(_context)).getNetDbSummary(_out);
else
renderer.renderStatusHTML(_out, _full);
} catch (IOException ioe) {
@@ -101,6 +105,8 @@ public class NetDbHelper extends HelperBase {
return 3;
if (_full == 1)
return 4;
+ if (_full == 3)
+ return 7;
return 0;
}
@@ -119,7 +125,7 @@ public class NetDbHelper extends HelperBase {
for (int i = 0; i < titles.length; i++) {
if (i == 2 && tab != 2)
continue; // can't nav to lookup
- if (i == 5 && !_context.getBooleanProperty(PROP_ADVANCED))
+ if ((i == 5 || i == 7) && !_context.getBooleanProperty(PROP_ADVANCED))
continue;
if (i == tab) {
// we are there
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/NetDbRenderer.java b/apps/routerconsole/java/src/net/i2p/router/web/NetDbRenderer.java
index 48c5c3a30d..c9a34ece06 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/NetDbRenderer.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/NetDbRenderer.java
@@ -40,7 +40,7 @@ import net.i2p.util.ObjectCounter;
import net.i2p.util.Translate;
import net.i2p.util.VersionComparator;
-public class NetDbRenderer {
+class NetDbRenderer {
private final RouterContext _context;
public NetDbRenderer (RouterContext ctx) {
@@ -69,7 +69,7 @@ public class NetDbRenderer {
_us = us;
}
public int compare(LeaseSet l, LeaseSet r) {
- return HashDistance.getDistance(_us, l.getRoutingKey()).subtract(HashDistance.getDistance(_us, r.getRoutingKey())).signum();
+ return HashDistance.getDistance(_us, l.getRoutingKey()).compareTo(HashDistance.getDistance(_us, r.getRoutingKey()));
}
}
@@ -266,7 +266,7 @@ public class NetDbRenderer {
* http://forums.sun.com/thread.jspa?threadID=597652
* @since 0.7.14
*/
- private static double biLog2(BigInteger a) {
+ public static double biLog2(BigInteger a) {
int b = a.bitLength() - 1;
double c = 0;
double d = 0.5;
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/SybilRenderer.java b/apps/routerconsole/java/src/net/i2p/router/web/SybilRenderer.java
new file mode 100644
index 0000000000..d2a3143aec
--- /dev/null
+++ b/apps/routerconsole/java/src/net/i2p/router/web/SybilRenderer.java
@@ -0,0 +1,640 @@
+package net.i2p.router.web;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.io.Writer;
+import java.math.BigInteger;
+import java.text.DecimalFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+import net.i2p.data.DataHelper;
+import net.i2p.data.Destination;
+import net.i2p.data.Hash;
+import net.i2p.data.LeaseSet;
+import net.i2p.data.router.RouterAddress;
+import net.i2p.data.router.RouterInfo;
+import net.i2p.router.RouterContext;
+import net.i2p.router.TunnelPoolSettings;
+import net.i2p.router.peermanager.PeerProfile;
+import net.i2p.router.tunnel.pool.TunnelPool;
+import net.i2p.router.util.HashDistance;
+import net.i2p.util.Log;
+import net.i2p.util.ObjectCounter;
+import net.i2p.util.Translate;
+import net.i2p.util.VersionComparator;
+
+/**
+ * For debugging only.
+ * Parts may later move to router as a periodic monitor.
+ * Adapted from NetDbRenderer.
+ *
+ * @since 0.9.24
+ *
+ */
+class SybilRenderer {
+
+ private final RouterContext _context;
+
+ private static final int PAIRMAX = 10;
+ private static final int MAX = 10;
+ // multiplied by size - 1
+ private static final double POINTS32 = 15.0;
+ // multiplied by size - 1
+ private static final double POINTS24 = 2.0;
+ private static final double MIN_CLOSE = 242.0;
+ private static final double MIN_DISPLAY_POINTS = 3.0;
+
+ public SybilRenderer(RouterContext ctx) {
+ _context = ctx;
+ }
+
+ /**
+ * Entry point
+ */
+ public String getNetDbSummary(Writer out) throws IOException {
+ renderRouterInfoHTML(out, (String)null);
+ return "";
+ }
+
+ private static class RouterInfoRoutingKeyComparator implements Comparator This is an experimental network database tool for debugging and analysis. Do not panic even if you see warnings below. " +
+ "Possible \"threats\" are summarized at the bottom, however these are unlikely to be real threats. " +
+ "If you see anything you would like to discuss with the devs, contact us on IRC #i2p-dev. Average closest floodfill distance: " + fmt.format(avgMinDist) + " Routing Data: \"").append(DataHelper.getUTF8(_context.routerKeyGenerator().getModData()))
+ .append("\" Last Changed: ").append(new Date(_context.routerKeyGenerator().getLastChanged()));
+ buf.append(" Next Routing Data: \"").append(DataHelper.getUTF8(_context.routerKeyGenerator().getNextModData()))
+ .append("\" Rotates in: ").append(DataHelper.formatDuration(_context.routerKeyGenerator().getTimeTillMidnight()));
+ buf.append(" Threat Points: " + fmt.format(p) + "No known floodfills
");
+ return;
+ }
+
+ StringBuilder buf = new StringBuilder(4*1024);
+ buf.append("Known Floodfills: ").append(ris.size()).append("
");
+
+ double tot = 0;
+ int count = 200;
+ byte[] b = new byte[32];
+ for (int i = 0; i < count; i++) {
+ _context.random().nextBytes(b);
+ Hash h = new Hash(b);
+ double d = closestDistance(h, ris);
+ tot += d;
+ }
+ DecimalFormat fmt = new DecimalFormat("#0.00");
+ double avgMinDist = tot / count;
+ buf.append("Closest Floodfills to Our Routing Key (Where we Store our RI)
");
+ renderRouterInfoHTML(out, buf, ourRKey, avgMinDist, ris, points);
+
+ buf.append("Closest Floodfills to Our Router Hash (DHT Neighbors if we are Floodfill)
");
+ renderRouterInfoHTML(out, buf, us, avgMinDist, ris, points);
+
+ MapClosest floodfills to the Routing Key for Our Destination " + DataHelper.escapeHTML(name) + " (where we store our LS)
");
+ renderRouterInfoHTML(out, buf, rkey, avgMinDist, ris, points);
+ }
+
+ if (!points.isEmpty()) {
+ ListRouters with Most Threat Points
");
+ for (Hash h : warns) {
+ RouterInfo ri = _context.netDb().lookupRouterInfoLocally(h);
+ if (h == null)
+ continue;
+ Points pp = points.get(h);
+ double p = pp.points;
+ if (p < MIN_DISPLAY_POINTS)
+ break; // sorted
+ buf.append("");
+ for (String s : pp.reasons) {
+ buf.append("
Hash Distance: ").append(fmt.format(distance)).append(": "); + buf.append("
"); + renderRouterInfo(buf, p.r1, null, false, false); + renderRouterInfo(buf, p.r2, null, false, false); + double point = MIN_CLOSE - distance; + if (point > 0) { + addPoints(points, p.r1.getHash(), point, fmt.format(point) + ": Too close to other floodfill " + p.r2.getHash().toBase64()); + addPoints(points, p.r2.getHash(), point, fmt.format(point) + ": Too close to other floodfill " + p.r1.getHash().toBase64()); + } + } + out.write(buf.toString()); + out.flush(); + buf.setLength(0); + } + + private double closestDistance(Hash h, List").append(count).append(" floodfills with IP ").append(i0).append('.') + .append(i1).append('.').append(i2).append('.').append(i3) + .append(":
"); + for (RouterInfo info : ris) { + byte[] ip = getIP(info); + if (ip == null) + continue; + if ((ip[0] & 0xff) != i0) + continue; + if ((ip[1] & 0xff) != i1) + continue; + if ((ip[2] & 0xff) != i2) + continue; + if ((ip[3] & 0xff) != i3) + continue; + found = true; + renderRouterInfo(buf, info, null, false, false); + double point = POINTS32 * (count - 1); + addPoints(points, info.getHash(), point, fmt.format(point) + ": Same IP with " + (count - 1) + " other"); + } + } + if (!found) + buf.append("None
"); + out.write(buf.toString()); + out.flush(); + buf.setLength(0); + } + + private void renderIPGroups24(Writer out, StringBuilder buf, List").append(count).append(" floodfills in ").append(i0).append('.') + .append(i1).append('.').append(i2).append(".0/24:
"); + for (RouterInfo info : ris) { + byte[] ip = getIP(info); + if (ip == null) + continue; + if ((ip[0] & 0xff) != i0) + continue; + if ((ip[1] & 0xff) != i1) + continue; + if ((ip[2] & 0xff) != i2) + continue; + renderRouterInfo(buf, info, null, false, false); + double point = POINTS24 * (count - 1); + addPoints(points, info.getHash(), point, fmt.format(point) + ": Same /24 IP with " + (count - 1) + " other"); + } + } + out.write(buf.toString()); + out.flush(); + buf.setLength(0); + } + + /** no points */ + private void renderIPGroups16(Writer out, StringBuilder buf, List").append(count).append(" floodfills in ").append(i0).append('.') + .append(i1).append(".0.0/16:
"); + for (RouterInfo info : ris) { + byte[] ip = getIP(info); + if (ip == null) + continue; + if ((ip[0] & 0xff) != i0) + continue; + if ((ip[1] & 0xff) != i1) + continue; + renderRouterInfo(buf, info, null, false, false); + } + } + out.write(buf.toString()); + out.flush(); + buf.setLength(0); + } + + private void renderRouterInfoHTML(Writer out, StringBuilder buf, Hash us, double avgMinDist, + ListNot to worry, but above router is closer than average minimum distance " + fmt.format(avgMinDist) + "
"); + } else if (i == 1) { + buf.append("Not to worry, but above routers are closer than average minimum distance " + fmt.format(avgMinDist) + "
"); + } else if (i == 2) { + buf.append("Possible Sybil Warning - above routers are closer than average minimum distance " + fmt.format(avgMinDist) + "
"); + } else { + buf.append("Major Sybil Warning - above router is closer than average minimum distance " + fmt.format(avgMinDist) + "
"); + } + } + // this is dumb because they are already sorted + if (dist < min) + min = dist; + if (dist > max) + max = dist; + tot += dist; + if (i == medIdx) + median = dist; + else if (i == medIdx + 1 && isEven) + median = (median + dist) / 2; + double point = MIN_CLOSE - dist; + if (point > 0) { + point *= 2.0; + addPoints(points, ri.getHash(), point, fmt.format(point) + ": Too close to our key " + us.toBase64()); + } + if (i >= MAX - 1) + break; + } + double avg = tot / count; + buf.append("Totals for " + count + " floodfills: MIN=" + fmt.format(min) + " AVG=" + fmt.format(avg) + " MEDIAN=" + fmt.format(median) + " MAX=" + fmt.format(max) + "
\n"); + out.write(buf.toString()); + out.flush(); + buf.setLength(0); + } + + /** + * For debugging + * http://forums.sun.com/thread.jspa?threadID=597652 + * @since 0.7.14 + */ + private static double biLog2(BigInteger a) { + return NetDbRenderer.biLog2(a); + } + + /** + * Countries now in a separate bundle + * @param code two-letter country code + * @since 0.9.9 + */ + private String getTranslatedCountry(String code) { + String name = _context.commSystem().getCountryName(code); + return Translate.getString(name, _context, Messages.COUNTRY_BUNDLE_NAME); + } + + /** + * Be careful to use stripHTML for any displayed routerInfo data + * to prevent vulnerabilities + * + * @param us ROUTING KEY or null + * @param full ignored + * @return distance to us if non-null, else 0 + */ + private double renderRouterInfo(StringBuilder buf, RouterInfo info, Hash us, boolean isUs, boolean full) { + String hash = info.getIdentity().getHash().toBase64(); + buf.append(""); + double distance = 0; + if (isUs) { + buf.append("" + _t("Our info") + ": ").append(hash).append(" |
---|
\n"); + } else { + buf.append("" + _t("Peer info for") + ": ").append(hash).append("\n"); + if (!full) { + buf.append("[").append(_t("Full entry")).append("]"); + } + buf.append(" |
\n");
+ if (us != null) {
+ DecimalFormat fmt = new DecimalFormat("#0.00");
+ BigInteger dist = HashDistance.getDistance(us, info.getHash());
+ distance = biLog2(dist);
+ buf.append("Hash Distance: ").append(fmt.format(distance)).append(" "); + } + } + buf.append("Routing Key: ").append(info.getRoutingKey().toBase64()).append(" \n"); + buf.append("Version: ").append(DataHelper.stripHTML(info.getVersion())).append(" \n"); + buf.append("Caps: ").append(DataHelper.stripHTML(info.getCapabilities())).append(" \n"); + String kls = info.getOption("netdb.knownLeaseSets"); + if (kls != null) + buf.append("Lease Sets: ").append(DataHelper.stripHTML(kls)).append(" \n"); + String kr = info.getOption("netdb.knownRouters"); + if (kr != null) + buf.append("Routers: ").append(DataHelper.stripHTML(kr)).append(" \n"); + + long now = _context.clock().now(); + if (!isUs) { + PeerProfile prof = _context.profileOrganizer().getProfileNonblocking(info.getHash()); + if (prof != null) { + long heard = prof.getFirstHeardAbout(); + if (heard > 0) { + long age = now - heard; + if (age > 0) { + buf.append("First heard about: ") + .append(_t("{0} ago", DataHelper.formatDuration2(age))).append(" \n"); + } else { + // shouldnt happen + buf.append("First heard about: in ").append(DataHelper.formatDuration2(0-age)).append("??? \n"); + } + } + // any other profile stuff? + } + } + long age = now - info.getPublished(); + if (isUs && _context.router().isHidden()) { + buf.append("").append(_t("Hidden")).append(", ").append(_t("Updated")).append(": ") + .append(_t("{0} ago", DataHelper.formatDuration2(age))).append(" \n"); + } else if (age > 0) { + buf.append("").append(_t("Published")).append(": ") + .append(_t("{0} ago", DataHelper.formatDuration2(age))).append(" \n"); + } else { + // shouldnt happen + buf.append("" + _t("Published") + ": in ").append(DataHelper.formatDuration2(0-age)).append("??? \n"); + } + buf.append("").append(_t("Signing Key")).append(": ") + .append(info.getIdentity().getSigningPublicKey().getType().toString()); + buf.append(" \n" + _t("Addresses") + ": "); + String country = _context.commSystem().getCountry(info.getIdentity().getHash()); + if(country != null) { + buf.append(" |