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.data.router.RouterKeyGenerator;
import net.i2p.router.RouterContext;
import net.i2p.router.TunnelPoolSettings;
import net.i2p.router.peermanager.DBHistory;
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 = 20;
private static final int MAX = 10;
// multiplied by size - 1, will also get POINTS24 added
private static final double POINTS32 = 5.0;
// multiplied by size - 1, will also get POINTS16 added
private static final double POINTS24 = 5.0;
// multiplied by size - 1
private static final double POINTS16 = 0.25;
private static final double POINTS_US32 = 25.0;
private static final double POINTS_US24 = 25.0;
private static final double POINTS_US16 = 10.0;
private static final double POINTS_FAMILY = -2.0;
private static final double MIN_CLOSE = 242.0;
private static final double OUR_KEY_FACTOR = 4.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);
RouterKeyGenerator rkgen = _context.routerKeyGenerator();
Hash nkey = rkgen.getNextRoutingKey(us);
buf.append("Closest Floodfills to Tomorrow's Routing Key (Where we will Store our RI)
");
renderRouterInfoHTML(out, buf, nkey, avgMinDist, ris, points);
buf.append("Closest Floodfills to Our Router Hash (DHT Neighbors if we are Floodfill)
");
renderRouterInfoHTML(out, buf, us, avgMinDist, ris, points);
// Distance to our published destinations analysis
MapClosest floodfills to the Routing Key for " + DataHelper.escapeHTML(name) + " (where we store our LS)
");
renderRouterInfoHTML(out, buf, rkey, avgMinDist, ris, points);
nkey = rkgen.getNextRoutingKey(ls.getHash());
buf.append("Closest floodfills to Tomorrow's Routing Key for " + DataHelper.escapeHTML(name) + " (where we will store our LS)
");
renderRouterInfoHTML(out, buf, nkey, avgMinDist, ris, points);
}
// TODO Profile analysis
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); } String b2 = p.r2.getHash().toBase64(); addPoints(points, p.r1.getHash(), point, "Very close (" + fmt.format(distance) + ") to other floodfill " + b2 + ""); String b1 = p.r1.getHash().toBase64(); addPoints(points, p.r2.getHash(), point, "Very close (" + fmt.format(distance) + ") to other floodfill " + b1 + ""); } out.write(buf.toString()); out.flush(); buf.setLength(0); } private double closestDistance(Hash h, List"); if (ip[2] == ourIP[2]) { if (ip[3] == ourIP[3]) { buf.append("Same IP as us"); addPoints(points, info.getHash(), POINTS_US32, "Same IP as us"); } else { buf.append("Same /24 as us"); addPoints(points, info.getHash(), POINTS_US24, "Same /24 as us"); } } else { buf.append("Same /16 as us"); addPoints(points, info.getHash(), POINTS_US16, "Same /16 as us"); } buf.append(":
"); renderRouterInfo(buf, info, null, false, false); found = true; } } if (!found) buf.append("None
"); out.write(buf.toString()); out.flush(); buf.setLength(0); } private void renderIPGroups32(Writer out, StringBuilder buf, 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, "Same IP with " + (count - 1) + " other" + (( count > 2) ? "s" : "")); } } 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; found = true; renderRouterInfo(buf, info, null, false, false); double point = POINTS24 * (count - 1); addPoints(points, info.getHash(), point, "Same /24 IP with " + (count - 1) + " other" + (( count > 2) ? "s" : "")); } } if (!found) buf.append("None
"); out.write(buf.toString()); out.flush(); buf.setLength(0); } 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; found = true; // limit display //renderRouterInfo(buf, info, null, false, false); double point = POINTS16 * (count - 1); addPoints(points, info.getHash(), point, "Same /16 IP with " + (count - 1) + " other" + (( count > 2) ? "s" : "")); } } if (!found) buf.append("None
"); out.write(buf.toString()); out.flush(); buf.setLength(0); } private void renderIPGroupsFamily(Writer out, StringBuilder buf, List").append(count).append(" floodfills in declared family \"").append(DataHelper.escapeHTML(s) + '"') .append("
"); for (RouterInfo info : ris) { String fam = info.getOption("family"); if (fam == null) continue; if (!fam.equals(s)) continue; found = true; // limit display //renderRouterInfo(buf, info, null, false, false); double point = POINTS_FAMILY; if (count > 1) addPoints(points, info.getHash(), point, "Same declared family \"" + DataHelper.escapeHTML(s) + "\" with " + (count - 1) + " other" + (( count > 2) ? "s" : "")); else addPoints(points, info.getHash(), point, "Declared family \"" + DataHelper.escapeHTML(s) + '"'); } } if (!found) buf.append("None
"); 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 *= OUR_KEY_FACTOR; addPoints(points, ri.getHash(), point, "Very close (" + fmt.format(dist) + ") 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("Router") + ": ").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 fam = info.getOption("family"); if (fam != null) buf.append("Family: ").append(DataHelper.escapeHTML(fam)).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 = Math.max(now - heard, 1); buf.append("First heard about: ") .append(_t("{0} ago", DataHelper.formatDuration2(age))).append(" \n"); } heard = prof.getLastHeardAbout(); if (heard > 0) { long age = Math.max(now - heard, 1); buf.append("Last heard about: ") .append(_t("{0} ago", DataHelper.formatDuration2(age))).append(" \n"); } heard = prof.getLastHeardFrom(); if (heard > 0) { long age = Math.max(now - heard, 1); buf.append("Last heard from: ") .append(_t("{0} ago", DataHelper.formatDuration2(age))).append(" \n"); } DBHistory dbh = prof.getDBHistory(); if (dbh != null) { heard = dbh.getLastLookupSuccessful(); if (heard > 0) { long age = Math.max(now - heard, 1); buf.append("Last lookup successful: ") .append(_t("{0} ago", DataHelper.formatDuration2(age))).append(" \n"); } heard = dbh.getLastLookupFailed(); if (heard > 0) { long age = Math.max(now - heard, 1); buf.append("Last lookup failed: ") .append(_t("{0} ago", DataHelper.formatDuration2(age))).append(" \n"); } heard = dbh.getLastStoreSuccessful(); if (heard > 0) { long age = Math.max(now - heard, 1); buf.append("Last store successful: ") .append(_t("{0} ago", DataHelper.formatDuration2(age))).append(" \n"); } heard = dbh.getLastStoreFailed(); if (heard > 0) { long age = Math.max(now - heard, 1); buf.append("Last store failed: ") .append(_t("{0} ago", DataHelper.formatDuration2(age))).append(" \n"); } } // any other profile stuff? } } long age = Math.max(now - info.getPublished(), 1); 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 { buf.append("").append(_t("Published")).append(": ") .append(_t("{0} ago", DataHelper.formatDuration2(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(" |