diff --git a/apps/i2psnark/java/build.xml b/apps/i2psnark/java/build.xml index a4fd04c063..5a4d3dd249 100644 --- a/apps/i2psnark/java/build.xml +++ b/apps/i2psnark/java/build.xml @@ -100,15 +100,15 @@ - - + + - + @@ -121,7 +121,7 @@ - + diff --git a/apps/i2psnark/java/src/org/klomp/snark/Peer.java b/apps/i2psnark/java/src/org/klomp/snark/Peer.java index ab15bb85ca..a7c4f4127a 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/Peer.java +++ b/apps/i2psnark/java/src/org/klomp/snark/Peer.java @@ -57,8 +57,8 @@ public class Peer implements Comparable private DataOutputStream dout; /** running counters */ - private long downloaded; - private long uploaded; + private final AtomicLong downloaded = new AtomicLong(); + private final AtomicLong uploaded = new AtomicLong(); // Keeps state for in/out connections. Non-null when the handshake // was successful, the connection setup and runs @@ -618,7 +618,7 @@ public class Peer implements Comparable * @since 0.8.4 */ public void downloaded(int size) { - downloaded += size; + downloaded.addAndGet(size); } /** @@ -626,7 +626,7 @@ public class Peer implements Comparable * @since 0.8.4 */ public void uploaded(int size) { - uploaded += size; + uploaded.addAndGet(size); } /** @@ -635,7 +635,7 @@ public class Peer implements Comparable */ public long getDownloaded() { - return downloaded; + return downloaded.get(); } /** @@ -644,7 +644,7 @@ public class Peer implements Comparable */ public long getUploaded() { - return uploaded; + return uploaded.get(); } /** @@ -652,8 +652,8 @@ public class Peer implements Comparable */ public void resetCounters() { - downloaded = 0; - uploaded = 0; + downloaded.set(0); + uploaded.set(0); } public long getInactiveTime() { diff --git a/apps/i2psnark/java/src/org/klomp/snark/Snark.java b/apps/i2psnark/java/src/org/klomp/snark/Snark.java index 740b7c2be4..2bb8413295 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/Snark.java +++ b/apps/i2psnark/java/src/org/klomp/snark/Snark.java @@ -27,7 +27,6 @@ import java.io.InputStream; import java.util.Collections; import java.util.List; import java.util.Properties; -import java.util.Random; import java.util.StringTokenizer; import net.i2p.I2PAppContext; @@ -245,16 +244,19 @@ public class Snark * * @deprecated unused */ +/**** Snark(I2PSnarkUtil util, String torrent, String ip, int user_port, StorageListener slistener, CoordinatorListener clistener) { this(util, torrent, ip, user_port, slistener, clistener, null, null, null, true, "."); } +****/ /** * single torrent - via router * * @deprecated unused */ +/**** public Snark(I2PAppContext ctx, Properties opts, String torrent, StorageListener slistener, boolean start, String rootDir) { this(new I2PSnarkUtil(ctx), torrent, null, -1, slistener, null, null, null, null, false, rootDir); @@ -284,6 +286,7 @@ public class Snark if (start) this.startTorrent(); } +****/ /** * multitorrent @@ -515,18 +518,13 @@ public class Snark // Create a new ID and fill it with something random. First nine // zeros bytes, then three bytes filled with snark and then - // sixteen random bytes. + // eight random bytes. byte snark = (((3 + 7 + 10) * (1000 - 8)) / 992) - 17; byte[] rv = new byte[20]; - Random random = I2PAppContext.getGlobalContext().random(); - int i; - for (i = 0; i < 9; i++) - rv[i] = 0; - rv[i++] = snark; - rv[i++] = snark; - rv[i++] = snark; - while (i < 20) - rv[i++] = (byte)random.nextInt(256); + rv[9] = snark; + rv[10] = snark; + rv[11] = snark; + I2PAppContext.getGlobalContext().random().nextBytes(rv, 12, 8); return rv; } @@ -958,6 +956,7 @@ public class Snark * non-valid argument list. The given listeners will be * passed to all components that take one. */ +/**** private static Snark parseArguments(String[] args, StorageListener slistener, CoordinatorListener clistener) @@ -972,6 +971,7 @@ public class Snark int i = 0; while (i < args.length) { +****/ /* if (args[i].equals("--debug")) { @@ -993,7 +993,9 @@ public class Snark catch (NumberFormatException nfe) { } } } - else */ if (args[i].equals("--port")) + else */ +/**** + if (args[i].equals("--port")) { if (args.length - 1 < i + 1) usage("--port needs port number to listen on"); @@ -1099,6 +1101,7 @@ public class Snark System.out.println (" \tor (with --share) a file to share."); } +****/ /** * Aborts program abnormally. diff --git a/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java index a6efd082ae..038e5437d8 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java +++ b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java @@ -61,7 +61,7 @@ public class I2PSnarkServlet extends BasicServlet { private static final String DEFAULT_NAME = "i2psnark"; public static final String PROP_CONFIG_FILE = "i2psnark.configFile"; - private static final String WARBASE = "/.icons/"; + private static final String WARBASE = "/.resources/"; private static final char HELLIP = '\u2026'; public I2PSnarkServlet() { @@ -194,22 +194,8 @@ public class I2PSnarkServlet extends BasicServlet { resp.setHeader("Content-Security-Policy", "default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'"); resp.setHeader("X-XSS-Protection", "1; mode=block"); - String peerParam = req.getParameter("p"); - String stParam = req.getParameter("st"); - String peerString; - if (peerParam == null || (!_manager.util().connected()) || - peerParam.replaceAll("[a-zA-Z0-9~=-]", "").length() > 0) { // XSS - peerString = ""; - } else { - peerString = "?p=" + DataHelper.stripHTML(peerParam); - } - if (stParam != null && !stParam.equals("0")) { - stParam = DataHelper.stripHTML(stParam); - if (peerString.length() > 0) - peerString += "&st=" + stParam; - else - peerString = "?st="+ stParam; - } + String pOverride = _manager.util().connected() ? null : ""; + String peerString = getQueryString(req, pOverride, null, null); // AJAX for mainsection if ("/.ajax/xhr1.html".equals(path)) { @@ -292,6 +278,7 @@ public class I2PSnarkServlet extends BasicServlet { out.write(_("Configuration")); else out.write(_("Anonymous BitTorrent Client")); + String peerParam = req.getParameter("p"); if ("2".equals(peerParam)) out.write(" | Debug Mode"); out.write("\n"); @@ -413,13 +400,7 @@ public class I2PSnarkServlet extends BasicServlet { boolean isForm = _manager.util().connected() || !snarks.isEmpty(); if (isForm) { out.write("
\n"); - out.write("\n"); - // don't lose peer setting - if (peerParam != null) - out.write("\n"); - // ...or st setting - if (stParam != null) - out.write("\n"); + writeHiddenInputs(out, req, null); } out.write(TABLE_HEADER); @@ -441,18 +422,29 @@ public class I2PSnarkServlet extends BasicServlet { } int pageSize = Math.max(_manager.getPageSize(), 5); - out.write(" 1; + out.write(""); + String sort = ("2".equals(currentSort)) ? "-2" : "2"; + if (showSort) { + out.write(""); + } + out.write("\"");\n"); + out.write("\">"); + if (showSort) + out.write(""); + out.write("\n"); if (_manager.util().connected() && !snarks.isEmpty()) { out.write(" "); out.write(""); } else { - out.write("?p=1"); - if (stParam != null) { - out.write("&st="); - out.write(stParam); - } + // enable peer view + out.write(getQueryString(req, "1", null, null)); out.write("\">"); out.write("
\n"); } out.write("\n"); + // cycle through sort by name or type + boolean isTypeSort = false; + if (showSort) { + if (currentSort == null || "0".equals(currentSort) || "1".equals(currentSort)) { + sort = "-1"; + } else if ("-1".equals(currentSort)) { + sort = "12"; + isTypeSort = true; + } else if ("12".equals(currentSort)) { + sort = "-12"; + isTypeSort = true; + } else { + sort = ""; + } + out.write("
"); + } out.write("\"");\n"); + out.write("\">"); + if (showSort) + out.write(""); + out.write("\n"); if (total > 0 && (start > 0 || total > pageSize)) { - writePageNav(out, start, pageSize, total, peerParam, noThinsp); + writePageNav(out, req, start, pageSize, total, noThinsp); } out.write("\n"); if (_manager.util().connected() && !snarks.isEmpty()) { + if (showSort) { + sort = ("4".equals(currentSort)) ? "-4" : "4"; + out.write(""); + } out.write("\"");"); + if (showSort) + out.write(""); } out.write("\n"); + // cycle through sort by size or downloaded + boolean isDlSort = false; + if (showSort) { + if ("5".equals(currentSort)) { + sort = "-5"; + } else if ("-5".equals(currentSort)) { + sort = "6"; + isDlSort = true; + } else if ("6".equals(currentSort)) { + sort = "-6"; + isDlSort = true; + } else { + sort = "5"; + } + out.write(""); + } out.write("\"");"); + if (showSort) + out.write(""); out.write("\n"); + boolean isRatSort = false; if (!snarks.isEmpty()) { + // cycle through sort by uploaded or ratio + boolean nextRatSort = false; + if (showSort) { + if ("7".equals(currentSort)) { + sort = "-7"; + } else if ("-7".equals(currentSort)) { + sort = "11"; + nextRatSort = true; + } else if ("11".equals(currentSort)) { + sort = "-11"; + nextRatSort = true; + isRatSort = true; + } else if ("-11".equals(currentSort)) { + sort = "7"; + isRatSort = true; + } else { + sort = "7"; + } + out.write(""); + } out.write("\"");"); + if (showSort) + out.write(""); } out.write("\n"); if (_manager.util().connected() && !snarks.isEmpty()) { + if (showSort) { + sort = ("8".equals(currentSort)) ? "-8" : "8"; + out.write(""); + } out.write("\"");"); + out.write("\">"); + if (showSort) + out.write(""); } out.write("\n"); if (_manager.util().connected() && !snarks.isEmpty()) { + if (showSort) { + sort = ("9".equals(currentSort)) ? "-9" : "9"; + out.write(""); + } out.write("\"");"); + out.write("\">"); + if (showSort) + out.write(""); } out.write("\n"); @@ -580,12 +671,11 @@ public class I2PSnarkServlet extends BasicServlet { String uri = _contextPath + '/'; boolean showDebug = "2".equals(peerParam); - String stParamStr = stParam == null ? "" : "&st=" + stParam; for (int i = 0; i < total; i++) { Snark snark = snarks.get(i); boolean showPeers = showDebug || "1".equals(peerParam) || Base64.encode(snark.getInfoHash()).equals(peerParam); boolean hide = i < start || i >= start + pageSize; - displaySnark(out, snark, uri, i, stats, showPeers, isDegraded, noThinsp, showDebug, hide, stParamStr); + displaySnark(out, req, snark, uri, i, stats, showPeers, isDegraded, noThinsp, showDebug, hide, isRatSort); } if (total == 0) { @@ -636,17 +726,105 @@ public class I2PSnarkServlet extends BasicServlet { return start == 0; } + /** + * hidden inputs for nonce and paramters p, st, and sort + * + * @param out writes to it + * @param action if non-null, add it as the action + * @since 0.9.16 + */ + private void writeHiddenInputs(PrintWriter out, HttpServletRequest req, String action) { + StringBuilder buf = new StringBuilder(256); + writeHiddenInputs(buf, req, action); + out.write(buf.toString()); + } + + /** + * hidden inputs for nonce and paramters p, st, and sort + * + * @param out appends to it + * @param action if non-null, add it as the action + * @since 0.9.16 + */ + private void writeHiddenInputs(StringBuilder buf, HttpServletRequest req, String action) { + buf.append("\n"); + String peerParam = req.getParameter("p"); + if (peerParam != null) { + buf.append("\n"); + } + String stParam = req.getParameter("st"); + if (stParam != null) { + buf.append("\n"); + } + String soParam = req.getParameter("sort"); + if (soParam != null) { + buf.append("\n"); + } + if (action != null) { + buf.append("\n"); + } + } + + /** + * Build HTML-escaped and stripped query string + * + * @param p override or "" for default or null to keep the same as in req + * @param st override or "" for default or null to keep the same as in req + * @param so override or "" for default or null to keep the same as in req + * @return non-null, possibly empty + * @since 0.9.16 + */ + private static String getQueryString(HttpServletRequest req, String p, String st, String so) { + StringBuilder buf = new StringBuilder(64); + if (p == null) { + p = req.getParameter("p"); + if (p != null) + p = DataHelper.stripHTML(p); + } + if (p != null && !p.equals("")) + buf.append("?p=").append(p); + if (so == null) { + so = req.getParameter("sort"); + if (so != null) + so = DataHelper.stripHTML(so); + } + if (so != null && !so.equals("")) { + if (buf.length() <= 0) + buf.append("?sort="); + else + buf.append("&sort="); + buf.append(so); + } + if (st == null) { + st = req.getParameter("st"); + if (st != null) + st = DataHelper.stripHTML(st); + } + if (st != null && !st.equals("")) { + if (buf.length() <= 0) + buf.append("?st="); + else + buf.append("&st="); + buf.append(st); + } + return buf.toString(); + } + /** * @since 0.9.6 */ - private void writePageNav(PrintWriter out, int start, int pageSize, int total, - String peerParam, boolean noThinsp) { + private void writePageNav(PrintWriter out, HttpServletRequest req, int start, int pageSize, int total, + boolean noThinsp) { // Page nav if (start > 0) { // First out.write("" + "\""" + @@ -655,9 +833,9 @@ public class I2PSnarkServlet extends BasicServlet { //if (prev > 0) { if (true) { // Back - out.write("  0) ? Integer.toString(prev) : ""; + out.write(getQueryString(req, null, sprev, null)); out.write("\">" + "\""" + @@ -690,9 +868,8 @@ public class I2PSnarkServlet extends BasicServlet { //if (next + pageSize < total) { if (true) { // Next - out.write(" " + "\""" + @@ -700,9 +877,8 @@ public class I2PSnarkServlet extends BasicServlet { } // Last int last = ((total - 1) / pageSize) * pageSize; - out.write(" " + "\""" + @@ -1190,34 +1366,22 @@ public class I2PSnarkServlet extends BasicServlet { return buf.toString(); } - /** - * Sort alphabetically in current locale, ignore case, ignore leading "the " - * (I guess this is worth it, a lot of torrents start with "The " - * @since 0.7.14 - */ - private static class TorrentNameComparator implements Comparator, Serializable { - - public int compare(Snark l, Snark r) { - // put downloads and magnets first - if (l.getStorage() == null && r.getStorage() != null) - return -1; - if (l.getStorage() != null && r.getStorage() == null) - return 1; - String ls = l.getBaseName(); - String llc = ls.toLowerCase(Locale.US); - if (llc.startsWith("the ") || llc.startsWith("the.") || llc.startsWith("the_")) - ls = ls.substring(4); - String rs = r.getBaseName(); - String rlc = rs.toLowerCase(Locale.US); - if (rlc.startsWith("the ") || rlc.startsWith("the.") || rlc.startsWith("the_")) - rs = rs.substring(4); - return Collator.getInstance().compare(ls, rs); - } - } - private List getSortedSnarks(HttpServletRequest req) { ArrayList rv = new ArrayList(_manager.getTorrents()); - Collections.sort(rv, new TorrentNameComparator()); + if (rv.size() > 1) { + int sort = 0; + String ssort = req.getParameter("sort"); + if (ssort != null) { + try { + sort = Integer.parseInt(ssort); + } catch (NumberFormatException nfe) {} + } + try { + Collections.sort(rv, Sorters.getComparator(sort, this)); + } catch (IllegalArgumentException iae) { + // Java 7 TimSort - may be unstable + } + } return rv; } @@ -1229,11 +1393,11 @@ public class I2PSnarkServlet extends BasicServlet { * * @param stats in/out param (totals) * @param statsOnly if true, output nothing, update stats only - * @param stParam non null; empty or e.g. &st=10 */ - private void displaySnark(PrintWriter out, Snark snark, String uri, int row, long stats[], boolean showPeers, + private void displaySnark(PrintWriter out, HttpServletRequest req, + Snark snark, String uri, int row, long stats[], boolean showPeers, boolean isDegraded, boolean noThinsp, boolean showDebug, boolean statsOnly, - String stParam) throws IOException { + boolean showRatios) throws IOException { // stats long uploaded = snark.getUploaded(); stats[0] += snark.getDownloaded(); @@ -1335,7 +1499,7 @@ public class I2PSnarkServlet extends BasicServlet { if (curPeers > 0 && !showPeers) statusString = "\"\"" + "" + txt + - ": " + + ": " + curPeers + thinsp(noThinsp) + ngettext("1 peer", "{0} peers", knownPeers) + ""; else @@ -1351,7 +1515,7 @@ public class I2PSnarkServlet extends BasicServlet { if (isRunning && curPeers > 0 && downBps > 0 && !showPeers) statusString = "\"\"" + "" + _("OK") + - ": " + + ": " + curPeers + thinsp(noThinsp) + ngettext("1 peer", "{0} peers", knownPeers) + ""; else if (isRunning && curPeers > 0 && downBps > 0) @@ -1362,7 +1526,7 @@ public class I2PSnarkServlet extends BasicServlet { else if (isRunning && curPeers > 0 && !showPeers) statusString = "\"\"" + "" + _("Stalled") + - ": " + + ": " + curPeers + thinsp(noThinsp) + ngettext("1 peer", "{0} peers", knownPeers) + ""; else if (isRunning && curPeers > 0) @@ -1466,8 +1630,17 @@ public class I2PSnarkServlet extends BasicServlet { // out.write("??"); // no meta size yet out.write("\n\t"); out.write(""); - if (isValid && uploaded > 0) - out.write(formatSize(uploaded)); + if (isValid) { + if (showRatios) { + if (total > 0) { + double ratio = uploaded / ((double) total); + out.write((new DecimalFormat("0.000")).format(ratio)); + out.write(" x"); + } + } else if (uploaded > 0) { + out.write(formatSize(uploaded)); + } + } out.write("\n\t"); out.write(""); if (isRunning && needed > 0) @@ -1484,7 +1657,8 @@ public class I2PSnarkServlet extends BasicServlet { } else if (isRunning) { // Stop Button if (isDegraded) - out.write("\n"); // *not* enctype="multipart/form-data", so that the input type=file sends the filename, not the file out.write("\n"); - out.write("\n"); - out.write("\n"); - // don't lose peer setting - String peerParam = req.getParameter("p"); - if (peerParam != null) - out.write("\n"); + writeHiddenInputs(out, req, "Add"); out.write("
"); out.write("\"\" "); out.write(_("Add Torrent")); @@ -1874,12 +2046,7 @@ public class I2PSnarkServlet extends BasicServlet { out.write("
\n"); // *not* enctype="multipart/form-data", so that the input type=file sends the filename, not the file out.write("\n"); - out.write("\n"); - out.write("\n"); - // don't lose peer setting - String peerParam = req.getParameter("p"); - if (peerParam != null) - out.write("\n"); + writeHiddenInputs(out, req, "Create"); out.write(""); out.write("\"\" "); out.write(_("Create Torrent")); @@ -1945,10 +2112,9 @@ public class I2PSnarkServlet extends BasicServlet { //int seedPct = 0; out.write("\n" + - "
\n" + - "\n" + - "\n" + - "" + + "
\n"); + writeHiddenInputs(out, req, "Save"); + out.write("" + "\"\" "); out.write(_("Configuration")); out.write("
\n" + @@ -2131,10 +2297,9 @@ public class I2PSnarkServlet extends BasicServlet { private void writeTrackerForm(PrintWriter out, HttpServletRequest req) throws IOException { StringBuilder buf = new StringBuilder(1024); buf.append("\n" + - "
\n" + - "\n" + - "\n" + - "" + + "
\n"); + writeHiddenInputs(buf, req, "Save2"); + buf.append("" + "\"\" "); buf.append(_("Trackers")); buf.append("
\n" + @@ -2435,6 +2600,10 @@ public class I2PSnarkServlet extends BasicServlet { // dummy r = new File(""); } + + boolean showPriority = snark != null && snark.getStorage() != null && !snark.getStorage().complete() && + r.isDirectory(); + StringBuilder buf=new StringBuilder(4096); buf.append(DOCTYPE).append(""); if (title.endsWith("/")) @@ -2442,8 +2611,14 @@ public class I2PSnarkServlet extends BasicServlet { String directory = title; title = _("Torrent") + ": " + DataHelper.escapeHTML(title); buf.append(title); - buf.append("").append(HEADER_A).append(_themePath).append(HEADER_B).append("" + - "\n
\n").append(HEADER_A).append(_themePath).append(HEADER_B) + .append("\n"); + if (showPriority) + buf.append("\n"); + buf.append("\n
\"\"  "); if (_contextName.equals(DEFAULT_NAME)) buf.append(_("I2PSnark")); @@ -2453,8 +2628,6 @@ public class I2PSnarkServlet extends BasicServlet { if (parent) // always true buf.append("
"); - boolean showPriority = snark != null && snark.getStorage() != null && !snark.getStorage().complete() && - r.isDirectory(); if (showPriority) { buf.append("\n"); buf.append("\n"); @@ -2594,9 +2767,20 @@ public class I2PSnarkServlet extends BasicServlet { .append(": ") .append((new DecimalFormat("0.00%")).format(completion)); else - buf.append(" \"\" ") - .append(_("Complete")); - // else unknown + buf.append(" \"\" ") + .append(_("Complete")).append(""); + // up ratio + buf.append(" \"\" ") + .append(_("Upload ratio")) + .append(": "); + long uploaded = snark.getUploaded(); + if (uploaded > 0) { + double ratio = uploaded / ((double) snark.getTotalLength()); + buf.append((new DecimalFormat("0.000")).format(ratio)); + buf.append(" x"); + } else { + buf.append('0'); + } long needed = snark.getNeededLength(); if (needed > 0) buf.append(" \"\" ") @@ -2781,19 +2965,19 @@ public class I2PSnarkServlet extends BasicServlet { if (showPriority) { buf.append(""); if ((!complete) && (!item.isDirectory())) { - buf.append(" 0) - buf.append("checked=\"true\""); + buf.append("checked=\"checked\""); buf.append('>').append(_("High")); - buf.append("').append(_("Normal")); - buf.append("').append(_("Skip")); showSaveButton = true; } @@ -2802,9 +2986,16 @@ public class I2PSnarkServlet extends BasicServlet { buf.append("\n"); } if (showSaveButton) { - buf.append(" \n"); + buf.append(" " + + "") + .append(toImg("clock_red")).append(_("Set all high")).append("\n" + + "") + .append(toImg("clock")).append(_("Set all normal")).append("\n" + + "") + .append(toImg("cancel")).append(_("Skip all")).append("\n" + + "

\n" + + "\n"); } buf.append("\n"); if (showPriority) @@ -2814,7 +3005,12 @@ public class I2PSnarkServlet extends BasicServlet { return buf.toString(); } - /** @since 0.7.14 */ + /** + * Pick an icon; try to catch the common types in an i2p environment. + * + * @return file name not including ".png" + * @since 0.7.14 + */ private String toIcon(File item) { if (item.isDirectory()) return "folder"; @@ -2823,10 +3019,12 @@ public class I2PSnarkServlet extends BasicServlet { /** * Pick an icon; try to catch the common types in an i2p environment + * Pkg private for FileTypeSorter. + * * @return file name not including ".png" * @since 0.7.14 */ - private String toIcon(String path) { + String toIcon(String path) { String icon; // Note that for this to work well, our custom mime.properties file must be loaded. String plc = path.toLowerCase(Locale.US); @@ -2873,12 +3071,12 @@ public class I2PSnarkServlet extends BasicServlet { /** @since 0.7.14 */ private String toImg(String icon) { - return "\"\""; + return toImg(icon, ""); } /** @since 0.8.2 */ private String toImg(String icon, String altText) { - return "\"""; + return "\"""; } /** @since 0.8.1 */ diff --git a/apps/i2psnark/java/src/org/klomp/snark/web/Sorters.java b/apps/i2psnark/java/src/org/klomp/snark/web/Sorters.java new file mode 100644 index 0000000000..057400226a --- /dev/null +++ b/apps/i2psnark/java/src/org/klomp/snark/web/Sorters.java @@ -0,0 +1,341 @@ +package org.klomp.snark.web; + +import java.io.Serializable; +import java.text.Collator; +import java.util.Collections; +import java.util.Comparator; +import java.util.Locale; + +import org.klomp.snark.MetaInfo; +import org.klomp.snark.Snark; + +/** + * Comparators for various columns + * + * @since 0.9.16 from TorrentNameComparator, moved from I2PSnarkservlet + */ +class Sorters { + + /** + * Negative is reverse + * + *
    + *
  • 0, 1: Name + *
  • 2: Status + *
  • 3: Peers + *
  • 4: ETA + *
  • 5: Size + *
  • 6: Downloaded + *
  • 7: Uploaded + *
  • 8: Down rate + *
  • 9: Up rate + *
  • 10: Remaining (needed) + *
  • 11: Upload ratio + *
  • 11: File type + *
+ * + * @param servlet for file type callback only + */ + public static Comparator getComparator(int type, I2PSnarkServlet servlet) { + boolean rev = type < 0; + Comparator rv; + switch (type) { + + case -1: + case 0: + case 1: + default: + rv = new TorrentNameComparator(); + if (rev) + rv = Collections.reverseOrder(rv); + break; + + case -2: + case 2: + rv = new StatusComparator(rev); + break; + + case -3: + case 3: + rv = new PeersComparator(rev); + break; + + case -4: + case 4: + rv = new ETAComparator(rev); + break; + + case -5: + case 5: + rv = new SizeComparator(rev); + break; + + case -6: + case 6: + rv = new DownloadedComparator(rev); + break; + + case -7: + case 7: + rv = new UploadedComparator(rev); + break; + + case -8: + case 8: + rv = new DownRateComparator(rev); + break; + + case -9: + case 9: + rv = new UpRateComparator(rev); + break; + + case -10: + case 10: + rv = new RemainingComparator(rev); + break; + + case -11: + case 11: + rv = new RatioComparator(rev); + break; + + case -12: + case 12: + rv = new FileTypeComparator(rev, servlet); + break; + + } + return rv; + } + + + /** + * Sort alphabetically in current locale, ignore case, ignore leading "the " + * (I guess this is worth it, a lot of torrents start with "The " + * @since 0.7.14 + */ + private static class TorrentNameComparator implements Comparator, Serializable { + + public int compare(Snark l, Snark r) { + return comp(l, r); + } + + public static int comp(Snark l, Snark r) { + // put downloads and magnets first + if (l.getStorage() == null && r.getStorage() != null) + return -1; + if (l.getStorage() != null && r.getStorage() == null) + return 1; + String ls = l.getBaseName(); + String llc = ls.toLowerCase(Locale.US); + if (llc.startsWith("the ") || llc.startsWith("the.") || llc.startsWith("the_")) + ls = ls.substring(4); + String rs = r.getBaseName(); + String rlc = rs.toLowerCase(Locale.US); + if (rlc.startsWith("the ") || rlc.startsWith("the.") || rlc.startsWith("the_")) + rs = rs.substring(4); + return Collator.getInstance().compare(ls, rs); + } + } + + /** + * Forward or reverse sort, but the fallback is always forward + */ + private static abstract class Sort implements Comparator, Serializable { + + private final boolean _rev; + + public Sort(boolean rev) { + _rev = rev; + } + + public int compare(Snark l, Snark r) { + int rv = compareIt(l, r); + if (rv != 0) + return _rev ? 0 - rv : rv; + return TorrentNameComparator.comp(l, r); + } + + protected abstract int compareIt(Snark l, Snark r); + + protected static int compLong(long l, long r) { + if (l < r) + return -1; + if (l > r) + return 1; + return 0; + } + } + + + private static class StatusComparator extends Sort { + + private StatusComparator(boolean rev) { super(rev); } + + public int compareIt(Snark l, Snark r) { + int rv = getStatus(l) - getStatus(r); + if (rv != 0) + return rv; + // use reverse remaining as first tie break + return compLong(r.getNeededLength(), l.getNeededLength()); + } + + private static int getStatus(Snark snark) { + long remaining = snark.getRemainingLength(); + long needed = snark.getNeededLength(); + if (snark.isStopped()) { + if (remaining < 0) + return 0; + if (remaining > 0) + return 5; + return 10; + } + if (snark.isStarting()) + return 15; + if (snark.isAllocating()) + return 20; + if (remaining < 0) + return 15; // magnet + if (remaining == 0) + return 100; + if (snark.isChecking()) + return 95; + if (snark.getNeededLength() <= 0) + return 90; + if (snark.getPeerCount() <= 0) + return 40; + if (snark.getDownloadRate() <= 0) + return 50; + return 60; + } + } + + private static class PeersComparator extends Sort { + + public PeersComparator(boolean rev) { super(rev); } + + public int compareIt(Snark l, Snark r) { + return l.getPeerCount() - r.getPeerCount(); + } + } + + private static class RemainingComparator extends Sort { + + public RemainingComparator(boolean rev) { super(rev); } + + public int compareIt(Snark l, Snark r) { + return compLong(l.getNeededLength(), r.getNeededLength()); + } + } + + private static class ETAComparator extends Sort { + + public ETAComparator(boolean rev) { super(rev); } + + public int compareIt(Snark l, Snark r) { + return compLong(eta(l), eta(r)); + } + + private static long eta(Snark snark) { + long needed = snark.getNeededLength(); + if (needed <= 0) + return 0; + long total = snark.getTotalLength(); + if (needed > total) + needed = total; + long downBps = snark.getDownloadRate(); + if (downBps > 0) + return needed / downBps; + return Long.MAX_VALUE; + } + } + + private static class SizeComparator extends Sort { + + public SizeComparator(boolean rev) { super(rev); } + + public int compareIt(Snark l, Snark r) { + return compLong(l.getTotalLength(), r.getTotalLength()); + } + } + + private static class DownloadedComparator extends Sort { + + public DownloadedComparator(boolean rev) { super(rev); } + + public int compareIt(Snark l, Snark r) { + long ld = l.getTotalLength() - l.getRemainingLength(); + long rd = r.getTotalLength() - r.getRemainingLength(); + return compLong(ld, rd); + } + } + + private static class UploadedComparator extends Sort { + + public UploadedComparator(boolean rev) { super(rev); } + + public int compareIt(Snark l, Snark r) { + return compLong(l.getUploaded(), r.getUploaded()); + } + } + + private static class DownRateComparator extends Sort { + + public DownRateComparator(boolean rev) { super(rev); } + + public int compareIt(Snark l, Snark r) { + return compLong(l.getDownloadRate(), r.getDownloadRate()); + } + } + + private static class UpRateComparator extends Sort { + + public UpRateComparator(boolean rev) { super(rev); } + + public int compareIt(Snark l, Snark r) { + return compLong(l.getUploadRate(), r.getUploadRate()); + } + } + + private static class RatioComparator extends Sort { + + private static final long M = 128 * 1024 * 1024; + + public RatioComparator(boolean rev) { super(rev); } + + public int compareIt(Snark l, Snark r) { + long lt = l.getTotalLength(); + long ld = lt > 0 ? ((M * l.getUploaded()) / lt) : 0; + long rt = r.getTotalLength(); + long rd = rt > 0 ? ((M * r.getUploaded()) / rt) : 0; + return compLong(ld, rd); + } + } + + private static class FileTypeComparator extends Sort { + + private final I2PSnarkServlet servlet; + + public FileTypeComparator(boolean rev, I2PSnarkServlet servlet) { + super(rev); + this.servlet = servlet; + } + + public int compareIt(Snark l, Snark r) { + String ls = toName(l); + String rs = toName(r); + return ls.compareTo(rs); + } + + private String toName(Snark snark) { + MetaInfo meta = snark.getMetaInfo(); + if (meta == null) + return "0"; + if (meta.getFiles() != null) + return "1"; + // arbitrary sort based on icon name + return servlet.toIcon(meta.getName()); + } + } +} diff --git a/apps/i2psnark/mime.properties b/apps/i2psnark/mime.properties index b251fb72ea..fff1a696e9 100644 --- a/apps/i2psnark/mime.properties +++ b/apps/i2psnark/mime.properties @@ -8,6 +8,7 @@ epub = application/epub+zip flac = audio/flac flv = video/x-flv iso = application/x-iso9660-image +js = text/javascript m4a = audio/mp4a-latm m4v = video/x-m4v mkv = video/x-matroska diff --git a/apps/i2psnark/icons/application.png b/apps/i2psnark/resources/icons/application.png similarity index 100% rename from apps/i2psnark/icons/application.png rename to apps/i2psnark/resources/icons/application.png diff --git a/apps/i2psnark/icons/basket_put.png b/apps/i2psnark/resources/icons/basket_put.png similarity index 100% rename from apps/i2psnark/icons/basket_put.png rename to apps/i2psnark/resources/icons/basket_put.png diff --git a/apps/i2psnark/icons/cancel.png b/apps/i2psnark/resources/icons/cancel.png similarity index 100% rename from apps/i2psnark/icons/cancel.png rename to apps/i2psnark/resources/icons/cancel.png diff --git a/apps/i2psnark/icons/cd.png b/apps/i2psnark/resources/icons/cd.png similarity index 100% rename from apps/i2psnark/icons/cd.png rename to apps/i2psnark/resources/icons/cd.png diff --git a/apps/i2psnark/icons/clock.png b/apps/i2psnark/resources/icons/clock.png similarity index 100% rename from apps/i2psnark/icons/clock.png rename to apps/i2psnark/resources/icons/clock.png diff --git a/apps/i2psnark/icons/clock_red.png b/apps/i2psnark/resources/icons/clock_red.png similarity index 100% rename from apps/i2psnark/icons/clock_red.png rename to apps/i2psnark/resources/icons/clock_red.png diff --git a/apps/i2psnark/icons/compress.png b/apps/i2psnark/resources/icons/compress.png similarity index 100% rename from apps/i2psnark/icons/compress.png rename to apps/i2psnark/resources/icons/compress.png diff --git a/apps/i2psnark/icons/film.png b/apps/i2psnark/resources/icons/film.png similarity index 100% rename from apps/i2psnark/icons/film.png rename to apps/i2psnark/resources/icons/film.png diff --git a/apps/i2psnark/icons/folder.png b/apps/i2psnark/resources/icons/folder.png similarity index 100% rename from apps/i2psnark/icons/folder.png rename to apps/i2psnark/resources/icons/folder.png diff --git a/apps/i2psnark/icons/html.png b/apps/i2psnark/resources/icons/html.png similarity index 100% rename from apps/i2psnark/icons/html.png rename to apps/i2psnark/resources/icons/html.png diff --git a/apps/i2psnark/icons/magnet.png b/apps/i2psnark/resources/icons/magnet.png similarity index 100% rename from apps/i2psnark/icons/magnet.png rename to apps/i2psnark/resources/icons/magnet.png diff --git a/apps/i2psnark/icons/music.png b/apps/i2psnark/resources/icons/music.png similarity index 100% rename from apps/i2psnark/icons/music.png rename to apps/i2psnark/resources/icons/music.png diff --git a/apps/i2psnark/icons/package.png b/apps/i2psnark/resources/icons/package.png similarity index 100% rename from apps/i2psnark/icons/package.png rename to apps/i2psnark/resources/icons/package.png diff --git a/apps/i2psnark/icons/page.png b/apps/i2psnark/resources/icons/page.png similarity index 100% rename from apps/i2psnark/icons/page.png rename to apps/i2psnark/resources/icons/page.png diff --git a/apps/i2psnark/icons/page_white.png b/apps/i2psnark/resources/icons/page_white.png similarity index 100% rename from apps/i2psnark/icons/page_white.png rename to apps/i2psnark/resources/icons/page_white.png diff --git a/apps/i2psnark/icons/page_white_acrobat.png b/apps/i2psnark/resources/icons/page_white_acrobat.png similarity index 100% rename from apps/i2psnark/icons/page_white_acrobat.png rename to apps/i2psnark/resources/icons/page_white_acrobat.png diff --git a/apps/i2psnark/icons/photo.png b/apps/i2psnark/resources/icons/photo.png similarity index 100% rename from apps/i2psnark/icons/photo.png rename to apps/i2psnark/resources/icons/photo.png diff --git a/apps/i2psnark/icons/plugin.png b/apps/i2psnark/resources/icons/plugin.png similarity index 100% rename from apps/i2psnark/icons/plugin.png rename to apps/i2psnark/resources/icons/plugin.png diff --git a/apps/i2psnark/icons/tick.png b/apps/i2psnark/resources/icons/tick.png similarity index 100% rename from apps/i2psnark/icons/tick.png rename to apps/i2psnark/resources/icons/tick.png diff --git a/apps/i2psnark/resources/js/folder.js b/apps/i2psnark/resources/js/folder.js new file mode 100644 index 0000000000..fbc906bf92 --- /dev/null +++ b/apps/i2psnark/resources/js/folder.js @@ -0,0 +1,93 @@ +function setupbuttons() { + updatesetallbuttons(); + var form = document.forms[0]; + form.savepri.disabled = true; + form.savepri.className = 'disabled'; +} + +function priorityclicked() { + updatesetallbuttons(); + var form = document.forms[0]; + form.savepri.disabled = false; + form.savepri.className = 'accept'; +} + +function updatesetallbuttons() { + var notNorm = false; + var notHigh = false; + var notSkip = false; + var form = document.forms[0]; + for(i = 0; i < form.elements.length; i++) { + var elem = form.elements[i]; + if (elem.type == 'radio') { + if (!elem.checked) { + if (elem.className == 'prinorm') + notNorm = true; + else if (elem.className == 'prihigh') + notHigh = true; + else + notSkip = true; + } + } + } + if (notNorm) + document.getElementById('setallnorm').className = 'control'; + else + document.getElementById('setallnorm').className = 'controld'; + if (notHigh) + document.getElementById('setallhigh').className = 'control'; + else + document.getElementById('setallhigh').className = 'controld'; + if (notSkip) + document.getElementById('setallskip').className = 'control'; + else + document.getElementById('setallskip').className = 'controld'; +} + +function setallnorm() { + var form = document.forms[0]; + for(i = 0; i < form.elements.length; i++) { + var elem = form.elements[i]; + if (elem.type == 'radio') { + if (elem.className === 'prinorm') + elem.checked = true; + } + } + document.getElementById('setallnorm').className = 'controld'; + document.getElementById('setallhigh').className = 'control'; + document.getElementById('setallskip').className = 'control'; + form.savepri.disabled = false; + form.savepri.className = 'accept'; +} + +function setallhigh() { + var form = document.forms[0]; + for(i = 0; i < form.elements.length; i++) { + var elem = form.elements[i]; + if (elem.type == 'radio') { + if (elem.className === 'prihigh') + elem.checked = true; + } + } + document.getElementById('setallnorm').className = 'control'; + document.getElementById('setallhigh').className = 'controld'; + document.getElementById('setallskip').className = 'control'; + form.savepri.disabled = false; + form.savepri.className = 'accept'; +} + +function setallskip() { + var form = document.forms[0]; + for(i = 0; i < form.elements.length; i++) { + var elem = form.elements[i]; + if (elem.type == 'radio') { + if (elem.className === 'priskip') + elem.checked = true; + } + } + document.getElementById('setallnorm').className = 'control'; + document.getElementById('setallhigh').className = 'control'; + document.getElementById('setallskip').className = 'controld'; + form.savepri.disabled = false; + form.savepri.className = 'accept'; +} diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClientBase.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClientBase.java index 4813fa2a17..858458e3ff 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClientBase.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClientBase.java @@ -601,9 +601,12 @@ public abstract class I2PTunnelHTTPClientBase extends I2PTunnelClientBase implem return; int status = ise != null ? ise.getStatus() : -1; String error; - //TODO MessageStatusMessage.STATUS_SEND_FAILURE_UNSUPPORTED_ENCRYPTION if (status == MessageStatusMessage.STATUS_SEND_FAILURE_NO_LEASESET) { + // We won't get this one unless it is treated as a hard failure + // in streaming. See PacketQueue.java error = usingWWWProxy ? "nolsp" : "nols"; + } else if (status == MessageStatusMessage.STATUS_SEND_FAILURE_UNSUPPORTED_ENCRYPTION) { + error = usingWWWProxy ? "encp" : "enc"; } else { error = usingWWWProxy ? "dnfp" : "dnf"; } diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java index 0597db5f19..b49f51a07f 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java @@ -188,8 +188,7 @@ public class EditBean extends IndexBean { /** @since 0.9.12 */ public boolean isSigTypeAvailable(int code) { - SigType type = SigType.getByCode(code); - return type != null && type.isAvailable(); + return SigType.isAvailable(code); } /** @since 0.8.9 */ diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigNetHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigNetHelper.java index efe9f4f819..7f13242d3d 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigNetHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigNetHelper.java @@ -5,7 +5,7 @@ import java.util.HashSet; import java.util.Set; import net.i2p.data.DataHelper; -import net.i2p.data.RouterAddress; +import net.i2p.data.router.RouterAddress; import net.i2p.router.CommSystemFacade; import net.i2p.router.Router; import net.i2p.router.transport.TransportManager; 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 9fae3ab935..b669f1447c 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/NetDbRenderer.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/NetDbRenderer.java @@ -29,8 +29,8 @@ import net.i2p.data.Destination; import net.i2p.data.Hash; import net.i2p.data.Lease; import net.i2p.data.LeaseSet; -import net.i2p.data.RouterAddress; -import net.i2p.data.RouterInfo; +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.util.HashDistance; // debug @@ -415,7 +415,9 @@ public class NetDbRenderer { // shouldnt happen buf.append("" + _("Published") + ": in ").append(DataHelper.formatDuration2(0-age)).append("???
\n"); } - buf.append("" + _("Address(es)") + ": "); + buf.append("").append(_("Signing Key")).append(": ") + .append(info.getIdentity().getSigningPublicKey().getType().toString()); + buf.append("
\n" + _("Address(es)") + ": "); String country = _context.commSystem().getCountry(info.getIdentity().getHash()); if(country != null) { buf.append("\"").append(country.toUpperCase(Locale.US)).append('\"');= 0) { // a retxed SYN succeeded before the first SYN failed diff --git a/build.xml b/build.xml index 9c9d9e4a55..f584aeab84 100644 --- a/build.xml +++ b/build.xml @@ -548,7 +548,7 @@ windowtitle="I2P Anonymous Network - Java Documentation - Version ${release.number}"> - + diff --git a/core/java/src/net/i2p/crypto/ECUtil.java b/core/java/src/net/i2p/crypto/ECUtil.java new file mode 100644 index 0000000000..8d22284804 --- /dev/null +++ b/core/java/src/net/i2p/crypto/ECUtil.java @@ -0,0 +1,135 @@ +package net.i2p.crypto; + +import java.math.BigInteger; +import java.security.spec.ECField; +import java.security.spec.ECFieldFp; +import java.security.spec.ECPoint; +import java.security.spec.EllipticCurve; + +import net.i2p.util.NativeBigInteger; + +/** + * Used by KeyGenerator.getSigningPublicKey() + * + * Modified from + * http://stackoverflow.com/questions/15727147/scalar-multiplication-of-point-over-elliptic-curve + * Apparently public domain. + * Supported P-192 only. + * Added curve parameters to support all curves. + * + * @since 0.9.16 + */ +class ECUtil { + + private static final BigInteger TWO = new BigInteger("2"); + private static final BigInteger THREE = new BigInteger("3"); + + public static ECPoint scalarMult(ECPoint p, BigInteger kin, EllipticCurve curve) { + ECPoint r = ECPoint.POINT_INFINITY; + BigInteger prime = ((ECFieldFp) curve.getField()).getP(); + BigInteger k = kin.mod(prime); + int length = k.bitLength(); + byte[] binarray = new byte[length]; + for (int i = 0; i <= length-1; i++) { + binarray[i] = k.mod(TWO).byteValue(); + k = k.divide(TWO); + } + + for (int i = length-1; i >= 0; i--) { + // i should start at length-1 not -2 because the MSB of binarry may not be 1 + r = doublePoint(r, curve); + if (binarray[i] == 1) + r = addPoint(r, p, curve); + } + return r; + } + + private static ECPoint addPoint(ECPoint r, ECPoint s, EllipticCurve curve) { + if (r.equals(s)) + return doublePoint(r, curve); + else if (r.equals(ECPoint.POINT_INFINITY)) + return s; + else if (s.equals(ECPoint.POINT_INFINITY)) + return r; + BigInteger prime = ((ECFieldFp) curve.getField()).getP(); + BigInteger slope = (r.getAffineY().subtract(s.getAffineY())).multiply(r.getAffineX().subtract(s.getAffineX()).modInverse(prime)).mod(prime); + slope = new NativeBigInteger(slope); + BigInteger xOut = (slope.modPow(TWO, prime).subtract(r.getAffineX())).subtract(s.getAffineX()).mod(prime); + BigInteger yOut = s.getAffineY().negate().mod(prime); + yOut = yOut.add(slope.multiply(s.getAffineX().subtract(xOut))).mod(prime); + ECPoint out = new ECPoint(xOut, yOut); + return out; + } + + private static ECPoint doublePoint(ECPoint r, EllipticCurve curve) { + if (r.equals(ECPoint.POINT_INFINITY)) + return r; + BigInteger slope = (r.getAffineX().pow(2)).multiply(THREE); + slope = slope.add(curve.getA()); + BigInteger prime = ((ECFieldFp) curve.getField()).getP(); + slope = slope.multiply((r.getAffineY().multiply(TWO)).modInverse(prime)); + BigInteger xOut = slope.pow(2).subtract(r.getAffineX().multiply(TWO)).mod(prime); + BigInteger yOut = (r.getAffineY().negate()).add(slope.multiply(r.getAffineX().subtract(xOut))).mod(prime); + ECPoint out = new ECPoint(xOut, yOut); + return out; + } + + /** + * P-192 test only. + * See KeyGenerator.main() for a test of all supported curves. + */ +/**** + public static void main(String[] args) { + EllipticCurve P192 = ECConstants.P192_SPEC.getCurve(); + BigInteger xs = new BigInteger("d458e7d127ae671b0c330266d246769353a012073e97acf8", 16); + BigInteger ys = new BigInteger("325930500d851f336bddc050cf7fb11b5673a1645086df3b", 16); + BigInteger xt = new BigInteger("f22c4395213e9ebe67ddecdd87fdbd01be16fb059b9753a4", 16); + BigInteger yt = new BigInteger("264424096af2b3597796db48f8dfb41fa9cecc97691a9c79", 16); + ECPoint S = new ECPoint(xs,ys); + ECPoint T = new ECPoint(xt,yt); + + // Verifying addition + ECPoint Rst = addPoint(S, T, P192); + BigInteger xst = new BigInteger("48e1e4096b9b8e5ca9d0f1f077b8abf58e843894de4d0290", 16); // Specified value of x of point R for addition in NIST Routine example + System.out.println("x-coordinate of point Rst is : " + Rst.getAffineX()); + System.out.println("y-coordinate of point Rst is : " + Rst.getAffineY()); + if (Rst.getAffineX().equals(xst)) + System.out.println("Adding is correct"); + else + System.out.println("Adding FAIL"); + + //Verifying Doubling + BigInteger xr = new BigInteger("30c5bc6b8c7da25354b373dc14dd8a0eba42d25a3f6e6962", 16); // Specified value of x of point R for doubling in NIST Routine example + BigInteger yr = new BigInteger("0dde14bc4249a721c407aedbf011e2ddbbcb2968c9d889cf", 16); + ECPoint R2s = new ECPoint(xr, yr); // Specified value of y of point R for doubling in NIST Routine example + System.out.println("x-coordinate of point R2s is : " + R2s.getAffineX()); + System.out.println("y-coordinate of point R2s is : " + R2s.getAffineY()); + System.out.println("x-coordinate of calculated point is : " + doublePoint(S, P192).getAffineX()); + System.out.println("y-coordinate of calculated point is : " + doublePoint(S, P192).getAffineY()); + if (R2s.getAffineX().equals(doublePoint(S, P192).getAffineX()) && + R2s.getAffineY().equals(doublePoint(S, P192).getAffineY())) + System.out.println("Doubling is correct"); + else + System.out.println("Doubling FAIL"); + + xr = new BigInteger("1faee4205a4f669d2d0a8f25e3bcec9a62a6952965bf6d31", 16); // Specified value of x of point R for scalar Multiplication in NIST Routine example + yr = new BigInteger("5ff2cdfa508a2581892367087c696f179e7a4d7e8260fb06", 16); // Specified value of y of point R for scalar Multiplication in NIST Routine example + ECPoint Rds = new ECPoint(xr, yr); + BigInteger d = new BigInteger("a78a236d60baec0c5dd41b33a542463a8255391af64c74ee", 16); + + ECPoint Rs = scalarMult(S, d, P192); + + System.out.println("x-coordinate of point Rds is : " + Rds.getAffineX()); + System.out.println("y-coordinate of point Rds is : " + Rds.getAffineY()); + System.out.println("x-coordinate of calculated point is : " + Rs.getAffineX()); + System.out.println("y-coordinate of calculated point is : " + Rs.getAffineY()); + + + if (Rds.getAffineX().equals(Rs.getAffineX()) && + Rds.getAffineY().equals(Rs.getAffineY())) + System.out.println("Scalar Multiplication is correct"); + else + System.out.println("Scalar Multiplication FAIL"); + } +****/ +} diff --git a/core/java/src/net/i2p/crypto/KeyGenerator.java b/core/java/src/net/i2p/crypto/KeyGenerator.java index f078aaa5c0..c23a215232 100644 --- a/core/java/src/net/i2p/crypto/KeyGenerator.java +++ b/core/java/src/net/i2p/crypto/KeyGenerator.java @@ -12,11 +12,25 @@ package net.i2p.crypto; import java.math.BigInteger; import java.security.GeneralSecurityException; import java.security.InvalidKeyException; +import java.security.KeyFactory; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.ProviderException; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPoint; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.EllipticCurve; +import java.security.spec.RSAKeyGenParameterSpec; +import java.security.spec.RSAPublicKeySpec; import net.i2p.I2PAppContext; +import net.i2p.crypto.eddsa.EdDSAPrivateKey; +import net.i2p.crypto.eddsa.EdDSAPublicKey; +import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec; import net.i2p.data.Hash; import net.i2p.data.PrivateKey; import net.i2p.data.PublicKey; @@ -268,24 +282,56 @@ public class KeyGenerator { } /** Convert a SigningPrivateKey to a SigningPublicKey. - * DSA-SHA1 only. + * As of 0.9.16, supports all key types. * * @param priv a SigningPrivateKey object * @return a SigningPublicKey object - * @throws IllegalArgumentException on bad key + * @throws IllegalArgumentException on bad key or unknown type */ public static SigningPublicKey getSigningPublicKey(SigningPrivateKey priv) { - if (priv.getType() != SigType.DSA_SHA1) - throw new IllegalArgumentException(); - BigInteger x = new NativeBigInteger(1, priv.toByteArray()); - BigInteger y = CryptoConstants.dsag.modPow(x, CryptoConstants.dsap); - SigningPublicKey pub = new SigningPublicKey(); + SigType type = priv.getType(); + if (type == null) + throw new IllegalArgumentException("Unknown type"); try { - pub.setData(SigUtil.rectify(y, SigningPublicKey.KEYSIZE_BYTES)); - } catch (InvalidKeyException ike) { - throw new IllegalArgumentException(ike); + switch (type.getBaseAlgorithm()) { + case DSA: + BigInteger x = new NativeBigInteger(1, priv.toByteArray()); + BigInteger y = CryptoConstants.dsag.modPow(x, CryptoConstants.dsap); + SigningPublicKey pub = new SigningPublicKey(); + pub.setData(SigUtil.rectify(y, SigningPublicKey.KEYSIZE_BYTES)); + return pub; + + case EC: + ECPrivateKey ecpriv = SigUtil.toJavaECKey(priv); + BigInteger s = ecpriv.getS(); + ECParameterSpec spec = (ECParameterSpec) type.getParams(); + EllipticCurve curve = spec.getCurve(); + ECPoint g = spec.getGenerator(); + ECPoint w = ECUtil.scalarMult(g, s, curve); + ECPublicKeySpec ecks = new ECPublicKeySpec(w, ecpriv.getParams()); + KeyFactory eckf = KeyFactory.getInstance("EC"); + ECPublicKey ecpub = (ECPublicKey) eckf.generatePublic(ecks); + return SigUtil.fromJavaKey(ecpub, type); + + case RSA: + RSAPrivateKey rsapriv = SigUtil.toJavaRSAKey(priv); + BigInteger exp = ((RSAKeyGenParameterSpec)type.getParams()).getPublicExponent(); + RSAPublicKeySpec rsaks = new RSAPublicKeySpec(rsapriv.getModulus(), exp); + KeyFactory rsakf = KeyFactory.getInstance("RSA"); + RSAPublicKey rsapub = (RSAPublicKey) rsakf.generatePublic(rsaks); + return SigUtil.fromJavaKey(rsapub, type); + + case EdDSA: + EdDSAPrivateKey epriv = SigUtil.toJavaEdDSAKey(priv); + EdDSAPublicKey epub = new EdDSAPublicKey(new EdDSAPublicKeySpec(epriv.getA(), epriv.getParams())); + return SigUtil.fromJavaKey(epub, type); + + default: + throw new IllegalArgumentException("Unsupported algorithm"); + } + } catch (GeneralSecurityException gse) { + throw new IllegalArgumentException("Conversion failed", gse); } - return pub; } public static void main(String args[]) { @@ -322,14 +368,20 @@ public class KeyGenerator { long stime = 0; long vtime = 0; SimpleDataStructure keys[] = KeyGenerator.getInstance().generateSigningKeys(type); - //System.out.println("pubkey " + keys[0]); + SigningPublicKey pubkey = (SigningPublicKey) keys[0]; + SigningPrivateKey privkey = (SigningPrivateKey) keys[1]; + SigningPublicKey pubkey2 = getSigningPublicKey(privkey); + if (pubkey.equals(pubkey2)) + System.out.println(type + " private-to-public test PASSED"); + else + System.out.println(type + " private-to-public test FAILED"); //System.out.println("privkey " + keys[1]); for (int i = 0; i < runs; i++) { RandomSource.getInstance().nextBytes(src); long start = System.nanoTime(); - Signature sig = DSAEngine.getInstance().sign(src, (SigningPrivateKey) keys[1]); + Signature sig = DSAEngine.getInstance().sign(src, privkey); long mid = System.nanoTime(); - boolean ok = DSAEngine.getInstance().verifySignature(sig, src, (SigningPublicKey) keys[0]); + boolean ok = DSAEngine.getInstance().verifySignature(sig, src, pubkey); long end = System.nanoTime(); stime += mid - start; vtime += end - mid; diff --git a/core/java/src/net/i2p/crypto/SigUtil.java b/core/java/src/net/i2p/crypto/SigUtil.java index cbf330ccb4..99159766d3 100644 --- a/core/java/src/net/i2p/crypto/SigUtil.java +++ b/core/java/src/net/i2p/crypto/SigUtil.java @@ -331,7 +331,7 @@ public class SigUtil { } /** - * @deprecated unused + * */ public static RSAPrivateKey toJavaRSAKey(SigningPrivateKey pk) throws GeneralSecurityException { @@ -344,7 +344,7 @@ public class SigUtil { } /** - * @deprecated unused + * */ public static SigningPublicKey fromJavaKey(RSAPublicKey pk, SigType type) throws GeneralSecurityException { diff --git a/core/java/src/net/i2p/data/DataHelper.java b/core/java/src/net/i2p/data/DataHelper.java index d3dc368583..72bfa1c37d 100644 --- a/core/java/src/net/i2p/data/DataHelper.java +++ b/core/java/src/net/i2p/data/DataHelper.java @@ -24,20 +24,15 @@ import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; -import java.io.Serializable; import java.io.UnsupportedEncodingException; import java.math.BigInteger; import java.security.MessageDigest; import java.text.DecimalFormat; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.Iterator; -import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Properties; @@ -638,13 +633,17 @@ public class DataHelper { * Integers are a fixed number of bytes (numBytes), stored as unsigned integers in network byte order. * @param value value to write out, non-negative * @param rawStream stream to write to - * @param numBytes number of bytes to write the number into (padding as necessary) - * @throws DataFormatException if value is negative + * @param numBytes number of bytes to write the number into, 1-8 (padding as necessary) + * @throws DataFormatException if value is negative or if numBytes not 1-8 * @throws IOException if there is an IO error writing to the stream */ public static void writeLong(OutputStream rawStream, int numBytes, long value) throws DataFormatException, IOException { - if (value < 0) throw new DataFormatException("Value is negative (" + value + ")"); + if (numBytes <= 0 || numBytes > 8) + // probably got the args backwards + throw new DataFormatException("Bad byte count " + numBytes); + if (value < 0) + throw new DataFormatException("Value is negative (" + value + ")"); for (int i = (numBytes - 1) * 8; i >= 0; i -= 8) { byte cur = (byte) (value >> i); rawStream.write(cur); @@ -1425,58 +1424,6 @@ public class DataHelper { out.write(data); } - /** - * Sort based on the Hash of the DataStructure. - * Warning - relatively slow. - * WARNING - this sort order must be consistent network-wide, so while the order is arbitrary, - * it cannot be changed. - * Why? Just because it has to be consistent so signing will work. - * How to spec as returning the same type as the param? - * DEPRECATED - Only used by RouterInfo. - * - * @return a new list - */ - public static List sortStructures(Collection dataStructures) { - if (dataStructures == null) return Collections.emptyList(); - - // This used to use Hash.toString(), which is insane, since a change to toString() - // would break the whole network. Now use Hash.toBase64(). - // Note that the Base64 sort order is NOT the same as the raw byte sort order, - // despite what you may read elsewhere. - - //ArrayList rv = new ArrayList(dataStructures.size()); - //TreeMap tm = new TreeMap(); - //for (DataStructure struct : dataStructures) { - // tm.put(struct.calculateHash().toString(), struct); - //} - //for (DataStructure struct : tm.values()) { - // rv.add(struct); - //} - ArrayList rv = new ArrayList(dataStructures); - sortStructureList(rv); - return rv; - } - - /** - * See above. - * DEPRECATED - Only used by RouterInfo. - * - * @since 0.9 - */ - static void sortStructureList(List dataStructures) { - Collections.sort(dataStructures, new DataStructureComparator()); - } - - /** - * See sortStructures() comments. - * @since 0.8.3 - */ - private static class DataStructureComparator implements Comparator, Serializable { - public int compare(DataStructure l, DataStructure r) { - return l.calculateHash().toBase64().compareTo(r.calculateHash().toBase64()); - } - } - /** * NOTE: formatDuration2() recommended in most cases for readability */ diff --git a/core/java/src/net/i2p/data/KeysAndCert.java b/core/java/src/net/i2p/data/KeysAndCert.java index b0a8a845b7..309799a01f 100644 --- a/core/java/src/net/i2p/data/KeysAndCert.java +++ b/core/java/src/net/i2p/data/KeysAndCert.java @@ -77,6 +77,13 @@ public class KeysAndCert extends DataStructureImpl { _signingKey = key; } + /** + * @since 0.9.16 + */ + public byte[] getPadding() { + return _padding; + } + /** * @throws IllegalStateException if was already set * @since 0.9.12 @@ -114,6 +121,8 @@ public class KeysAndCert extends DataStructureImpl { _publicKey.writeBytes(out); if (_padding != null) out.write(_padding); + else if (_signingKey.length() < SigningPublicKey.KEYSIZE_BYTES) + throw new DataFormatException("No padding set"); _signingKey.writeTruncatedBytes(out); _certificate.writeBytes(out); } diff --git a/core/java/src/net/i2p/data/PrivateKey.java b/core/java/src/net/i2p/data/PrivateKey.java index f10248189e..163edcc3f5 100644 --- a/core/java/src/net/i2p/data/PrivateKey.java +++ b/core/java/src/net/i2p/data/PrivateKey.java @@ -50,6 +50,7 @@ public class PrivateKey extends SimpleDataStructure { /** derives a new PublicKey object derived from the secret contents * of this PrivateKey * @return a PublicKey object + * @throws IllegalArgumentException on bad key */ public PublicKey toPublic() { return KeyGenerator.getPublicKey(this); diff --git a/core/java/src/net/i2p/data/PrivateKeyFile.java b/core/java/src/net/i2p/data/PrivateKeyFile.java index 012310b9fe..42a26ef367 100644 --- a/core/java/src/net/i2p/data/PrivateKeyFile.java +++ b/core/java/src/net/i2p/data/PrivateKeyFile.java @@ -1,11 +1,13 @@ package net.i2p.data; +import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; -import java.io.FileOutputStream; +import java.io.InputStream; import java.io.IOException; +import java.io.OutputStream; import java.security.GeneralSecurityException; import java.util.Locale; import java.util.Map; @@ -24,6 +26,7 @@ import net.i2p.crypto.DSAEngine; import net.i2p.crypto.KeyGenerator; import net.i2p.crypto.SigType; import net.i2p.util.RandomSource; +import net.i2p.util.SecureFileOutputStream; /** * This helper class reads and writes files in the @@ -48,11 +51,11 @@ public class PrivateKeyFile { private static final int HASH_EFFORT = VerifiedDestination.MIN_HASHCASH_EFFORT; - private final File file; + protected final File file; private final I2PClient client; - private Destination dest; - private PrivateKey privKey; - private SigningPrivateKey signingPrivKey; + protected Destination dest; + protected PrivateKey privKey; + protected SigningPrivateKey signingPrivKey; /** * Create a new PrivateKeyFile, or modify an existing one, with various @@ -224,6 +227,16 @@ public class PrivateKeyFile { */ public PrivateKeyFile(File file, PublicKey pubkey, SigningPublicKey spubkey, Certificate cert, PrivateKey pk, SigningPrivateKey spk) { + this(file, pubkey, spubkey, cert, pk, spk, null); + } + + /** + * @param padding null OK, must be non-null if spubkey length < 128 + * @throws IllegalArgumentException on mismatch of spubkey and spk types + * @since 0.9.16 + */ + public PrivateKeyFile(File file, PublicKey pubkey, SigningPublicKey spubkey, Certificate cert, + PrivateKey pk, SigningPrivateKey spk, byte[] padding) { if (spubkey.getType() != spk.getType()) throw new IllegalArgumentException("Signing key type mismatch"); this.file = file; @@ -232,6 +245,8 @@ public class PrivateKeyFile { this.dest.setPublicKey(pubkey); this.dest.setSigningPublicKey(spubkey); this.dest.setCertificate(cert); + if (padding != null) + this.dest.setPadding(padding); this.privKey = pk; this.signingPrivKey = spk; } @@ -241,9 +256,9 @@ public class PrivateKeyFile { */ public Destination createIfAbsent() throws I2PException, IOException, DataFormatException { if(!this.file.exists()) { - FileOutputStream out = null; + OutputStream out = null; try { - out = new FileOutputStream(this.file); + out = new SecureFileOutputStream(this.file); if (this.client != null) this.client.createDestination(out); else @@ -257,7 +272,10 @@ public class PrivateKeyFile { return getDestination(); } - /** Also sets the local privKey and signingPrivKey */ + /** + * If the destination is not set, read it in from the file. + * Also sets the local privKey and signingPrivKey. + */ public Destination getDestination() throws I2PSessionException, IOException, DataFormatException { if (dest == null) { I2PSession s = open(); @@ -408,9 +426,9 @@ public class PrivateKeyFile { } public I2PSession open(Properties opts) throws I2PSessionException, IOException { - FileInputStream in = null; + InputStream in = null; try { - in = new FileInputStream(this.file); + in = new BufferedInputStream(new FileInputStream(this.file)); I2PSession s = this.client.createSession(in, opts); return s; } finally { @@ -424,13 +442,12 @@ public class PrivateKeyFile { * Copied from I2PClientImpl.createDestination() */ public void write() throws IOException, DataFormatException { - FileOutputStream out = null; + OutputStream out = null; try { - out = new FileOutputStream(this.file); + out = new SecureFileOutputStream(this.file); this.dest.writeBytes(out); this.privKey.writeBytes(out); this.signingPrivKey.writeBytes(out); - out.flush(); } finally { if (out != null) { try { out.close(); } catch (IOException ioe) {} @@ -438,6 +455,23 @@ public class PrivateKeyFile { } } + /** + * Verify that the PublicKey matches the PrivateKey, and + * the SigningPublicKey matches the SigningPrivateKey. + * + * @return success + * @since 0.9.16 + */ + public boolean validateKeyPairs() { + try { + if (!dest.getPublicKey().equals(KeyGenerator.getPublicKey(privKey))) + return false; + return dest.getSigningPublicKey().equals(KeyGenerator.getSigningPublicKey(signingPrivKey)); + } catch (IllegalArgumentException iae) { + return false; + } + } + @Override public String toString() { StringBuilder s = new StringBuilder(128); diff --git a/core/java/src/net/i2p/data/SigningPrivateKey.java b/core/java/src/net/i2p/data/SigningPrivateKey.java index a8fcbb2081..07b8969e3c 100644 --- a/core/java/src/net/i2p/data/SigningPrivateKey.java +++ b/core/java/src/net/i2p/data/SigningPrivateKey.java @@ -75,8 +75,12 @@ public class SigningPrivateKey extends SimpleDataStructure { return _type; } - /** converts this signing private key to its public equivalent - * @return a SigningPublicKey object derived from this private key + /** + * Converts this signing private key to its public equivalent. + * As of 0.9.16, supports all key types. + * + * @return a SigningPublicKey object derived from this private key + * @throws IllegalArgumentException on bad key or unknown or unsupported type */ public SigningPublicKey toPublic() { return KeyGenerator.getSigningPublicKey(this); diff --git a/installer/resources/proxy/enc-header.ht b/installer/resources/proxy/enc-header.ht new file mode 100644 index 0000000000..e5c271e234 --- /dev/null +++ b/installer/resources/proxy/enc-header.ht @@ -0,0 +1,24 @@ +HTTP/1.1 504 Gateway Timeout +Content-Type: text/html; charset=UTF-8 +Cache-control: no-cache +Connection: close +Proxy-Connection: close + + + +_("Warning: Eepsite Unreachable") + + + + + +
+

_("Warning: Eepsite Unreachable")

+

+_("The eepsite was not reachable, because it uses encryption options that are not supported by your I2P or Java version.") +


+

_("Could not connect to the following destination:") +

diff --git a/installer/resources/proxy/encp-header.ht b/installer/resources/proxy/encp-header.ht new file mode 100644 index 0000000000..9d53fb7076 --- /dev/null +++ b/installer/resources/proxy/encp-header.ht @@ -0,0 +1,25 @@ +HTTP/1.1 504 Gateway Timeout +Content-Type: text/html; charset=UTF-8 +Cache-control: no-cache +Connection: close +Proxy-Connection: close + + + +_("Warning: Outproxy Unreachable") + + + + + +
+

_("Warning: Outproxy Unreachable")

+

+_("The HTTP outproxy was not reachable, because it uses encryption options that are not supported by your I2P or Java version.") +_("You may want to {0}retry{1} as this will randomly reselect an outproxy from the pool you have defined {2}here{3} (if you have more than one configured).", "", "", "", "") +_("If you continue to have trouble you may want to edit your outproxy list {0}here{1}.", "", "") +

+

_("Could not connect to the following destination:")

diff --git a/installer/resources/themes/snark/ubergine/snark.css b/installer/resources/themes/snark/ubergine/snark.css index 4a57291f5e..3eff3d0fc2 100644 --- a/installer/resources/themes/snark/ubergine/snark.css +++ b/installer/resources/themes/snark/ubergine/snark.css @@ -526,6 +526,40 @@ a:hover { font-weight: bold; } +a.control, a.controld { + background: #989; + border: 1px inset #bbb; + border-radius: 4px; + color: #000; + font-weight: bold; + margin: 2px 4px; + padding: 2px; + text-shadow: 0px 0px #410; + white-space: nowrap; +} + +a.controld { + color: #444; + font-weight: normal; +} + +a.controld img { + display: none; +} + +a.control:hover { + background-color: #f60; + border: 1px outset #bbb; + color: #fff; + text-shadow: 0px 1px 5px #f00; +} + +a.control:active { + background: #000 !important; + color: #f60 !important; + text-shadow: 0 !important; +} + input { font-size: 8.5pt; font-weight: bold; @@ -598,6 +632,14 @@ input[type=radio] { input.default { width: 1px; height: 1px; visibility: hidden; } +input.disabled, input.disabled:hover { + background-color: #989; + border: 1px inset #bbb; + color: #444; + font-weight: normal; + text-shadow: 0px 0px 0px #444; +} + input.accept { background: #989 url('../../console/images/accept.png') no-repeat 2px center; padding: 2px 3px 2px 20px !important; diff --git a/router/java/src/net/i2p/data/i2np/BuildRequestRecord.java b/router/java/src/net/i2p/data/i2np/BuildRequestRecord.java index 10533cfac8..805c8cc4d7 100644 --- a/router/java/src/net/i2p/data/i2np/BuildRequestRecord.java +++ b/router/java/src/net/i2p/data/i2np/BuildRequestRecord.java @@ -146,10 +146,10 @@ public class BuildRequestRecord { return (_data.getData()[_data.getOffset() + OFF_FLAG] & FLAG_OUTBOUND_ENDPOINT) != 0; } /** - * Time that the request was sent, truncated to the nearest hour + * Time that the request was sent (ms), truncated to the nearest hour */ public long readRequestTime() { - return DataHelper.fromLong(_data.getData(), _data.getOffset() + OFF_REQ_TIME, 4) * 60l * 60l * 1000l; + return DataHelper.fromLong(_data.getData(), _data.getOffset() + OFF_REQ_TIME, 4) * (60 * 60 * 1000L); } /** * What message ID should we send the request to the next hop with. If this is the outbound tunnel endpoint, @@ -250,6 +250,8 @@ public class BuildRequestRecord { else if (isOutEndpoint) buf[OFF_FLAG] |= FLAG_OUTBOUND_ENDPOINT; long truncatedHour = ctx.clock().now(); + // prevent hop identification at top of the hour + truncatedHour -= ctx.random().nextInt(90*1000); truncatedHour /= (60l*60l*1000l); DataHelper.toLong(buf, OFF_REQ_TIME, 4, truncatedHour); DataHelper.toLong(buf, OFF_SEND_MSG_ID, 4, nextMsgId); diff --git a/router/java/src/net/i2p/data/i2np/DatabaseLookupMessage.java b/router/java/src/net/i2p/data/i2np/DatabaseLookupMessage.java index 37ff20186c..790e376227 100644 --- a/router/java/src/net/i2p/data/i2np/DatabaseLookupMessage.java +++ b/router/java/src/net/i2p/data/i2np/DatabaseLookupMessage.java @@ -17,7 +17,7 @@ import java.util.Set; import net.i2p.I2PAppContext; import net.i2p.data.DataHelper; import net.i2p.data.Hash; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterInfo; import net.i2p.data.SessionKey; import net.i2p.data.SessionTag; import net.i2p.data.TunnelId; diff --git a/router/java/src/net/i2p/data/i2np/DatabaseStoreMessage.java b/router/java/src/net/i2p/data/i2np/DatabaseStoreMessage.java index 8e48a74399..b34912e8d8 100644 --- a/router/java/src/net/i2p/data/i2np/DatabaseStoreMessage.java +++ b/router/java/src/net/i2p/data/i2np/DatabaseStoreMessage.java @@ -18,7 +18,7 @@ import net.i2p.data.DataFormatException; import net.i2p.data.DataHelper; import net.i2p.data.Hash; import net.i2p.data.LeaseSet; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterInfo; import net.i2p.data.TunnelId; /** diff --git a/core/java/src/net/i2p/data/RouterAddress.java b/router/java/src/net/i2p/data/router/RouterAddress.java similarity index 98% rename from core/java/src/net/i2p/data/RouterAddress.java rename to router/java/src/net/i2p/data/router/RouterAddress.java index 960e495faf..ca0f94d08a 100644 --- a/core/java/src/net/i2p/data/RouterAddress.java +++ b/router/java/src/net/i2p/data/router/RouterAddress.java @@ -1,4 +1,4 @@ -package net.i2p.data; +package net.i2p.data.router; /* * free (adj.): unencumbered; not under the control of others @@ -17,6 +17,9 @@ import java.util.Date; import java.util.Map; import java.util.Properties; +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.data.DataStructureImpl; import net.i2p.util.Addresses; import net.i2p.util.OrderedProperties; @@ -36,6 +39,7 @@ import net.i2p.util.OrderedProperties; * several releases for the change to propagate as it is backwards-incompatible. * Restored as of 0.9.12. * + * @since 0.9.16 moved from net.i2p.data * @author jrandom */ public class RouterAddress extends DataStructureImpl { diff --git a/core/java/src/net/i2p/data/RouterIdentity.java b/router/java/src/net/i2p/data/router/RouterIdentity.java similarity index 89% rename from core/java/src/net/i2p/data/RouterIdentity.java rename to router/java/src/net/i2p/data/router/RouterIdentity.java index 346bb5f8d9..6dc7ca2d75 100644 --- a/core/java/src/net/i2p/data/RouterIdentity.java +++ b/router/java/src/net/i2p/data/router/RouterIdentity.java @@ -1,4 +1,7 @@ -package net.i2p.data; +package net.i2p.data.router; + +import net.i2p.data.Certificate; +import net.i2p.data.KeysAndCert; /* * free (adj.): unencumbered; not under the control of others @@ -16,6 +19,7 @@ package net.i2p.data; * As of 0.9.9 this data structure is immutable after the two keys and the certificate * are set; attempts to change them will throw an IllegalStateException. * + * @since 0.9.16 moved from net.i2p.data * @author jrandom */ public class RouterIdentity extends KeysAndCert { diff --git a/core/java/src/net/i2p/data/RouterInfo.java b/router/java/src/net/i2p/data/router/RouterInfo.java similarity index 98% rename from core/java/src/net/i2p/data/RouterInfo.java rename to router/java/src/net/i2p/data/router/RouterInfo.java index 22f9ce5ddc..390b013ec5 100644 --- a/core/java/src/net/i2p/data/RouterInfo.java +++ b/router/java/src/net/i2p/data/router/RouterInfo.java @@ -1,4 +1,4 @@ -package net.i2p.data; +package net.i2p.data.router; /* * free (adj.): unencumbered; not under the control of others @@ -31,6 +31,13 @@ import net.i2p.crypto.SHA1; import net.i2p.crypto.SHA1Hash; import net.i2p.crypto.SHA256Generator; import net.i2p.crypto.SigType; +import net.i2p.data.DatabaseEntry; +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.data.Hash; +import net.i2p.data.KeysAndCert; +import net.i2p.data.Signature; +import net.i2p.data.SimpleDataStructure; import net.i2p.util.Clock; import net.i2p.util.Log; import net.i2p.util.OrderedProperties; @@ -47,6 +54,7 @@ import net.i2p.util.SystemVersion; * To ensure integrity of the RouterInfo, methods that change an element of the * RouterInfo will throw an IllegalStateException after the RouterInfo is signed. * + * @since 0.9.16 moved from net.i2p.data * @author jrandom */ public class RouterInfo extends DatabaseEntry { @@ -190,7 +198,7 @@ public class RouterInfo extends DatabaseEntry { // WARNING this sort algorithm cannot be changed, as it must be consistent // network-wide. The signature is not checked at readin time, but only // later, and the addresses are stored in a Set, not a List. - DataHelper.sortStructureList(_addresses); + SortHelper.sortStructureList(_addresses); } } } @@ -308,7 +316,7 @@ public class RouterInfo extends DatabaseEntry { // WARNING this sort algorithm cannot be changed, as it must be consistent // network-wide. The signature is not checked at readin time, but only // later, and the hashes are stored in a Set, not a List. - peers = (Collection) DataHelper.sortStructures(peers); + peers = (Collection) SortHelper.sortStructures(peers); for (Hash peerHash : peers) { peerHash.writeBytes(out); } diff --git a/router/java/src/net/i2p/data/router/RouterPrivateKeyFile.java b/router/java/src/net/i2p/data/router/RouterPrivateKeyFile.java new file mode 100644 index 0000000000..2f466ca055 --- /dev/null +++ b/router/java/src/net/i2p/data/router/RouterPrivateKeyFile.java @@ -0,0 +1,62 @@ +package net.i2p.data.router; + + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.io.IOException; + +import net.i2p.crypto.SigType; +import net.i2p.data.DataFormatException; +import net.i2p.data.Destination; +import net.i2p.data.PrivateKey; +import net.i2p.data.PrivateKeyFile; +import net.i2p.data.SigningPrivateKey; + +/** + * Same format as super, simply adds a method to + * treat it as a RouterIdentity instead of a Destination. + * + * @since 0.9.16 + */ +public class RouterPrivateKeyFile extends PrivateKeyFile { + + public RouterPrivateKeyFile(File file) { + super(file); + } + + /** + * Read it in from the file. + * Also sets the local privKey and signingPrivKey. + */ + public RouterIdentity getRouterIdentity() throws IOException, DataFormatException { + InputStream in = null; + try { + in = new BufferedInputStream(new FileInputStream(this.file)); + RouterIdentity ri = new RouterIdentity(); + ri.readBytes(in); + privKey = new PrivateKey(); + privKey.readBytes(in); + SigType type = ri.getSigningPublicKey().getType(); + if (type == null) + throw new DataFormatException("Unknown sig type"); + signingPrivKey = new SigningPrivateKey(type); + signingPrivKey.readBytes(in); + + // set it a Destination, so we may call validateKeyPairs() + // or other methods + dest = new Destination(); + dest.setPublicKey(ri.getPublicKey()); + dest.setSigningPublicKey(ri.getSigningPublicKey()); + dest.setCertificate(ri.getCertificate()); + dest.setPadding(ri.getPadding()); + + return ri; + } finally { + if (in != null) { + try { in.close(); } catch (IOException ioe) {} + } + } + } +} diff --git a/router/java/src/net/i2p/data/router/SortHelper.java b/router/java/src/net/i2p/data/router/SortHelper.java new file mode 100644 index 0000000000..0c40fa3eda --- /dev/null +++ b/router/java/src/net/i2p/data/router/SortHelper.java @@ -0,0 +1,79 @@ +package net.i2p.data.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.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import net.i2p.data.DataStructure; + +/** + * The sorting of addresses in RIs + * + * @since 0.9.16 moved from DataHelper + */ +class SortHelper { + + /** + * Sort based on the Hash of the DataStructure. + * Warning - relatively slow. + * WARNING - this sort order must be consistent network-wide, so while the order is arbitrary, + * it cannot be changed. + * Why? Just because it has to be consistent so signing will work. + * How to spec as returning the same type as the param? + * DEPRECATED - Only used by RouterInfo. + * + * @return a new list + */ + public static List sortStructures(Collection dataStructures) { + if (dataStructures == null) return Collections.emptyList(); + + // This used to use Hash.toString(), which is insane, since a change to toString() + // would break the whole network. Now use Hash.toBase64(). + // Note that the Base64 sort order is NOT the same as the raw byte sort order, + // despite what you may read elsewhere. + + //ArrayList rv = new ArrayList(dataStructures.size()); + //TreeMap tm = new TreeMap(); + //for (DataStructure struct : dataStructures) { + // tm.put(struct.calculateHash().toString(), struct); + //} + //for (DataStructure struct : tm.values()) { + // rv.add(struct); + //} + ArrayList rv = new ArrayList(dataStructures); + sortStructureList(rv); + return rv; + } + + /** + * See above. + * DEPRECATED - Only used by RouterInfo. + * + * @since 0.9 + */ + static void sortStructureList(List dataStructures) { + Collections.sort(dataStructures, new DataStructureComparator()); + } + + /** + * See sortStructures() comments. + * @since 0.8.3 + */ + private static class DataStructureComparator implements Comparator, Serializable { + public int compare(DataStructure l, DataStructure r) { + return l.calculateHash().toBase64().compareTo(r.calculateHash().toBase64()); + } + } +} diff --git a/router/java/src/net/i2p/data/router/package.html b/router/java/src/net/i2p/data/router/package.html new file mode 100644 index 0000000000..fac87d4981 --- /dev/null +++ b/router/java/src/net/i2p/data/router/package.html @@ -0,0 +1,7 @@ + + +

+Classes formerly in net.i2p.data but moved here as they are only used by the router. +

+ + diff --git a/router/java/src/net/i2p/router/Blocklist.java b/router/java/src/net/i2p/router/Blocklist.java index 093a6fdc5a..a6fe1d2093 100644 --- a/router/java/src/net/i2p/router/Blocklist.java +++ b/router/java/src/net/i2p/router/Blocklist.java @@ -28,8 +28,8 @@ import java.util.TreeSet; import net.i2p.data.Base64; import net.i2p.data.DataHelper; import net.i2p.data.Hash; -import net.i2p.data.RouterAddress; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterAddress; +import net.i2p.data.router.RouterInfo; import net.i2p.router.networkdb.kademlia.FloodfillNetworkDatabaseFacade; import net.i2p.util.Addresses; import net.i2p.util.ConcurrentHashSet; diff --git a/router/java/src/net/i2p/router/CommSystemFacade.java b/router/java/src/net/i2p/router/CommSystemFacade.java index 9c7d92338d..59a22eb9ca 100644 --- a/router/java/src/net/i2p/router/CommSystemFacade.java +++ b/router/java/src/net/i2p/router/CommSystemFacade.java @@ -13,7 +13,7 @@ import java.io.Writer; import java.util.Collections; import java.util.List; import net.i2p.data.Hash; -import net.i2p.data.RouterAddress; +import net.i2p.data.router.RouterAddress; /** * Manages the communication subsystem between peers, including connections, diff --git a/router/java/src/net/i2p/router/HandlerJobBuilder.java b/router/java/src/net/i2p/router/HandlerJobBuilder.java index c1c9832cdc..62e2074a5f 100644 --- a/router/java/src/net/i2p/router/HandlerJobBuilder.java +++ b/router/java/src/net/i2p/router/HandlerJobBuilder.java @@ -9,7 +9,7 @@ package net.i2p.router; */ import net.i2p.data.Hash; -import net.i2p.data.RouterIdentity; +import net.i2p.data.router.RouterIdentity; import net.i2p.data.i2np.I2NPMessage; /** diff --git a/router/java/src/net/i2p/router/InNetMessagePool.java b/router/java/src/net/i2p/router/InNetMessagePool.java index 28296cebba..fb4c2a632c 100644 --- a/router/java/src/net/i2p/router/InNetMessagePool.java +++ b/router/java/src/net/i2p/router/InNetMessagePool.java @@ -14,7 +14,7 @@ import java.util.Date; import java.util.List; import net.i2p.data.Hash; -import net.i2p.data.RouterIdentity; +import net.i2p.data.router.RouterIdentity; import net.i2p.data.i2np.DatabaseLookupMessage; import net.i2p.data.i2np.DatabaseSearchReplyMessage; import net.i2p.data.i2np.DeliveryStatusMessage; diff --git a/router/java/src/net/i2p/router/KeyManager.java b/router/java/src/net/i2p/router/KeyManager.java index ce4992f8bd..2807e8fe7f 100644 --- a/router/java/src/net/i2p/router/KeyManager.java +++ b/router/java/src/net/i2p/router/KeyManager.java @@ -18,6 +18,7 @@ import java.io.OutputStream; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import net.i2p.crypto.SigType; import net.i2p.data.DataFormatException; import net.i2p.data.DataStructure; import net.i2p.data.Destination; @@ -26,6 +27,7 @@ import net.i2p.data.PrivateKey; import net.i2p.data.PublicKey; import net.i2p.data.SigningPrivateKey; import net.i2p.data.SigningPublicKey; +import net.i2p.router.startup.CreateRouterInfoJob; import net.i2p.util.Log; import net.i2p.util.SecureDirectory; import net.i2p.util.SecureFileOutputStream; @@ -47,10 +49,10 @@ public class KeyManager { public final static String PROP_KEYDIR = "router.keyBackupDir"; public final static String DEFAULT_KEYDIR = "keyBackup"; - private final static String KEYFILE_PRIVATE_ENC = "privateEncryption.key"; - private final static String KEYFILE_PUBLIC_ENC = "publicEncryption.key"; - private final static String KEYFILE_PRIVATE_SIGNING = "privateSigning.key"; - private final static String KEYFILE_PUBLIC_SIGNING = "publicSigning.key"; + public final static String KEYFILE_PRIVATE_ENC = "privateEncryption.key"; + public final static String KEYFILE_PUBLIC_ENC = "publicEncryption.key"; + public final static String KEYFILE_PRIVATE_SIGNING = "privateSigning.key"; + public final static String KEYFILE_PUBLIC_SIGNING = "publicSigning.key"; public KeyManager(RouterContext context) { _context = context; @@ -151,8 +153,9 @@ public class KeyManager { private void syncKeys(File keyDir) { syncPrivateKey(keyDir); syncPublicKey(keyDir); - syncSigningKey(keyDir); - syncVerificationKey(keyDir); + SigType type = CreateRouterInfoJob.getSigTypeConfig(getContext()); + syncSigningKey(keyDir, type); + syncVerificationKey(keyDir, type); } private void syncPrivateKey(File keyDir) { @@ -181,27 +184,33 @@ public class KeyManager { _publicKey = (PublicKey) readin; } - private void syncSigningKey(File keyDir) { + /** + * @param type the SigType to expect on read-in, ignored on write + */ + private void syncSigningKey(File keyDir, SigType type) { DataStructure ds; File keyFile = new File(keyDir, KEYFILE_PRIVATE_SIGNING); boolean exists = (_signingPrivateKey != null); if (exists) ds = _signingPrivateKey; else - ds = new SigningPrivateKey(); + ds = new SigningPrivateKey(type); DataStructure readin = syncKey(keyFile, ds, exists); if (readin != null && !exists) _signingPrivateKey = (SigningPrivateKey) readin; } - private void syncVerificationKey(File keyDir) { + /** + * @param type the SigType to expect on read-in, ignored on write + */ + private void syncVerificationKey(File keyDir, SigType type) { DataStructure ds; File keyFile = new File(keyDir, KEYFILE_PUBLIC_SIGNING); boolean exists = (_signingPublicKey != null); if (exists) ds = _signingPublicKey; else - ds = new SigningPublicKey(); + ds = new SigningPublicKey(type); DataStructure readin = syncKey(keyFile, ds, exists); if (readin != null && !exists) _signingPublicKey = (SigningPublicKey) readin; diff --git a/router/java/src/net/i2p/router/LeaseSetKeys.java b/router/java/src/net/i2p/router/LeaseSetKeys.java index abfc566df9..849e54f47f 100644 --- a/router/java/src/net/i2p/router/LeaseSetKeys.java +++ b/router/java/src/net/i2p/router/LeaseSetKeys.java @@ -40,7 +40,7 @@ public class LeaseSetKeys { /** * Key with which a LeaseSet can be revoked (by republishing it with no Leases) * - * @deprecated unused + * Deprecated, unused */ public SigningPrivateKey getRevocationKey() { return _revocationKey; } diff --git a/router/java/src/net/i2p/router/MultiRouter.java b/router/java/src/net/i2p/router/MultiRouter.java index 5e8cb47608..abcd32e3d0 100644 --- a/router/java/src/net/i2p/router/MultiRouter.java +++ b/router/java/src/net/i2p/router/MultiRouter.java @@ -10,7 +10,7 @@ import java.util.Scanner; import net.i2p.I2PAppContext; import net.i2p.data.DataHelper; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterInfo; import net.i2p.router.Router; /** diff --git a/router/java/src/net/i2p/router/NetworkDatabaseFacade.java b/router/java/src/net/i2p/router/NetworkDatabaseFacade.java index 7fc3c50826..fdd1fd30fb 100644 --- a/router/java/src/net/i2p/router/NetworkDatabaseFacade.java +++ b/router/java/src/net/i2p/router/NetworkDatabaseFacade.java @@ -14,9 +14,10 @@ import java.util.Collections; import java.util.Set; import net.i2p.data.DatabaseEntry; +import net.i2p.data.Destination; import net.i2p.data.Hash; import net.i2p.data.LeaseSet; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterInfo; import net.i2p.router.networkdb.reseed.ReseedChecker; /** @@ -51,18 +52,51 @@ public abstract class NetworkDatabaseFacade implements Service { public abstract LeaseSet lookupLeaseSetLocally(Hash key); public abstract void lookupRouterInfo(Hash key, Job onFindJob, Job onFailedLookupJob, long timeoutMs); public abstract RouterInfo lookupRouterInfoLocally(Hash key); + + /** + * Lookup using the client's tunnels + * Succeeds even if LS validation fails due to unsupported sig type + * + * @param fromLocalDest use these tunnels for the lookup, or null for exploratory + * @since 0.9.16 + */ + public abstract void lookupDestination(Hash key, Job onFinishedJob, long timeoutMs, Hash fromLocalDest); + + /** + * Lookup locally in netDB and in badDest cache + * Succeeds even if LS validation failed due to unsupported sig type + * + * @since 0.9.16 + */ + public abstract Destination lookupDestinationLocally(Hash key); + /** - * return the leaseSet if another leaseSet already existed at that key + * @return the leaseSet if another leaseSet already existed at that key * * @throws IllegalArgumentException if the data is not valid */ public abstract LeaseSet store(Hash key, LeaseSet leaseSet) throws IllegalArgumentException; + /** - * return the routerInfo if another router already existed at that key + * @return the routerInfo if another router already existed at that key * * @throws IllegalArgumentException if the data is not valid */ public abstract RouterInfo store(Hash key, RouterInfo routerInfo) throws IllegalArgumentException; + + /** + * @return the old entry if it already existed at that key + * @throws IllegalArgumentException if the data is not valid + * @since 0.9.16 + */ + public DatabaseEntry store(Hash key, DatabaseEntry entry) throws IllegalArgumentException { + if (entry.getType() == DatabaseEntry.KEY_TYPE_ROUTERINFO) + return store(key, (RouterInfo) entry); + if (entry.getType() == DatabaseEntry.KEY_TYPE_LEASESET) + return store(key, (LeaseSet) entry); + throw new IllegalArgumentException("unknown type"); + } + /** * @throws IllegalArgumentException if the local router is not valid */ @@ -101,4 +135,12 @@ public abstract class NetworkDatabaseFacade implements Service { * @since IPv6 */ public boolean floodfillEnabled() { return false; }; + + /** + * Is it permanently negative cached? + * + * @param key only for Destinations; for RouterIdentities, see Banlist + * @since 0.9.16 + */ + public boolean isNegativeCachedForever(Hash key) { return false; } } diff --git a/router/java/src/net/i2p/router/OutNetMessage.java b/router/java/src/net/i2p/router/OutNetMessage.java index 633416640c..54434ac4b7 100644 --- a/router/java/src/net/i2p/router/OutNetMessage.java +++ b/router/java/src/net/i2p/router/OutNetMessage.java @@ -18,7 +18,7 @@ import java.util.List; import java.util.Map; import java.util.Set; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterInfo; import net.i2p.data.i2np.I2NPMessage; import net.i2p.router.util.CDPQEntry; import net.i2p.util.Log; diff --git a/router/java/src/net/i2p/router/PersistentKeyRing.java b/router/java/src/net/i2p/router/PersistentKeyRing.java index a3e71ee8e2..920eec7d29 100644 --- a/router/java/src/net/i2p/router/PersistentKeyRing.java +++ b/router/java/src/net/i2p/router/PersistentKeyRing.java @@ -70,9 +70,8 @@ public class PersistentKeyRing extends KeyRing { Hash h = e.getKey(); buf.append(h.toBase64().substring(0, 6)).append("…"); buf.append(""); - LeaseSet ls = _ctx.netDb().lookupLeaseSetLocally(h); - if (ls != null) { - Destination dest = ls.getDestination(); + Destination dest = _ctx.netDb().lookupDestinationLocally(h); + if (dest != null) { if (_ctx.clientManager().isLocal(dest)) { TunnelPoolSettings in = _ctx.tunnelManager().getInboundSettings(h); if (in != null && in.getDestinationNickname() != null) diff --git a/router/java/src/net/i2p/router/Router.java b/router/java/src/net/i2p/router/Router.java index 106efe00dd..3afd99de26 100644 --- a/router/java/src/net/i2p/router/Router.java +++ b/router/java/src/net/i2p/router/Router.java @@ -29,11 +29,12 @@ import net.i2p.data.Certificate; import net.i2p.data.DataFormatException; import net.i2p.data.DataHelper; import net.i2p.data.Destination; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterInfo; import net.i2p.data.SigningPrivateKey; import net.i2p.data.i2np.GarlicMessage; import net.i2p.router.message.GarlicMessageHandler; import net.i2p.router.networkdb.kademlia.FloodfillNetworkDatabaseFacade; +import net.i2p.router.startup.CreateRouterInfoJob; import net.i2p.router.startup.StartupJob; import net.i2p.router.startup.WorkingDir; import net.i2p.router.tasks.*; @@ -98,10 +99,6 @@ public class Router implements RouterClock.ClockShiftListener { /** this does not put an 'H' in your routerInfo **/ public final static String PROP_HIDDEN_HIDDEN = "router.isHidden"; public final static String PROP_DYNAMIC_KEYS = "router.dynamicKeys"; - public final static String PROP_INFO_FILENAME = "router.info.location"; - public final static String PROP_INFO_FILENAME_DEFAULT = "router.info"; - public final static String PROP_KEYS_FILENAME = "router.keys.location"; - public final static String PROP_KEYS_FILENAME_DEFAULT = "router.keys"; public final static String PROP_SHUTDOWN_IN_PROGRESS = "__shutdownInProgress"; public final static String DNS_CACHE_TIME = "" + (5*60); private static final String EVENTLOG = "eventlog.txt"; @@ -672,20 +669,6 @@ public class Router implements RouterClock.ClockShiftListener { return Boolean.parseBoolean(h); return _context.commSystem().isInBadCountry(); } - - /** - * Only called at startup via LoadRouterInfoJob and RebuildRouterInfoJob. - * Not called by periodic RepublishLocalRouterInfoJob. - * We don't want to change the cert on the fly as it changes the router hash. - * RouterInfo.isHidden() checks the capability, but RouterIdentity.isHidden() checks the cert. - * There's no reason to ever add a hidden cert? - * @return the certificate for a new RouterInfo - probably a null cert. - */ - public Certificate createCertificate() { - if (_context.getBooleanProperty(PROP_HIDDEN)) - return new Certificate(Certificate.CERTIFICATE_TYPE_HIDDEN, null); - return Certificate.NULL_CERT; - } /** * @since 0.9.3 @@ -698,16 +681,18 @@ public class Router implements RouterClock.ClockShiftListener { * Ugly list of files that we need to kill if we are building a new identity * */ - private static final String _rebuildFiles[] = new String[] { "router.info", - "router.keys", - "netDb/my.info", // no longer used - "connectionTag.keys", // never used? - "keyBackup/privateEncryption.key", - "keyBackup/privateSigning.key", - "keyBackup/publicEncryption.key", - "keyBackup/publicSigning.key", - "sessionKeys.dat" // no longer used - }; + private static final String _rebuildFiles[] = new String[] { + CreateRouterInfoJob.INFO_FILENAME, + CreateRouterInfoJob.KEYS_FILENAME, + CreateRouterInfoJob.KEYS2_FILENAME, + "netDb/my.info", // no longer used + "connectionTag.keys", // never used? + KeyManager.DEFAULT_KEYDIR + '/' + KeyManager.KEYFILE_PRIVATE_ENC, + KeyManager.DEFAULT_KEYDIR + '/' + KeyManager.KEYFILE_PUBLIC_ENC, + KeyManager.DEFAULT_KEYDIR + '/' + KeyManager.KEYFILE_PRIVATE_SIGNING, + KeyManager.DEFAULT_KEYDIR + '/' + KeyManager.KEYFILE_PUBLIC_SIGNING, + "sessionKeys.dat" // no longer used + }; public void killKeys() { //new Exception("Clearing identity files").printStackTrace(); diff --git a/router/java/src/net/i2p/router/RouterContext.java b/router/java/src/net/i2p/router/RouterContext.java index 1f8a38af68..20b2fe9b7d 100644 --- a/router/java/src/net/i2p/router/RouterContext.java +++ b/router/java/src/net/i2p/router/RouterContext.java @@ -10,7 +10,7 @@ import java.util.concurrent.CopyOnWriteArrayList; import net.i2p.I2PAppContext; import net.i2p.app.ClientAppManager; import net.i2p.data.Hash; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterInfo; import net.i2p.internal.InternalClientManager; import net.i2p.router.client.ClientManagerFacadeImpl; import net.i2p.router.crypto.TransientSessionKeyManager; diff --git a/router/java/src/net/i2p/router/RouterThrottleImpl.java b/router/java/src/net/i2p/router/RouterThrottleImpl.java index 805e7bbbc3..275e4d85a4 100644 --- a/router/java/src/net/i2p/router/RouterThrottleImpl.java +++ b/router/java/src/net/i2p/router/RouterThrottleImpl.java @@ -1,7 +1,7 @@ package net.i2p.router; import net.i2p.data.Hash; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterInfo; import net.i2p.router.peermanager.TunnelHistory; import net.i2p.stat.Rate; import net.i2p.stat.RateAverages; diff --git a/router/java/src/net/i2p/router/client/ClientMessageEventListener.java b/router/java/src/net/i2p/router/client/ClientMessageEventListener.java index cdcc0008cf..382552ce68 100644 --- a/router/java/src/net/i2p/router/client/ClientMessageEventListener.java +++ b/router/java/src/net/i2p/router/client/ClientMessageEventListener.java @@ -12,8 +12,10 @@ import java.util.Properties; import net.i2p.CoreVersion; import net.i2p.crypto.SigType; +import net.i2p.data.Destination; import net.i2p.data.Hash; import net.i2p.data.Payload; +import net.i2p.data.PublicKey; import net.i2p.data.i2cp.BandwidthLimitsMessage; import net.i2p.data.i2cp.CreateLeaseSetMessage; import net.i2p.data.i2cp.CreateSessionMessage; @@ -37,6 +39,7 @@ import net.i2p.data.i2cp.SessionId; import net.i2p.data.i2cp.SessionStatusMessage; import net.i2p.data.i2cp.SetDateMessage; import net.i2p.router.ClientTunnelSettings; +import net.i2p.router.LeaseSetKeys; import net.i2p.router.RouterContext; import net.i2p.util.Log; import net.i2p.util.PasswordManager; @@ -81,8 +84,8 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi _log.debug("Message received: \n" + message); int type = message.getType(); if (!_authorized) { - // TODO change to default true - boolean strict = _context.getBooleanProperty(PROP_AUTH_STRICT); + // Default true as of 0.9.16 + boolean strict = _context.getBooleanPropertyDefaultTrue(PROP_AUTH_STRICT); if ((strict && type != GetDateMessage.MESSAGE_TYPE) || (type != CreateSessionMessage.MESSAGE_TYPE && type != GetDateMessage.MESSAGE_TYPE && @@ -367,8 +370,41 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi _runner.disconnectClient("Invalid CreateLeaseSetMessage"); return; } - - _context.keyManager().registerKeys(message.getLeaseSet().getDestination(), message.getSigningPrivateKey(), message.getPrivateKey()); + Destination dest = _runner.getConfig().getDestination(); + Destination ndest = message.getLeaseSet().getDestination(); + if (!dest.equals(ndest)) { + if (_log.shouldLog(Log.ERROR)) + _log.error("Different destination in LS"); + _runner.disconnectClient("Different destination in LS"); + return; + } + LeaseSetKeys keys = _context.keyManager().getKeys(dest); + if (keys == null || + !message.getPrivateKey().equals(keys.getDecryptionKey())) { + // Verify and register crypto keys if new or if changed + // Private crypto key should never change, and if it does, + // one of the checks below will fail + PublicKey pk; + try { + pk = message.getPrivateKey().toPublic(); + } catch (IllegalArgumentException iae) { + if (_log.shouldLog(Log.ERROR)) + _log.error("Bad private key in LS"); + _runner.disconnectClient("Bad private key in LS"); + return; + } + if (!pk.equals(message.getLeaseSet().getEncryptionKey())) { + if (_log.shouldLog(Log.ERROR)) + _log.error("Private/public crypto key mismatch in LS"); + _runner.disconnectClient("Private/public crypto key mismatch in LS"); + return; + } + // just register new SPK, don't verify, unused + _context.keyManager().registerKeys(dest, message.getSigningPrivateKey(), message.getPrivateKey()); + } else if (!message.getSigningPrivateKey().equals(keys.getRevocationKey())) { + // just register new SPK, don't verify, unused + _context.keyManager().registerKeys(dest, message.getSigningPrivateKey(), message.getPrivateKey()); + } try { _context.netDb().publish(message.getLeaseSet()); } catch (IllegalArgumentException iae) { diff --git a/router/java/src/net/i2p/router/client/LookupDestJob.java b/router/java/src/net/i2p/router/client/LookupDestJob.java index be08388ba2..911365b8e8 100644 --- a/router/java/src/net/i2p/router/client/LookupDestJob.java +++ b/router/java/src/net/i2p/router/client/LookupDestJob.java @@ -38,7 +38,11 @@ class LookupDestJob extends JobImpl { } /** - * One of h or name non-null + * One of h or name non-null. + * + * For hash or b32 name, the dest will be returned if the LS can be found, + * even if the dest uses unsupported crypto. + * * @param reqID must be >= 0 if name != null * @param sessID must non-null if reqID >= 0 * @param fromLocalDest use these tunnels for the lookup, or null for exploratory @@ -88,7 +92,7 @@ class LookupDestJob extends JobImpl { returnFail(); } else { DoneJob done = new DoneJob(getContext()); - getContext().netDb().lookupLeaseSet(_hash, done, done, _timeout, _fromLocalDest); + getContext().netDb().lookupDestination(_hash, done, _timeout, _fromLocalDest); } } @@ -98,9 +102,9 @@ class LookupDestJob extends JobImpl { } public String getName() { return "LeaseSet Lookup Reply to Client"; } public void runJob() { - LeaseSet ls = getContext().netDb().lookupLeaseSetLocally(_hash); - if (ls != null) - returnDest(ls.getDestination()); + Destination dest = getContext().netDb().lookupDestinationLocally(_hash); + if (dest != null) + returnDest(dest); else returnFail(); } diff --git a/router/java/src/net/i2p/router/dummy/DummyNetworkDatabaseFacade.java b/router/java/src/net/i2p/router/dummy/DummyNetworkDatabaseFacade.java index 11b9419f38..460d7a7125 100644 --- a/router/java/src/net/i2p/router/dummy/DummyNetworkDatabaseFacade.java +++ b/router/java/src/net/i2p/router/dummy/DummyNetworkDatabaseFacade.java @@ -15,16 +15,17 @@ import java.util.Map; import java.util.Set; import net.i2p.data.DatabaseEntry; +import net.i2p.data.Destination; import net.i2p.data.Hash; import net.i2p.data.LeaseSet; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterInfo; import net.i2p.router.Job; import net.i2p.router.NetworkDatabaseFacade; import net.i2p.router.RouterContext; public class DummyNetworkDatabaseFacade extends NetworkDatabaseFacade { - private Map _routers; - private RouterContext _context; + private final Map _routers; + private final RouterContext _context; public DummyNetworkDatabaseFacade(RouterContext ctx) { _routers = Collections.synchronizedMap(new HashMap()); @@ -42,6 +43,11 @@ public class DummyNetworkDatabaseFacade extends NetworkDatabaseFacade { public void lookupLeaseSet(Hash key, Job onFindJob, Job onFailedLookupJob, long timeoutMs) {} public void lookupLeaseSet(Hash key, Job onFindJob, Job onFailedLookupJob, long timeoutMs, Hash fromLocalDest) {} public LeaseSet lookupLeaseSetLocally(Hash key) { return null; } + + public void lookupDestination(Hash key, Job onFinishedJob, long timeoutMs, Hash fromLocalDest) {} + + public Destination lookupDestinationLocally(Hash key) { return null; } + public void lookupRouterInfo(Hash key, Job onFindJob, Job onFailedLookupJob, long timeoutMs) { RouterInfo info = lookupRouterInfoLocally(key); if (info == null) @@ -50,13 +56,16 @@ public class DummyNetworkDatabaseFacade extends NetworkDatabaseFacade { _context.jobQueue().addJob(onFindJob); } public RouterInfo lookupRouterInfoLocally(Hash key) { return _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 = _routers.put(key, routerInfo); return rv; } + public void unpublish(LeaseSet localLeaseSet) {} public void fail(Hash dbEntry) { _routers.remove(dbEntry); diff --git a/router/java/src/net/i2p/router/message/GarlicConfig.java b/router/java/src/net/i2p/router/message/GarlicConfig.java index 9fe299e0c3..4970a589f9 100644 --- a/router/java/src/net/i2p/router/message/GarlicConfig.java +++ b/router/java/src/net/i2p/router/message/GarlicConfig.java @@ -14,7 +14,7 @@ import java.util.List; import net.i2p.data.Certificate; import net.i2p.data.PublicKey; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterInfo; import net.i2p.data.i2np.DeliveryInstructions; /** diff --git a/router/java/src/net/i2p/router/message/GarlicMessageHandler.java b/router/java/src/net/i2p/router/message/GarlicMessageHandler.java index c44ec2f055..3a762b6fd7 100644 --- a/router/java/src/net/i2p/router/message/GarlicMessageHandler.java +++ b/router/java/src/net/i2p/router/message/GarlicMessageHandler.java @@ -9,7 +9,7 @@ package net.i2p.router.message; */ import net.i2p.data.Hash; -import net.i2p.data.RouterIdentity; +import net.i2p.data.router.RouterIdentity; import net.i2p.data.i2np.GarlicMessage; import net.i2p.data.i2np.I2NPMessage; import net.i2p.router.HandlerJobBuilder; diff --git a/router/java/src/net/i2p/router/message/HandleGarlicMessageJob.java b/router/java/src/net/i2p/router/message/HandleGarlicMessageJob.java index 9392526b3f..9a22ec6e84 100644 --- a/router/java/src/net/i2p/router/message/HandleGarlicMessageJob.java +++ b/router/java/src/net/i2p/router/message/HandleGarlicMessageJob.java @@ -9,7 +9,7 @@ package net.i2p.router.message; */ import net.i2p.data.Hash; -import net.i2p.data.RouterIdentity; +import net.i2p.data.router.RouterIdentity; import net.i2p.data.i2np.DeliveryInstructions; import net.i2p.data.i2np.GarlicMessage; import net.i2p.data.i2np.I2NPMessage; diff --git a/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java b/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java index 5040606e8d..d9f7a0a516 100644 --- a/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java +++ b/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java @@ -18,7 +18,7 @@ import net.i2p.data.Lease; import net.i2p.data.LeaseSet; import net.i2p.data.Payload; import net.i2p.data.PublicKey; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterInfo; import net.i2p.data.SessionKey; import net.i2p.data.SessionTag; import net.i2p.data.i2cp.MessageId; @@ -425,12 +425,19 @@ public class OutboundClientMessageOneShotJob extends JobImpl { getContext().statManager().addRateData("client.leaseSetFailedRemoteTime", lookupTime); } - //if (_finished == Result.NONE) { + + int cause; + if (getContext().netDb().isNegativeCachedForever(_to.calculateHash())) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Unable to send to " + _toString + " because the sig type is unsupported"); + cause = MessageStatusMessage.STATUS_SEND_FAILURE_UNSUPPORTED_ENCRYPTION; + } else { if (_log.shouldLog(Log.WARN)) _log.warn("Unable to send to " + _toString + " because we couldn't find their leaseSet"); - //} + cause = MessageStatusMessage.STATUS_SEND_FAILURE_NO_LEASESET; + } - dieFatal(MessageStatusMessage.STATUS_SEND_FAILURE_NO_LEASESET); + dieFatal(cause); } } diff --git a/router/java/src/net/i2p/router/message/SendMessageDirectJob.java b/router/java/src/net/i2p/router/message/SendMessageDirectJob.java index 6d0cfdbcf2..cc7a337cc5 100644 --- a/router/java/src/net/i2p/router/message/SendMessageDirectJob.java +++ b/router/java/src/net/i2p/router/message/SendMessageDirectJob.java @@ -11,7 +11,7 @@ package net.i2p.router.message; import java.util.Date; import net.i2p.data.Hash; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterInfo; import net.i2p.data.i2np.I2NPMessage; import net.i2p.router.Job; import net.i2p.router.JobImpl; diff --git a/router/java/src/net/i2p/router/networkdb/HandleDatabaseLookupMessageJob.java b/router/java/src/net/i2p/router/networkdb/HandleDatabaseLookupMessageJob.java index 685e18512c..9931444aaf 100644 --- a/router/java/src/net/i2p/router/networkdb/HandleDatabaseLookupMessageJob.java +++ b/router/java/src/net/i2p/router/networkdb/HandleDatabaseLookupMessageJob.java @@ -14,8 +14,8 @@ import java.util.Set; import net.i2p.data.DatabaseEntry; import net.i2p.data.Hash; import net.i2p.data.LeaseSet; -import net.i2p.data.RouterIdentity; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterIdentity; +import net.i2p.data.router.RouterInfo; import net.i2p.data.SessionKey; import net.i2p.data.TunnelId; import net.i2p.data.i2np.DatabaseLookupMessage; diff --git a/router/java/src/net/i2p/router/networkdb/PublishLocalRouterInfoJob.java b/router/java/src/net/i2p/router/networkdb/PublishLocalRouterInfoJob.java index f6a29355b7..8b235260e3 100644 --- a/router/java/src/net/i2p/router/networkdb/PublishLocalRouterInfoJob.java +++ b/router/java/src/net/i2p/router/networkdb/PublishLocalRouterInfoJob.java @@ -12,7 +12,7 @@ import java.util.Date; import java.util.Properties; import net.i2p.data.DataFormatException; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterInfo; import net.i2p.data.SigningPrivateKey; import net.i2p.router.JobImpl; import net.i2p.router.Router; diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/BundleRouterInfos.java b/router/java/src/net/i2p/router/networkdb/kademlia/BundleRouterInfos.java index ebcde49c69..f916e976f1 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/BundleRouterInfos.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/BundleRouterInfos.java @@ -25,8 +25,8 @@ import gnu.getopt.Getopt; import net.i2p.I2PAppContext; import net.i2p.data.Hash; -import net.i2p.data.RouterAddress; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterAddress; +import net.i2p.data.router.RouterInfo; import net.i2p.router.transport.BadCountries; import net.i2p.router.transport.GeoIP; import net.i2p.util.FileUtil; diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/ExpireRoutersJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/ExpireRoutersJob.java index c2ea790a5a..b52df8e179 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/ExpireRoutersJob.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/ExpireRoutersJob.java @@ -12,7 +12,7 @@ import java.util.Set; import net.i2p.data.DatabaseEntry; import net.i2p.data.Hash; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterInfo; import net.i2p.router.CommSystemFacade; import net.i2p.router.JobImpl; import net.i2p.router.RouterContext; diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/ExploreJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/ExploreJob.java index 88122c0e27..aff802b772 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/ExploreJob.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/ExploreJob.java @@ -15,6 +15,8 @@ import java.util.Set; import net.i2p.data.Hash; import net.i2p.data.TunnelId; import net.i2p.data.i2np.DatabaseLookupMessage; +import net.i2p.data.i2np.I2NPMessage; +import net.i2p.data.router.RouterInfo; import net.i2p.kademlia.KBucketSet; import net.i2p.router.RouterContext; import net.i2p.util.Log; @@ -71,15 +73,15 @@ class ExploreJob extends SearchJob { * and PeerSelector doesn't include the floodfill peers, * so we add the ff peers ourselves and then use the regular PeerSelector. * - * TODO should we encrypt this also like we do for normal lookups? - * Could the OBEP capture it and reply with a reference to a hostile peer? - * * @param replyTunnelId tunnel to receive replies through * @param replyGateway gateway for the reply tunnel * @param expiration when the search should stop + * @param peer the peer to send it to + * + * @return a DatabaseLookupMessage or GarlicMessage */ @Override - protected DatabaseLookupMessage buildMessage(TunnelId replyTunnelId, Hash replyGateway, long expiration) { + protected I2NPMessage buildMessage(TunnelId replyTunnelId, Hash replyGateway, long expiration, RouterInfo peer) { DatabaseLookupMessage msg = new DatabaseLookupMessage(getContext(), true); msg.setSearchKey(getState().getTarget()); msg.setFrom(replyGateway); @@ -127,7 +129,27 @@ class ExploreJob extends SearchJob { _log.debug("Peers we don't want to hear about: " + dontIncludePeers); msg.setDontIncludePeers(dontIncludePeers); - return msg; + + // Now encrypt if we can + I2NPMessage outMsg; + if (getContext().getProperty(IterativeSearchJob.PROP_ENCRYPT_RI, IterativeSearchJob.DEFAULT_ENCRYPT_RI)) { + // request encrypted reply? + if (DatabaseLookupMessage.supportsEncryptedReplies(peer)) { + MessageWrapper.OneTimeSession sess; + sess = MessageWrapper.generateSession(getContext()); + if (_log.shouldLog(Log.INFO)) + _log.info(getJobId() + ": Requesting encrypted reply from " + peer.getIdentity().calculateHash() + + ' ' + sess.key + ' ' + sess.tag); + msg.setReplySession(sess.key, sess.tag); + } + outMsg = MessageWrapper.wrap(getContext(), msg, peer); + if (_log.shouldLog(Log.DEBUG)) + _log.debug(getJobId() + ": Encrypted exploratory DLM for " + getState().getTarget() + " to " + + peer.getIdentity().calculateHash()); + } else { + outMsg = msg; + } + return outMsg; } /** max # of concurrent searches */ diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/FloodOnlyLookupMatchJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/FloodOnlyLookupMatchJob.java index 8f3a193a5a..46f985a917 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/FloodOnlyLookupMatchJob.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/FloodOnlyLookupMatchJob.java @@ -2,10 +2,10 @@ package net.i2p.router.networkdb.kademlia; import net.i2p.data.DatabaseEntry; import net.i2p.data.LeaseSet; -import net.i2p.data.RouterInfo; import net.i2p.data.i2np.DatabaseSearchReplyMessage; import net.i2p.data.i2np.DatabaseStoreMessage; import net.i2p.data.i2np.I2NPMessage; +import net.i2p.data.router.RouterInfo; import net.i2p.router.JobImpl; import net.i2p.router.ReplyJob; import net.i2p.router.RouterContext; @@ -62,6 +62,9 @@ class FloodOnlyLookupMatchJob extends JobImpl implements ReplyJob { } else { getContext().netDb().store(dsm.getKey(), (RouterInfo) dsm.getEntry()); } + } catch (UnsupportedCryptoException uce) { + _search.failed(); + return; } catch (IllegalArgumentException iae) { if (_log.shouldLog(Log.WARN)) _log.warn(_search.getJobId() + ": Received an invalid store reply", iae); diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillDatabaseLookupMessageHandler.java b/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillDatabaseLookupMessageHandler.java index 8ef121d060..a07ac8c708 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillDatabaseLookupMessageHandler.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillDatabaseLookupMessageHandler.java @@ -9,7 +9,7 @@ package net.i2p.router.networkdb.kademlia; */ import net.i2p.data.Hash; -import net.i2p.data.RouterIdentity; +import net.i2p.data.router.RouterIdentity; import net.i2p.data.i2np.DatabaseLookupMessage; import net.i2p.data.i2np.I2NPMessage; import net.i2p.router.HandlerJobBuilder; diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillDatabaseStoreMessageHandler.java b/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillDatabaseStoreMessageHandler.java index e4c6ce71af..99d8c62658 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillDatabaseStoreMessageHandler.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillDatabaseStoreMessageHandler.java @@ -9,7 +9,7 @@ package net.i2p.router.networkdb.kademlia; */ import net.i2p.data.Hash; -import net.i2p.data.RouterIdentity; +import net.i2p.data.router.RouterIdentity; import net.i2p.data.i2np.DatabaseStoreMessage; import net.i2p.data.i2np.I2NPMessage; import net.i2p.router.HandlerJobBuilder; diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillMonitorJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillMonitorJob.java index 99c4beda29..de634943cb 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillMonitorJob.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillMonitorJob.java @@ -3,8 +3,8 @@ package net.i2p.router.networkdb.kademlia; import java.util.List; import net.i2p.data.Hash; -import net.i2p.data.RouterAddress; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterAddress; +import net.i2p.data.router.RouterInfo; import net.i2p.router.JobImpl; import net.i2p.router.Router; import net.i2p.router.RouterContext; diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillNetworkDatabaseFacade.java b/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillNetworkDatabaseFacade.java index 24a7bc40a1..e8a80368c4 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillNetworkDatabaseFacade.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillNetworkDatabaseFacade.java @@ -7,11 +7,12 @@ import java.util.Map; import java.util.Set; import net.i2p.data.DatabaseEntry; +import net.i2p.data.Destination; import net.i2p.data.Hash; -import net.i2p.data.RouterInfo; import net.i2p.data.TunnelId; import net.i2p.data.i2np.DatabaseLookupMessage; import net.i2p.data.i2np.DatabaseStoreMessage; +import net.i2p.data.router.RouterInfo; import net.i2p.router.Job; import net.i2p.router.JobImpl; import net.i2p.router.OutNetMessage; @@ -31,7 +32,6 @@ public class FloodfillNetworkDatabaseFacade extends KademliaNetworkDatabaseFacad private final Set _verifiesInProgress; private FloodThrottler _floodThrottler; private LookupThrottler _lookupThrottler; - private NegativeLookupCache _negativeCache; /** * This is the flood redundancy. Entries are @@ -65,7 +65,6 @@ public class FloodfillNetworkDatabaseFacade extends KademliaNetworkDatabaseFacad _context.statManager().createRateStat("netDb.searchReplyNotValidated", "How many search replies we get that we are NOT able to validate (fetch)", "NetworkDatabase", new long[] { 5*60*1000l, 10*60*1000l, 60*60*1000l, 3*60*60*1000l, 24*60*60*1000l }); _context.statManager().createRateStat("netDb.searchReplyValidationSkipped", "How many search replies we get from unreliable peers that we skip?", "NetworkDatabase", new long[] { 5*60*1000l, 10*60*1000l, 60*60*1000l, 3*60*60*1000l, 24*60*60*1000l }); _context.statManager().createRateStat("netDb.republishQuantity", "How many peers do we need to send a found leaseSet to?", "NetworkDatabase", new long[] { 10*60*1000l, 60*60*1000l, 3*60*60*1000l, 24*60*60*1000l }); - _context.statManager().createRateStat("netDb.negativeCache", "Aborted lookup, already cached", "NetworkDatabase", new long[] { 60*60*1000l }); } @Override @@ -73,7 +72,6 @@ public class FloodfillNetworkDatabaseFacade extends KademliaNetworkDatabaseFacad super.startup(); _context.jobQueue().addJob(new FloodfillMonitorJob(_context, this)); _lookupThrottler = new LookupThrottler(); - _negativeCache = new NegativeLookupCache(); // refresh old routers Job rrj = new RefreshRoutersJob(_context, this); @@ -171,25 +169,6 @@ public class FloodfillNetworkDatabaseFacade extends KademliaNetworkDatabaseFacad return _lookupThrottler.shouldThrottle(from, id); } - /** - * Increment in the negative lookup cache - * @since 0.9.4 - */ - void lookupFailed(Hash key) { - _negativeCache.lookupFailed(key); - } - - /** - * Is the key in the negative lookup cache? - * @since 0.9.4 - */ - boolean isNegativeCached(Hash key) { - boolean rv = _negativeCache.isCached(key); - if (rv) - _context.statManager().addRateData("netDb.negativeCache", 1); - return rv; - } - /** * Send to a subset of all floodfill peers. * We do this to implement Kademlia within the floodfills, i.e. @@ -301,7 +280,9 @@ public class FloodfillNetworkDatabaseFacade extends KademliaNetworkDatabaseFacad } /** - * Lookup using exploratory tunnels + * Lookup using exploratory tunnels. + * + * Caller should check negative cache and/or banlist before calling. * * Begin a kademlia style search for the key specified, which can take up to timeoutMs and * will fire the appropriate jobs on success or timeout (or if the kademlia search completes @@ -315,7 +296,10 @@ public class FloodfillNetworkDatabaseFacade extends KademliaNetworkDatabaseFacad } /** - * Lookup using the client's tunnels + * Lookup using the client's tunnels. + * + * Caller should check negative cache and/or banlist before calling. + * * @param fromLocalDest use these tunnels for the lookup, or null for exploratory * @return null always * @since 0.9.10 @@ -473,6 +457,7 @@ public class FloodfillNetworkDatabaseFacade extends KademliaNetworkDatabaseFacad // should we skip the search? if (_floodfillEnabled || _context.jobQueue().getMaxLag() > 500 || + _context.banlist().isBanlistedForever(peer) || getKBucketSetSize() > MAX_DB_BEFORE_SKIPPING_SEARCH) { // don't try to overload ourselves (e.g. failing 3000 router refs at // once, and then firing off 3000 netDb lookup tasks) diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillPeerSelector.java b/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillPeerSelector.java index fdf47bf3cc..6870edcd58 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillPeerSelector.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillPeerSelector.java @@ -18,8 +18,8 @@ import java.util.Set; import java.util.TreeSet; import net.i2p.data.Hash; -import net.i2p.data.RouterAddress; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterAddress; +import net.i2p.data.router.RouterInfo; import net.i2p.kademlia.KBucketSet; import net.i2p.kademlia.SelectionCollector; import net.i2p.kademlia.XORComparator; diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillVerifyStoreJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillVerifyStoreJob.java index f66b50020a..085c0c921a 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillVerifyStoreJob.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillVerifyStoreJob.java @@ -6,9 +6,10 @@ import java.util.Set; import net.i2p.data.Certificate; import net.i2p.data.DatabaseEntry; +import net.i2p.data.Destination; import net.i2p.data.Hash; import net.i2p.data.LeaseSet; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterInfo; import net.i2p.data.i2np.DatabaseLookupMessage; import net.i2p.data.i2np.DatabaseSearchReplyMessage; import net.i2p.data.i2np.DatabaseStoreMessage; @@ -173,9 +174,9 @@ class FloodfillVerifyStoreJob extends JobImpl { FloodfillPeerSelector sel = (FloodfillPeerSelector)_facade.getPeerSelector(); Certificate keyCert = null; if (!_isRouterInfo) { - LeaseSet ls = _facade.lookupLeaseSetLocally(_key); - if (ls != null) { - Certificate cert = ls.getDestination().getCertificate(); + Destination dest = _facade.lookupDestinationLocally(_key); + if (dest != null) { + Certificate cert = dest.getCertificate(); if (cert.getCertificateType() == Certificate.CERTIFICATE_TYPE_KEY) keyCert = cert; } diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/HandleFloodfillDatabaseLookupMessageJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/HandleFloodfillDatabaseLookupMessageJob.java index 0d03d7d4fe..eceb4b576e 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/HandleFloodfillDatabaseLookupMessageJob.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/HandleFloodfillDatabaseLookupMessageJob.java @@ -11,8 +11,8 @@ package net.i2p.router.networkdb.kademlia; import java.util.Set; import net.i2p.data.Hash; -import net.i2p.data.RouterIdentity; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterIdentity; +import net.i2p.data.router.RouterInfo; import net.i2p.data.TunnelId; import net.i2p.data.i2np.DatabaseStoreMessage; import net.i2p.data.i2np.DatabaseLookupMessage; diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/HandleFloodfillDatabaseStoreMessageJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/HandleFloodfillDatabaseStoreMessageJob.java index f623022ca7..ffb6d77f00 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/HandleFloodfillDatabaseStoreMessageJob.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/HandleFloodfillDatabaseStoreMessageJob.java @@ -14,9 +14,9 @@ import java.util.Date; import net.i2p.data.DatabaseEntry; import net.i2p.data.Hash; import net.i2p.data.LeaseSet; -import net.i2p.data.RouterAddress; -import net.i2p.data.RouterIdentity; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterAddress; +import net.i2p.data.router.RouterIdentity; +import net.i2p.data.router.RouterInfo; import net.i2p.data.i2np.DatabaseStoreMessage; import net.i2p.data.i2np.DeliveryStatusMessage; import net.i2p.router.JobImpl; @@ -51,6 +51,8 @@ public class HandleFloodfillDatabaseStoreMessageJob extends JobImpl { long recvBegin = System.currentTimeMillis(); String invalidMessage = null; + // set if invalid store but not his fault + boolean dontBlamePeer = false; boolean wasNew = false; RouterInfo prevNetDb = null; Hash key = _message.getKey(); @@ -72,6 +74,7 @@ public class HandleFloodfillDatabaseStoreMessageJob extends JobImpl { if (getContext().clientManager().isLocal(key)) { //getContext().statManager().addRateData("netDb.storeLocalLeaseSetAttempt", 1, 0); // throw rather than return, so that we send the ack below (prevent easy attack) + dontBlamePeer = true; throw new IllegalArgumentException("Peer attempted to store local leaseSet: " + key.toBase64().substring(0, 4)); } @@ -114,6 +117,9 @@ public class HandleFloodfillDatabaseStoreMessageJob extends JobImpl { //if (!ls.getReceivedAsReply()) // match.setReceivedAsPublished(true); } + } catch (UnsupportedCryptoException uce) { + invalidMessage = uce.getMessage(); + dontBlamePeer = true; } catch (IllegalArgumentException iae) { invalidMessage = iae.getMessage(); } @@ -131,8 +137,10 @@ public class HandleFloodfillDatabaseStoreMessageJob extends JobImpl { if (getContext().routerHash().equals(key)) { //getContext().statManager().addRateData("netDb.storeLocalRouterInfoAttempt", 1, 0); // throw rather than return, so that we send the ack below (prevent easy attack) + dontBlamePeer = true; throw new IllegalArgumentException("Peer attempted to store our RouterInfo"); } + getContext().profileManager().heardAbout(key); prevNetDb = getContext().netDb().store(key, ri); wasNew = ((null == prevNetDb) || (prevNetDb.getPublished() < ri.getPublished())); // Check new routerinfo address against blocklist @@ -152,7 +160,9 @@ public class HandleFloodfillDatabaseStoreMessageJob extends JobImpl { _log.warn("New address received, Blocklisting old peer " + key + ' ' + ri); } } - getContext().profileManager().heardAbout(key); + } catch (UnsupportedCryptoException uce) { + invalidMessage = uce.getMessage(); + dontBlamePeer = true; } catch (IllegalArgumentException iae) { invalidMessage = iae.getMessage(); } @@ -165,14 +175,16 @@ public class HandleFloodfillDatabaseStoreMessageJob extends JobImpl { long recvEnd = System.currentTimeMillis(); getContext().statManager().addRateData("netDb.storeRecvTime", recvEnd-recvBegin); - if (_message.getReplyToken() > 0) + // ack even if invalid or unsupported + // TODO any cases where we shouldn't? + if (_message.getReplyToken() > 0) sendAck(); long ackEnd = System.currentTimeMillis(); if (_from != null) _fromHash = _from.getHash(); if (_fromHash != null) { - if (invalidMessage == null) { + if (invalidMessage == null || dontBlamePeer) { getContext().profileManager().dbStoreReceived(_fromHash, wasNew); getContext().statManager().addRateData("netDb.storeHandled", ackEnd-recvEnd); } else { @@ -180,7 +192,7 @@ public class HandleFloodfillDatabaseStoreMessageJob extends JobImpl { if (_log.shouldLog(Log.WARN)) _log.warn("Peer " + _fromHash.toBase64() + " sent bad data: " + invalidMessage); } - } else if (invalidMessage != null) { + } else if (invalidMessage != null && !dontBlamePeer) { if (_log.shouldLog(Log.WARN)) _log.warn("Unknown peer sent bad data: " + invalidMessage); } diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/HarvesterJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/HarvesterJob.java index a69a2bc906..6bd687185c 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/HarvesterJob.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/HarvesterJob.java @@ -7,7 +7,7 @@ import java.util.Set; import java.util.TreeMap; import net.i2p.data.Hash; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterInfo; import net.i2p.data.i2np.DatabaseLookupMessage; import net.i2p.router.JobImpl; import net.i2p.router.OutNetMessage; diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/IterativeLookupJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/IterativeLookupJob.java index 13d7636887..ac283109ca 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/IterativeLookupJob.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/IterativeLookupJob.java @@ -1,7 +1,7 @@ package net.i2p.router.networkdb.kademlia; import net.i2p.data.Hash; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterInfo; import net.i2p.data.i2np.DatabaseSearchReplyMessage; import net.i2p.util.Log; import net.i2p.router.JobImpl; diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/IterativeSearchJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/IterativeSearchJob.java index 7a2fa77709..490a45a218 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/IterativeSearchJob.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/IterativeSearchJob.java @@ -13,9 +13,9 @@ import java.util.concurrent.ConcurrentHashMap; import net.i2p.data.Base64; import net.i2p.data.DataHelper; import net.i2p.data.Hash; -import net.i2p.data.RouterInfo; import net.i2p.data.i2np.DatabaseLookupMessage; import net.i2p.data.i2np.I2NPMessage; +import net.i2p.data.router.RouterInfo; import net.i2p.kademlia.KBucketSet; import net.i2p.kademlia.XORComparator; import net.i2p.router.CommSystemFacade; @@ -28,6 +28,8 @@ import net.i2p.router.TunnelInfo; import net.i2p.router.TunnelManagerFacade; import net.i2p.router.util.RandomIterator; import net.i2p.util.Log; +import net.i2p.util.NativeBigInteger; +import net.i2p.util.SystemVersion; /** * A traditional Kademlia search that continues to search @@ -88,8 +90,13 @@ class IterativeSearchJob extends FloodSearchJob { */ private static final int MAX_CONCURRENT = 1; - /** testing */ - private static final String PROP_ENCRYPT_RI = "router.encryptRouterLookups"; + public static final String PROP_ENCRYPT_RI = "router.encryptRouterLookups"; + + /** only on fast boxes, for now */ + public static final boolean DEFAULT_ENCRYPT_RI = + SystemVersion.isX86() && SystemVersion.is64Bit() && + !SystemVersion.isApache() && !SystemVersion.isGNU() && + NativeBigInteger.isNative(); /** * Lookup using exploratory tunnels @@ -315,7 +322,7 @@ class IterativeSearchJob extends FloodSearchJob { _sentTime.put(peer, Long.valueOf(now)); I2NPMessage outMsg = null; - if (_isLease || getContext().getBooleanProperty(PROP_ENCRYPT_RI)) { + if (_isLease || getContext().getProperty(PROP_ENCRYPT_RI, DEFAULT_ENCRYPT_RI)) { // Full ElG is fairly expensive so only do it for LS lookups // if we have the ff RI, garlic encrypt it RouterInfo ri = getContext().netDb().lookupRouterInfoLocally(peer); diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/KademliaNetworkDatabaseFacade.java b/router/java/src/net/i2p/router/networkdb/kademlia/KademliaNetworkDatabaseFacade.java index b4d108d024..5507710cdc 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/KademliaNetworkDatabaseFacade.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/KademliaNetworkDatabaseFacade.java @@ -19,14 +19,20 @@ import java.util.Iterator; import java.util.Map; import java.util.Set; +import net.i2p.crypto.SigType; +import net.i2p.data.Certificate; import net.i2p.data.DatabaseEntry; +import net.i2p.data.DataFormatException; import net.i2p.data.DataHelper; +import net.i2p.data.Destination; import net.i2p.data.Hash; +import net.i2p.data.KeyCertificate; import net.i2p.data.LeaseSet; -import net.i2p.data.RouterAddress; -import net.i2p.data.RouterInfo; import net.i2p.data.i2np.DatabaseLookupMessage; import net.i2p.data.i2np.DatabaseStoreMessage; +import net.i2p.data.router.RouterAddress; +import net.i2p.data.router.RouterIdentity; +import net.i2p.data.router.RouterInfo; import net.i2p.kademlia.KBucketSet; import net.i2p.kademlia.RejectTrimmer; import net.i2p.kademlia.SelectionCollector; @@ -63,6 +69,7 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { protected final RouterContext _context; private final ReseedChecker _reseedChecker; private volatile long _lastRIPublishTime; + private NegativeLookupCache _negativeCache; /** * Map of Hash to RepublishLeaseSetJob for leases we'realready managing. @@ -155,6 +162,7 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { _reseedChecker = new ReseedChecker(context); context.statManager().createRateStat("netDb.lookupDeferred", "how many lookups are deferred?", "NetworkDatabase", new long[] { 60*60*1000 }); context.statManager().createRateStat("netDb.exploreKeySet", "how many keys are queued for exploration?", "NetworkDatabase", new long[] { 60*60*1000 }); + context.statManager().createRateStat("netDb.negativeCache", "Aborted lookup, already cached", "NetworkDatabase", new long[] { 60*60*1000l }); // following are for StoreJob context.statManager().createRateStat("netDb.storeRouterInfoSent", "How many routerInfo store messages have we sent?", "NetworkDatabase", new long[] { 60*60*1000l }); context.statManager().createRateStat("netDb.storeLeaseSetSent", "How many leaseSet store messages have we sent?", "NetworkDatabase", new long[] { 60*60*1000l }); @@ -223,6 +231,7 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { //_ds = null; _exploreKeys.clear(); // hope this doesn't cause an explosion, it shouldn't. // _exploreKeys = null; + _negativeCache.clear(); } public synchronized void restart() { @@ -262,6 +271,7 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { //_ds = new TransientDataStore(); // _exploreKeys = new HashSet(64); _dbDir = dbDir; + _negativeCache = new NegativeLookupCache(); createHandlers(); @@ -480,7 +490,8 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { } /** - * Lookup using exploratory tunnels + * Lookup using exploratory tunnels. + * Use lookupDestination() if you don't need the LS or need it validated. */ public void lookupLeaseSet(Hash key, Job onFindJob, Job onFailedLookupJob, long timeoutMs) { lookupLeaseSet(key, onFindJob, onFailedLookupJob, timeoutMs, null); @@ -488,6 +499,8 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { /** * Lookup using the client's tunnels + * Use lookupDestination() if you don't need the LS or need it validated. + * * @param fromLocalDest use these tunnels for the lookup, or null for exploratory * @since 0.9.10 */ @@ -500,6 +513,11 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { _log.debug("leaseSet found locally, firing " + onFindJob); if (onFindJob != null) _context.jobQueue().addJob(onFindJob); + } else if (isNegativeCached(key)) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Negative cached, not searching: " + key); + if (onFailedLookupJob != null) + _context.jobQueue().addJob(onFailedLookupJob); } else { if (_log.shouldLog(Log.DEBUG)) _log.debug("leaseSet not found locally, running search"); @@ -509,6 +527,9 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { _log.debug("after lookupLeaseSet"); } + /** + * Use lookupDestination() if you don't need the LS or need it validated. + */ public LeaseSet lookupLeaseSetLocally(Hash key) { if (!_initialized) return null; DatabaseEntry ds = _ds.get(key); @@ -531,6 +552,47 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { return null; } } + + /** + * Lookup using the client's tunnels + * Succeeds even if LS validation and store fails due to unsupported sig type, expired, etc. + * + * Note that there are not separate success and fail jobs. Caller must call + * lookupDestinationLocally() in the job to determine success. + * + * @param onFinishedJob non-null + * @param fromLocalDest use these tunnels for the lookup, or null for exploratory + * @since 0.9.16 + */ + public void lookupDestination(Hash key, Job onFinishedJob, long timeoutMs, Hash fromLocalDest) { + if (!_initialized) return; + Destination d = lookupDestinationLocally(key); + if (d != null) { + _context.jobQueue().addJob(onFinishedJob); + } else { + search(key, onFinishedJob, onFinishedJob, timeoutMs, true, fromLocalDest); + } + } + + /** + * Lookup locally in netDB and in badDest cache + * Succeeds even if LS validation fails due to unsupported sig type, expired, etc. + * + * @since 0.9.16 + */ + public Destination lookupDestinationLocally(Hash key) { + if (!_initialized) return null; + DatabaseEntry ds = _ds.get(key); + if (ds != null) { + if (ds.getType() == DatabaseEntry.KEY_TYPE_LEASESET) { + LeaseSet ls = (LeaseSet)ds; + return ls.getDestination(); + } + } else { + return _negativeCache.getBadDest(key); + } + return null; + } public void lookupRouterInfo(Hash key, Job onFindJob, Job onFailedLookupJob, long timeoutMs) { if (!_initialized) return; @@ -538,6 +600,9 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { if (ri != null) { if (onFindJob != null) _context.jobQueue().addJob(onFindJob); + } else if (_context.banlist().isBanlistedForever(key)) { + if (onFailedLookupJob != null) + _context.jobQueue().addJob(onFailedLookupJob); } else { search(key, onFindJob, onFailedLookupJob, timeoutMs, false); } @@ -694,9 +759,10 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { * Unlike for RouterInfos, this is only called once, when stored. * After that, LeaseSet.isCurrent() is used. * + * @throws UnsupportedCryptoException if that's why it failed. * @return reason why the entry is not valid, or null if it is valid */ - private String validate(Hash key, LeaseSet leaseSet) { + private String validate(Hash key, LeaseSet leaseSet) throws UnsupportedCryptoException { if (!key.equals(leaseSet.getDestination().calculateHash())) { if (_log.shouldLog(Log.WARN)) _log.warn("Invalid store attempt! key does not match leaseSet.destination! key = " @@ -704,9 +770,11 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { return "Key does not match leaseSet.destination - " + key.toBase64(); } if (!leaseSet.verifySignature()) { + // throws UnsupportedCryptoException + processStoreFailure(key, leaseSet); if (_log.shouldLog(Log.WARN)) - _log.warn("Invalid leaseSet signature! leaseSet = " + leaseSet); - return "Invalid leaseSet signature on " + leaseSet.getDestination().calculateHash().toBase64(); + _log.warn("Invalid leaseSet signature! " + leaseSet); + return "Invalid leaseSet signature on " + key; } long earliest = leaseSet.getEarliestLeaseDate(); long latest = leaseSet.getLatestLeaseDate(); @@ -722,7 +790,7 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { + " first exp. " + new Date(earliest) + " last exp. " + new Date(latest), new Exception("Rejecting store")); - return "Expired leaseSet for " + leaseSet.getDestination().calculateHash().toBase64() + return "Expired leaseSet for " + leaseSet.getDestination().calculateHash() + " expired " + DataHelper.formatDuration(age) + " ago"; } if (latest > now + (Router.CLOCK_FUDGE_FACTOR + MAX_LEASE_FUTURE)) { @@ -739,9 +807,13 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { } /** - * Store the leaseSet + * Store the leaseSet. + * + * If the store fails due to unsupported crypto, it will negative cache + * the hash until restart. * * @throws IllegalArgumentException if the leaseSet is not valid + * @throws UnsupportedCryptoException if that's why it failed. * @return previous entry or null */ public LeaseSet store(Hash key, LeaseSet leaseSet) throws IllegalArgumentException { @@ -798,6 +870,10 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { * * Call this only on first store, to check the key and signature once * + * If the store fails due to unsupported crypto, it will banlist + * the router hash until restart and then throw UnsupportedCrytpoException. + * + * @throws UnsupportedCryptoException if that's why it failed. * @return reason why the entry is not valid, or null if it is valid */ private String validate(Hash key, RouterInfo routerInfo) throws IllegalArgumentException { @@ -807,6 +883,8 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { return "Key does not match routerInfo.identity"; } if (!routerInfo.isValid()) { + // throws UnsupportedCryptoException + processStoreFailure(key, routerInfo); if (_log.shouldLog(Log.WARN)) _log.warn("Invalid routerInfo signature! forged router structure! router = " + routerInfo); return "Invalid routerInfo signature"; @@ -892,15 +970,29 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { } /** - * store the routerInfo + * Store the routerInfo. + * + * If the store fails due to unsupported crypto, it will banlist + * the router hash until restart and then throw UnsupportedCrytpoException. * * @throws IllegalArgumentException if the routerInfo is not valid + * @throws UnsupportedCryptoException if that's why it failed. * @return previous entry or null */ public RouterInfo store(Hash key, RouterInfo routerInfo) throws IllegalArgumentException { return store(key, routerInfo, true); } + /** + * Store the routerInfo. + * + * If the store fails due to unsupported crypto, it will banlist + * the router hash until restart and then throw UnsupportedCrytpoException. + * + * @throws IllegalArgumentException if the routerInfo is not valid + * @throws UnsupportedCryptoException if that's why it failed. + * @return previous entry or null + */ RouterInfo store(Hash key, RouterInfo routerInfo, boolean persist) throws IllegalArgumentException { if (!_initialized) return null; @@ -934,6 +1026,59 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { _kb.add(key); return rv; } + + /** + * If the validate fails, call this + * to determine if it was because of unsupported crypto. + * + * If so, this will banlist-forever the router hash or permanently negative cache the dest hash, + * and then throw the exception. Otherwise it does nothing. + * + * @throws UnsupportedCryptoException if that's why it failed. + * @since 0.9.16 + */ + private void processStoreFailure(Hash h, DatabaseEntry entry) throws UnsupportedCryptoException { + if (entry.getHash().equals(h)) { + if (entry.getType() == DatabaseEntry.KEY_TYPE_LEASESET) { + LeaseSet ls = (LeaseSet) entry; + Destination d = ls.getDestination(); + Certificate c = d.getCertificate(); + if (c.getCertificateType() == Certificate.CERTIFICATE_TYPE_KEY) { + try { + KeyCertificate kc = c.toKeyCertificate(); + SigType type = kc.getSigType(); + if (type == null || !type.isAvailable()) { + failPermanently(d); + String stype = (type != null) ? type.toString() : Integer.toString(kc.getSigTypeCode()); + if (_log.shouldLog(Log.WARN)) + _log.warn("Unsupported sig type " + stype + " for destination " + h); + throw new UnsupportedCryptoException("Sig type " + stype); + } + } catch (DataFormatException dfe) {} + } + } else if (entry.getType() == DatabaseEntry.KEY_TYPE_ROUTERINFO) { + RouterInfo ri = (RouterInfo) entry; + RouterIdentity id = ri.getIdentity(); + Certificate c = id.getCertificate(); + if (c.getCertificateType() == Certificate.CERTIFICATE_TYPE_KEY) { + try { + KeyCertificate kc = c.toKeyCertificate(); + SigType type = kc.getSigType(); + if (type == null || !type.isAvailable()) { + String stype = (type != null) ? type.toString() : Integer.toString(kc.getSigTypeCode()); + _context.banlist().banlistRouterForever(h, "Unsupported signature type " + stype); + if (_log.shouldLog(Log.WARN)) + _log.warn("Unsupported sig type " + stype + " for router " + h); + throw new UnsupportedCryptoException("Sig type " + stype); + } + } catch (DataFormatException dfe) {} + } + } + } + if (_log.shouldLog(Log.WARN)) + _log.warn("Verify fail, cause unknown: " + entry); + } + /** * Final remove for a leaseset. @@ -1005,8 +1150,12 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { * without any match) * * Unused - called only by FNDF.searchFull() from FloodSearchJob which is overridden - don't use this. + * + * @throws UnsupportedOperationException always */ SearchJob search(Hash key, Job onFindJob, Job onFailedLookupJob, long timeoutMs, boolean isLease) { + throw new UnsupportedOperationException(); +/**** if (!_initialized) return null; boolean isNew = true; SearchJob searchJob = null; @@ -1031,6 +1180,7 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { _context.statManager().addRateData("netDb.lookupDeferred", deferred, searchJob.getExpiration()-_context.clock().now()); } return searchJob; +****/ } /** @@ -1102,6 +1252,47 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { _context.jobQueue().addJob(new StoreJob(_context, this, key, ds, onSuccess, onFailure, sendTimeout, toIgnore)); } + /** + * Increment in the negative lookup cache + * + * @param key for Destinations or RouterIdentities + * @since 0.9.4 moved from FNDF to KNDF in 0.9.16 + */ + void lookupFailed(Hash key) { + _negativeCache.lookupFailed(key); + } + + /** + * Is the key in the negative lookup cache? + *& + * @param key for Destinations or RouterIdentities + * @since 0.9.4 moved from FNDF to KNDF in 0.9.16 + */ + boolean isNegativeCached(Hash key) { + boolean rv = _negativeCache.isCached(key); + if (rv) + _context.statManager().addRateData("netDb.negativeCache", 1); + return rv; + } + + /** + * Negative cache until restart + * @since 0.9.16 + */ + void failPermanently(Destination dest) { + _negativeCache.failPermanently(dest); + } + + /** + * Is it permanently negative cached? + * + * @param key only for Destinations; for RouterIdentities, see Banlist + * @since 0.9.16 + */ + public boolean isNegativeCachedForever(Hash key) { + return _negativeCache.getBadDest(key) != null; + } + /** * Debug info, HTML formatted * @since 0.9.10 diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/MessageWrapper.java b/router/java/src/net/i2p/router/networkdb/kademlia/MessageWrapper.java index 15ae87a8bc..ff9d3d5011 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/MessageWrapper.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/MessageWrapper.java @@ -8,7 +8,7 @@ import net.i2p.crypto.TagSetHandle; import net.i2p.data.Certificate; import net.i2p.data.Hash; import net.i2p.data.PublicKey; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterInfo; import net.i2p.data.SessionKey; import net.i2p.data.SessionTag; import net.i2p.data.i2np.DeliveryInstructions; diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/NegativeLookupCache.java b/router/java/src/net/i2p/router/networkdb/kademlia/NegativeLookupCache.java index abbb67ed62..1784f9434d 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/NegativeLookupCache.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/NegativeLookupCache.java @@ -1,6 +1,9 @@ package net.i2p.router.networkdb.kademlia; +import java.util.Map; +import net.i2p.data.Destination; import net.i2p.data.Hash; +import net.i2p.util.LHMCache; import net.i2p.util.ObjectCounter; import net.i2p.util.SimpleScheduler; import net.i2p.util.SimpleTimer; @@ -12,11 +15,15 @@ import net.i2p.util.SimpleTimer; */ class NegativeLookupCache { private final ObjectCounter counter; + private final Map badDests; + private static final int MAX_FAILS = 3; + private static final int MAX_BAD_DESTS = 128; private static final long CLEAN_TIME = 2*60*1000; public NegativeLookupCache() { this.counter = new ObjectCounter(); + this.badDests = new LHMCache(MAX_BAD_DESTS); SimpleScheduler.getInstance().addPeriodicEvent(new Cleaner(), CLEAN_TIME); } @@ -25,7 +32,46 @@ class NegativeLookupCache { } public boolean isCached(Hash h) { - return this.counter.count(h) >= MAX_FAILS; + if (counter.count(h) >= MAX_FAILS) + return true; + synchronized(badDests) { + return badDests.get(h) != null; + } + } + + /** + * Negative cache the hash until restart, + * but cache the destination. + * + * @since 0.9.16 + */ + public void failPermanently(Destination dest) { + Hash h = dest.calculateHash(); + synchronized(badDests) { + badDests.put(h, dest); + } + } + + /** + * Get an unsupported but cached Destination + * + * @return dest or null if not cached + * @since 0.9.16 + */ + public Destination getBadDest(Hash h) { + synchronized(badDests) { + return badDests.get(h); + } + } + + /** + * @since 0.9.16 + */ + public void clear() { + counter.clear(); + synchronized(badDests) { + badDests.clear(); + } } private class Cleaner implements SimpleTimer.TimedEvent { diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/PeerSelector.java b/router/java/src/net/i2p/router/networkdb/kademlia/PeerSelector.java index f73eb00fef..4eb23e13cb 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/PeerSelector.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/PeerSelector.java @@ -16,7 +16,7 @@ import java.util.Set; import java.util.TreeMap; import net.i2p.data.Hash; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterInfo; import net.i2p.kademlia.KBucketSet; import net.i2p.kademlia.SelectionCollector; import net.i2p.router.RouterContext; diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/PersistentDataStore.java b/router/java/src/net/i2p/router/networkdb/kademlia/PersistentDataStore.java index 620679b7e4..6c60e3a41c 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/PersistentDataStore.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/PersistentDataStore.java @@ -29,7 +29,7 @@ import net.i2p.data.Base64; import net.i2p.data.DatabaseEntry; import net.i2p.data.DataFormatException; import net.i2p.data.Hash; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterInfo; import net.i2p.router.JobImpl; import net.i2p.router.Router; import net.i2p.router.RouterContext; diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/RefreshRoutersJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/RefreshRoutersJob.java index 852a5c7920..20dd2a0754 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/RefreshRoutersJob.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/RefreshRoutersJob.java @@ -5,7 +5,7 @@ import java.util.List; import java.util.Set; import net.i2p.data.Hash; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterInfo; import net.i2p.router.JobImpl; import net.i2p.router.RouterContext; import net.i2p.util.Log; diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/SearchJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/SearchJob.java index 54a9383196..f0edded8b0 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/SearchJob.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/SearchJob.java @@ -17,11 +17,12 @@ import net.i2p.data.DatabaseEntry; import net.i2p.data.DataHelper; import net.i2p.data.Hash; import net.i2p.data.LeaseSet; -import net.i2p.data.RouterInfo; import net.i2p.data.TunnelId; import net.i2p.data.i2np.DatabaseLookupMessage; import net.i2p.data.i2np.DatabaseSearchReplyMessage; import net.i2p.data.i2np.DatabaseStoreMessage; +import net.i2p.data.i2np.I2NPMessage; +import net.i2p.data.router.RouterInfo; import net.i2p.router.Job; import net.i2p.router.JobImpl; import net.i2p.router.OutNetMessage; @@ -201,22 +202,29 @@ class SearchJob extends JobImpl { _log.debug(getJobId() + ": Already completed"); return; } + if (_state.isAborted()) { + if (_log.shouldLog(Log.INFO)) + _log.info(getJobId() + ": Search aborted"); + _state.complete(); + fail(); + return; + } if (_log.shouldLog(Log.INFO)) _log.info(getJobId() + ": Searching: " + _state); if (isLocal()) { if (_log.shouldLog(Log.INFO)) _log.info(getJobId() + ": Key found locally"); - _state.complete(true); + _state.complete(); succeed(); } else if (isExpired()) { if (_log.shouldLog(Log.INFO)) _log.info(getJobId() + ": Key search expired"); - _state.complete(true); + _state.complete(); fail(); } else if (_state.getAttempted().size() > MAX_PEERS_QUERIED) { if (_log.shouldLog(Log.INFO)) _log.info(getJobId() + ": Too many peers quried"); - _state.complete(true); + _state.complete(); fail(); } else { //_log.debug("Continuing search"); @@ -424,7 +432,7 @@ class SearchJob extends JobImpl { int timeout = getPerPeerTimeoutMs(to); long expiration = getContext().clock().now() + timeout; - DatabaseLookupMessage msg = buildMessage(inTunnelId, inTunnel.getPeer(0), expiration); + I2NPMessage msg = buildMessage(inTunnelId, inTunnel.getPeer(0), expiration, router); TunnelInfo outTunnel = getContext().tunnelManager().selectOutboundExploratoryTunnel(to); if (outTunnel == null) { @@ -437,9 +445,9 @@ class SearchJob extends JobImpl { if (_log.shouldLog(Log.DEBUG)) _log.debug(getJobId() + ": Sending search to " + to - + " for " + msg.getSearchKey().toBase64() + " w/ replies through [" - + msg.getFrom().toBase64() + "] via tunnel [" - + msg.getReplyTunnel() + "]"); + + " for " + getState().getTarget() + " w/ replies through " + + inTunnel.getPeer(0) + " via tunnel " + + inTunnelId); SearchMessageSelector sel = new SearchMessageSelector(getContext(), router, _expiration, _state); SearchUpdateReplyFoundJob reply = new SearchUpdateReplyFoundJob(getContext(), router, _state, _facade, @@ -482,8 +490,11 @@ class SearchJob extends JobImpl { * @param replyTunnelId tunnel to receive replies through * @param replyGateway gateway for the reply tunnel * @param expiration when the search should stop + * @param peer unused here; see ExploreJob extension + * + * @return a DatabaseLookupMessage */ - protected DatabaseLookupMessage buildMessage(TunnelId replyTunnelId, Hash replyGateway, long expiration) { + protected I2NPMessage buildMessage(TunnelId replyTunnelId, Hash replyGateway, long expiration, RouterInfo peer) { DatabaseLookupMessage msg = new DatabaseLookupMessage(getContext(), true); msg.setSearchKey(_state.getTarget()); //msg.setFrom(replyGateway.getIdentity().getHash()); diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/SearchMessageSelector.java b/router/java/src/net/i2p/router/networkdb/kademlia/SearchMessageSelector.java index 3d756529d8..30e8e4b18a 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/SearchMessageSelector.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/SearchMessageSelector.java @@ -3,7 +3,7 @@ package net.i2p.router.networkdb.kademlia; import java.util.concurrent.atomic.AtomicInteger; import net.i2p.data.Hash; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterInfo; import net.i2p.data.i2np.DatabaseSearchReplyMessage; import net.i2p.data.i2np.DatabaseStoreMessage; import net.i2p.data.i2np.I2NPMessage; diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/SearchReplyJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/SearchReplyJob.java index 8d8e5f19d9..a8354b096f 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/SearchReplyJob.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/SearchReplyJob.java @@ -2,7 +2,7 @@ package net.i2p.router.networkdb.kademlia; import net.i2p.data.Hash; import net.i2p.data.i2np.DatabaseSearchReplyMessage; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterInfo; import net.i2p.router.JobImpl; import net.i2p.router.RouterContext; import net.i2p.util.Log; diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/SearchState.java b/router/java/src/net/i2p/router/networkdb/kademlia/SearchState.java index 106c8ad4dc..61bd1b645d 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/SearchState.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/SearchState.java @@ -19,15 +19,16 @@ import net.i2p.router.RouterContext; */ class SearchState { private final RouterContext _context; - private final HashSet _pendingPeers; + private final Set _pendingPeers; private final Map _pendingPeerTimes; - private final HashSet _attemptedPeers; - private final HashSet _failedPeers; - private final HashSet _successfulPeers; - private final HashSet _repliedPeers; + private final Set _attemptedPeers; + private final Set _failedPeers; + private final Set _successfulPeers; + private final Set _repliedPeers; private final Hash _searchKey; private volatile long _completed; private volatile long _started; + private volatile boolean _aborted; public SearchState(RouterContext context, Hash key) { _context = context; @@ -87,10 +88,19 @@ class SearchState { return new HashSet(_failedPeers); } } + public boolean completed() { return _completed != -1; } - public void complete(boolean completed) { - if (completed) - _completed = _context.clock().now(); + + public void complete() { + _completed = _context.clock().now(); + } + + /** @since 0.9.16 */ + public boolean isAborted() { return _aborted; } + + /** @since 0.9.16 */ + public void abort() { + _aborted = true; } public long getWhenStarted() { return _started; } @@ -177,6 +187,8 @@ class SearchState { buf.append(" completed? false "); else buf.append(" completed on ").append(new Date(_completed)); + if (_aborted) + buf.append(" (Aborted)"); buf.append("\n\tAttempted: "); synchronized (_attemptedPeers) { buf.append(_attemptedPeers.size()).append(' '); diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/SearchUpdateReplyFoundJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/SearchUpdateReplyFoundJob.java index 22602b497a..63cc1c18f8 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/SearchUpdateReplyFoundJob.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/SearchUpdateReplyFoundJob.java @@ -5,7 +5,7 @@ import java.util.Date; import net.i2p.data.DatabaseEntry; import net.i2p.data.Hash; import net.i2p.data.LeaseSet; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterInfo; import net.i2p.data.i2np.DatabaseSearchReplyMessage; import net.i2p.data.i2np.DatabaseStoreMessage; import net.i2p.data.i2np.I2NPMessage; @@ -18,24 +18,26 @@ import net.i2p.util.Log; /** * Called after a match to a db search is found * + * Used only by SearchJob which is only used by ExploreJob */ class SearchUpdateReplyFoundJob extends JobImpl implements ReplyJob { - private Log _log; + private final Log _log; private I2NPMessage _message; - private Hash _peer; - private SearchState _state; - private KademliaNetworkDatabaseFacade _facade; - private SearchJob _job; - private TunnelInfo _outTunnel; - private TunnelInfo _replyTunnel; - private boolean _isFloodfillPeer; - private long _sentOn; + private final Hash _peer; + private final SearchState _state; + private final KademliaNetworkDatabaseFacade _facade; + private final SearchJob _job; + private final TunnelInfo _outTunnel; + private final TunnelInfo _replyTunnel; + private final boolean _isFloodfillPeer; + private final long _sentOn; public SearchUpdateReplyFoundJob(RouterContext context, RouterInfo peer, SearchState state, KademliaNetworkDatabaseFacade facade, SearchJob job) { this(context, peer, state, facade, job, null, null); } + public SearchUpdateReplyFoundJob(RouterContext context, RouterInfo peer, SearchState state, KademliaNetworkDatabaseFacade facade, SearchJob job, TunnelInfo outTunnel, TunnelInfo replyTunnel) { @@ -52,6 +54,7 @@ class SearchUpdateReplyFoundJob extends JobImpl implements ReplyJob { } public String getName() { return "Update Reply Found for Kademlia Search"; } + public void runJob() { if (_isFloodfillPeer) _job.decrementOutstandingFloodfillSearches(); @@ -59,7 +62,7 @@ class SearchUpdateReplyFoundJob extends JobImpl implements ReplyJob { I2NPMessage message = _message; if (_log.shouldLog(Log.INFO)) _log.info(getJobId() + ": Reply from " + _peer.toBase64() - + " with message " + message.getClass().getName()); + + " with message " + message.getClass().getSimpleName()); long howLong = System.currentTimeMillis() - _sentOn; // assume requests are 1KB (they're almost always much smaller, but tunnels have a fixed size) @@ -78,34 +81,21 @@ class SearchUpdateReplyFoundJob extends JobImpl implements ReplyJob { if (message instanceof DatabaseStoreMessage) { long timeToReply = _state.dataFound(_peer); - DatabaseStoreMessage msg = (DatabaseStoreMessage)message; DatabaseEntry entry = msg.getEntry(); - if (entry.getType() == DatabaseEntry.KEY_TYPE_LEASESET) { - try { - _facade.store(msg.getKey(), (LeaseSet) entry); - getContext().profileManager().dbLookupSuccessful(_peer, timeToReply); - } catch (IllegalArgumentException iae) { - if (_log.shouldLog(Log.ERROR)) - _log.warn("Peer " + _peer + " sent us an invalid leaseSet: " + iae.getMessage()); - getContext().profileManager().dbLookupReply(_peer, 0, 0, 1, 0, timeToReply); - } - } else if (entry.getType() == DatabaseEntry.KEY_TYPE_ROUTERINFO) { - if (_log.shouldLog(Log.INFO)) - _log.info(getJobId() + ": dbStore received on search containing router " - + msg.getKey() + " with publishDate of " - + new Date(entry.getDate())); - try { - _facade.store(msg.getKey(), (RouterInfo) entry); - getContext().profileManager().dbLookupSuccessful(_peer, timeToReply); - } catch (IllegalArgumentException iae) { - if (_log.shouldLog(Log.ERROR)) - _log.warn("Peer " + _peer + " sent us an invalid routerInfo: " + iae.getMessage()); - getContext().profileManager().dbLookupReply(_peer, 0, 0, 1, 0, timeToReply); - } - } else { - if (_log.shouldLog(Log.ERROR)) - _log.error(getJobId() + ": Unknown db store type?!@ " + entry.getType()); + try { + _facade.store(msg.getKey(), entry); + getContext().profileManager().dbLookupSuccessful(_peer, timeToReply); + } catch (UnsupportedCryptoException iae) { + // don't blame the peer + getContext().profileManager().dbLookupSuccessful(_peer, timeToReply); + _state.abort(); + // searchNext() will call fail() + } catch (IllegalArgumentException iae) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Peer " + _peer + " sent us invalid data: ", iae); + // blame the peer + getContext().profileManager().dbLookupReply(_peer, 0, 0, 1, 0, timeToReply); } } else if (message instanceof DatabaseSearchReplyMessage) { _job.replyFound((DatabaseSearchReplyMessage)message, _peer); diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/SingleLookupJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/SingleLookupJob.java index 711510fb4d..62a878d6de 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/SingleLookupJob.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/SingleLookupJob.java @@ -1,7 +1,7 @@ package net.i2p.router.networkdb.kademlia; import net.i2p.data.Hash; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterInfo; import net.i2p.data.i2np.DatabaseSearchReplyMessage; import net.i2p.router.JobImpl; import net.i2p.router.RouterContext; diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/StartExplorersJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/StartExplorersJob.java index cced2f5466..7938ab072c 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/StartExplorersJob.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/StartExplorersJob.java @@ -12,7 +12,7 @@ import java.util.HashSet; import java.util.Set; import net.i2p.data.Hash; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterInfo; import net.i2p.router.JobImpl; import net.i2p.router.Router; import net.i2p.router.RouterContext; diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/StoreJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/StoreJob.java index 311f2ba8f1..81bef6950a 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/StoreJob.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/StoreJob.java @@ -18,7 +18,7 @@ import net.i2p.data.DatabaseEntry; import net.i2p.data.DataFormatException; import net.i2p.data.Hash; import net.i2p.data.LeaseSet; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterInfo; import net.i2p.data.TunnelId; import net.i2p.data.i2np.DatabaseStoreMessage; import net.i2p.data.i2np.I2NPMessage; diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/StoreMessageSelector.java b/router/java/src/net/i2p/router/networkdb/kademlia/StoreMessageSelector.java index 6901f0afa6..352c19cd71 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/StoreMessageSelector.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/StoreMessageSelector.java @@ -1,7 +1,7 @@ package net.i2p.router.networkdb.kademlia; import net.i2p.data.Hash; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterInfo; import net.i2p.data.i2np.DeliveryStatusMessage; import net.i2p.data.i2np.I2NPMessage; import net.i2p.router.MessageSelector; diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/TransientDataStore.java b/router/java/src/net/i2p/router/networkdb/kademlia/TransientDataStore.java index 2a7318d25f..11603efb98 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/TransientDataStore.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/TransientDataStore.java @@ -18,7 +18,7 @@ import java.util.Set; import net.i2p.data.DatabaseEntry; import net.i2p.data.Hash; import net.i2p.data.LeaseSet; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterInfo; import net.i2p.router.RouterContext; import net.i2p.util.Log; diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/UnsupportedCryptoException.java b/router/java/src/net/i2p/router/networkdb/kademlia/UnsupportedCryptoException.java new file mode 100644 index 0000000000..0159eb0201 --- /dev/null +++ b/router/java/src/net/i2p/router/networkdb/kademlia/UnsupportedCryptoException.java @@ -0,0 +1,18 @@ +package net.i2p.router.networkdb.kademlia; + +/** + * Signature verification failed because the + * sig type is unknown or unavailable. + * + * @since 0.9.16 + */ +public class UnsupportedCryptoException extends IllegalArgumentException { + + public UnsupportedCryptoException(String msg) { + super(msg); + } + + public UnsupportedCryptoException(String msg, Throwable t) { + super(msg, t); + } +} diff --git a/router/java/src/net/i2p/router/peermanager/PeerManager.java b/router/java/src/net/i2p/router/peermanager/PeerManager.java index ef7b1de349..d2de16d9ba 100644 --- a/router/java/src/net/i2p/router/peermanager/PeerManager.java +++ b/router/java/src/net/i2p/router/peermanager/PeerManager.java @@ -19,7 +19,7 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import net.i2p.data.Hash; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterInfo; import net.i2p.router.PeerSelectionCriteria; import net.i2p.router.Router; import net.i2p.router.RouterContext; diff --git a/router/java/src/net/i2p/router/peermanager/PeerTestJob.java b/router/java/src/net/i2p/router/peermanager/PeerTestJob.java index 24176a2088..466c63ec19 100644 --- a/router/java/src/net/i2p/router/peermanager/PeerTestJob.java +++ b/router/java/src/net/i2p/router/peermanager/PeerTestJob.java @@ -5,7 +5,7 @@ import java.util.List; import java.util.Set; import net.i2p.data.Hash; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterInfo; import net.i2p.data.TunnelId; import net.i2p.data.i2np.DatabaseStoreMessage; import net.i2p.data.i2np.DeliveryStatusMessage; diff --git a/router/java/src/net/i2p/router/peermanager/ProfileOrganizer.java b/router/java/src/net/i2p/router/peermanager/ProfileOrganizer.java index a224f576ac..7e89cab255 100644 --- a/router/java/src/net/i2p/router/peermanager/ProfileOrganizer.java +++ b/router/java/src/net/i2p/router/peermanager/ProfileOrganizer.java @@ -19,8 +19,8 @@ import java.util.concurrent.locks.ReentrantReadWriteLock; import net.i2p.crypto.SHA256Generator; import net.i2p.data.Hash; -import net.i2p.data.RouterAddress; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterAddress; +import net.i2p.data.router.RouterInfo; import net.i2p.router.NetworkDatabaseFacade; import net.i2p.router.RouterContext; import net.i2p.router.tunnel.pool.TunnelPeerSelector; diff --git a/router/java/src/net/i2p/router/startup/BootCommSystemJob.java b/router/java/src/net/i2p/router/startup/BootCommSystemJob.java index 3af4164b5e..305869bada 100644 --- a/router/java/src/net/i2p/router/startup/BootCommSystemJob.java +++ b/router/java/src/net/i2p/router/startup/BootCommSystemJob.java @@ -16,7 +16,7 @@ import net.i2p.router.tasks.ReadConfigJob; import net.i2p.util.Log; /** This actually boots almost everything */ -public class BootCommSystemJob extends JobImpl { +class BootCommSystemJob extends JobImpl { private Log _log; public static final String PROP_USE_TRUSTED_LINKS = "router.trustedLinks"; diff --git a/router/java/src/net/i2p/router/startup/BootNetworkDbJob.java b/router/java/src/net/i2p/router/startup/BootNetworkDbJob.java index bf8d36a772..e512f9ea38 100644 --- a/router/java/src/net/i2p/router/startup/BootNetworkDbJob.java +++ b/router/java/src/net/i2p/router/startup/BootNetworkDbJob.java @@ -12,7 +12,7 @@ import net.i2p.router.JobImpl; import net.i2p.router.RouterContext; /** start up the network database */ -public class BootNetworkDbJob extends JobImpl { +class BootNetworkDbJob extends JobImpl { public BootNetworkDbJob(RouterContext ctx) { super(ctx); diff --git a/router/java/src/net/i2p/router/startup/BootPeerManagerJob.java b/router/java/src/net/i2p/router/startup/BootPeerManagerJob.java index 7ac5254f0e..33f4010236 100644 --- a/router/java/src/net/i2p/router/startup/BootPeerManagerJob.java +++ b/router/java/src/net/i2p/router/startup/BootPeerManagerJob.java @@ -12,7 +12,7 @@ import net.i2p.router.JobImpl; import net.i2p.router.RouterContext; /** start up the peer manager */ -public class BootPeerManagerJob extends JobImpl { +class BootPeerManagerJob extends JobImpl { public BootPeerManagerJob(RouterContext ctx) { super(ctx); diff --git a/router/java/src/net/i2p/router/startup/BuildTrustedLinksJob.java b/router/java/src/net/i2p/router/startup/BuildTrustedLinksJob.java index 3b88a2d9cd..566204068b 100644 --- a/router/java/src/net/i2p/router/startup/BuildTrustedLinksJob.java +++ b/router/java/src/net/i2p/router/startup/BuildTrustedLinksJob.java @@ -15,7 +15,7 @@ import net.i2p.router.RouterContext; /** * For future restricted routes. Does nothing now. */ -public class BuildTrustedLinksJob extends JobImpl { +class BuildTrustedLinksJob extends JobImpl { private final Job _next; public BuildTrustedLinksJob(RouterContext context, Job next) { diff --git a/router/java/src/net/i2p/router/startup/CreateRouterInfoJob.java b/router/java/src/net/i2p/router/startup/CreateRouterInfoJob.java index 498cbe6658..83f2c7a7ef 100644 --- a/router/java/src/net/i2p/router/startup/CreateRouterInfoJob.java +++ b/router/java/src/net/i2p/router/startup/CreateRouterInfoJob.java @@ -12,16 +12,22 @@ import java.io.BufferedOutputStream; import java.io.File; import java.io.IOException; import java.io.OutputStream; +import java.security.GeneralSecurityException; import java.util.Properties; +import net.i2p.crypto.SigType; import net.i2p.data.Certificate; import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.data.KeyCertificate; import net.i2p.data.PrivateKey; +import net.i2p.data.PrivateKeyFile; import net.i2p.data.PublicKey; -import net.i2p.data.RouterIdentity; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterIdentity; +import net.i2p.data.router.RouterInfo; import net.i2p.data.SigningPrivateKey; import net.i2p.data.SigningPublicKey; +import net.i2p.data.SimpleDataStructure; import net.i2p.router.Job; import net.i2p.router.JobImpl; import net.i2p.router.Router; @@ -40,7 +46,14 @@ public class CreateRouterInfoJob extends JobImpl { private final Log _log; private final Job _next; - public CreateRouterInfoJob(RouterContext ctx, Job next) { + public static final String INFO_FILENAME = "router.info"; + public static final String KEYS_FILENAME = "router.keys"; + public static final String KEYS2_FILENAME = "router.keys.dat"; + private static final String PROP_ROUTER_SIGTYPE = "router.sigType"; + /** TODO when changing, check isAvailable() and fallback to DSA_SHA1 */ + private static final SigType DEFAULT_SIGTYPE = SigType.DSA_SHA1; + + CreateRouterInfoJob(RouterContext ctx, Job next) { super(ctx); _next = next; _log = ctx.logManager().getLog(CreateRouterInfoJob.class); @@ -59,9 +72,13 @@ public class CreateRouterInfoJob extends JobImpl { /** * Writes 6 files: router.info (standard RI format), - * router,keys, and 4 individual key files under keyBackup/ + * router.keys2, and 4 individual key files under keyBackup/ * - * router.keys file format: Note that this is NOT the + * router.keys2 file format: This is the + * same "eepPriv.dat" format used by the client code, + * as documented in PrivateKeyFile. + * + * Old router.keys file format: Note that this is NOT the * same "eepPriv.dat" format used by the client code. *
      *   - Private key (256 bytes)
@@ -74,9 +91,9 @@ public class CreateRouterInfoJob extends JobImpl {
      *  Caller must hold Router.routerInfoFileLock.
      */
     RouterInfo createRouterInfo() {
+        SigType type = getSigTypeConfig(getContext());
         RouterInfo info = new RouterInfo();
         OutputStream fos1 = null;
-        OutputStream fos2 = null;
         try {
             info.setAddresses(getContext().commSystem().createAddresses());
             Properties stats = getContext().statPublisher().publishStatistics();
@@ -86,21 +103,26 @@ public class CreateRouterInfoJob extends JobImpl {
             // not necessary, in constructor
             //info.setPeers(new HashSet());
             info.setPublished(getCurrentPublishDate(getContext()));
-            RouterIdentity ident = new RouterIdentity();
-            Certificate cert = getContext().router().createCertificate();
-            ident.setCertificate(cert);
-            PublicKey pubkey = null;
-            PrivateKey privkey = null;
-            SigningPublicKey signingPubKey = null;
-            SigningPrivateKey signingPrivKey = null;
             Object keypair[] = getContext().keyGenerator().generatePKIKeypair();
-            pubkey = (PublicKey)keypair[0];
-            privkey = (PrivateKey)keypair[1];
-            Object signingKeypair[] = getContext().keyGenerator().generateSigningKeypair();
-            signingPubKey = (SigningPublicKey)signingKeypair[0];
-            signingPrivKey = (SigningPrivateKey)signingKeypair[1];
+            PublicKey pubkey = (PublicKey)keypair[0];
+            PrivateKey privkey = (PrivateKey)keypair[1];
+            SimpleDataStructure signingKeypair[] = getContext().keyGenerator().generateSigningKeys(type);
+            SigningPublicKey signingPubKey = (SigningPublicKey)signingKeypair[0];
+            SigningPrivateKey signingPrivKey = (SigningPrivateKey)signingKeypair[1];
+            RouterIdentity ident = new RouterIdentity();
+            Certificate cert = createCertificate(getContext(), signingPubKey);
+            ident.setCertificate(cert);
             ident.setPublicKey(pubkey);
             ident.setSigningPublicKey(signingPubKey);
+            byte[] padding;
+            int padLen = SigningPublicKey.KEYSIZE_BYTES - signingPubKey.length();
+            if (padLen > 0) {
+                padding = new byte[padLen];
+                getContext().random().nextBytes(padding);
+                ident.setPadding(padding);
+            } else {
+                padding = null;
+            }
             info.setIdentity(ident);
             
             info.sign(signingPrivKey);
@@ -108,34 +130,54 @@ public class CreateRouterInfoJob extends JobImpl {
             if (!info.isValid())
                 throw new DataFormatException("RouterInfo we just built is invalid: " + info);
             
-            String infoFilename = getContext().getProperty(Router.PROP_INFO_FILENAME, Router.PROP_INFO_FILENAME_DEFAULT);
-            File ifile = new File(getContext().getRouterDir(), infoFilename);
+            // remove router.keys
+            (new File(getContext().getRouterDir(), KEYS_FILENAME)).delete();
+
+            // write router.info
+            File ifile = new File(getContext().getRouterDir(), INFO_FILENAME);
             fos1 = new BufferedOutputStream(new SecureFileOutputStream(ifile));
             info.writeBytes(fos1);
             
-            String keyFilename = getContext().getProperty(Router.PROP_KEYS_FILENAME, Router.PROP_KEYS_FILENAME_DEFAULT);
-            File kfile = new File(getContext().getRouterDir(), keyFilename);
-            fos2 = new BufferedOutputStream(new SecureFileOutputStream(kfile));
-            privkey.writeBytes(fos2);
-            signingPrivKey.writeBytes(fos2);
-            pubkey.writeBytes(fos2);
-            signingPubKey.writeBytes(fos2);
+            // write router.keys.dat
+            File kfile = new File(getContext().getRouterDir(), KEYS2_FILENAME);
+            PrivateKeyFile pkf = new PrivateKeyFile(kfile, pubkey, signingPubKey, cert,
+                                                    privkey, signingPrivKey, padding);
+            pkf.write();
             
             getContext().keyManager().setKeys(pubkey, privkey, signingPubKey, signingPrivKey);
             
-            _log.info("Router info created and stored at " + ifile.getAbsolutePath() + " with private keys stored at " + kfile.getAbsolutePath() + " [" + info + "]");
+            if (_log.shouldLog(Log.INFO))
+                _log.info("Router info created and stored at " + ifile.getAbsolutePath() + " with private keys stored at " + kfile.getAbsolutePath() + " [" + info + "]");
             getContext().router().eventLog().addEvent(EventLog.REKEYED, ident.calculateHash().toBase64());
+        } catch (GeneralSecurityException gse) {
+            _log.log(Log.CRIT, "Error building the new router information", gse);
         } catch (DataFormatException dfe) {
             _log.log(Log.CRIT, "Error building the new router information", dfe);
         } catch (IOException ioe) {
             _log.log(Log.CRIT, "Error writing out the new router information", ioe);
         } finally {
             if (fos1 != null) try { fos1.close(); } catch (IOException ioe) {}
-            if (fos2 != null) try { fos2.close(); } catch (IOException ioe) {}
         }
         return info;
     }
     
+    /**
+     *  The configured SigType to expect on read-in
+     *  @since 0.9.16
+     */
+    public static SigType getSigTypeConfig(RouterContext ctx) {
+        SigType cstype = CreateRouterInfoJob.DEFAULT_SIGTYPE;
+        String sstype = ctx.getProperty(PROP_ROUTER_SIGTYPE);
+        if (sstype != null) {
+            SigType ntype = SigType.parseSigType(sstype);
+            if (ntype != null)
+                cstype = ntype;
+        }
+        // fallback?
+        if (cstype != SigType.DSA_SHA1 && !cstype.isAvailable())
+            cstype = SigType.DSA_SHA1;
+        return cstype;
+    }
     
     /**
      * We probably don't want to expose the exact time at which a router published its info.
@@ -146,4 +188,22 @@ public class CreateRouterInfoJob extends JobImpl {
         //_log.info("Setting published date to /now/");
         return context.clock().now();
     }
+
+    /**
+     *  Only called at startup via LoadRouterInfoJob and RebuildRouterInfoJob.
+     *  Not called by periodic RepublishLocalRouterInfoJob.
+     *  We don't want to change the cert on the fly as it changes the router hash.
+     *  RouterInfo.isHidden() checks the capability, but RouterIdentity.isHidden() checks the cert.
+     *  There's no reason to ever add a hidden cert?
+     *
+     *  @return the certificate for a new RouterInfo - probably a null cert.
+     *  @since 0.9.16 moved from Router
+     */
+    static Certificate createCertificate(RouterContext ctx, SigningPublicKey spk) {
+        if (spk.getType() != SigType.DSA_SHA1)
+            return new KeyCertificate(spk);
+        if (ctx.getBooleanProperty(Router.PROP_HIDDEN))
+            return new Certificate(Certificate.CERTIFICATE_TYPE_HIDDEN, null);
+        return Certificate.NULL_CERT;
+    }
 }
diff --git a/router/java/src/net/i2p/router/startup/LoadRouterInfoJob.java b/router/java/src/net/i2p/router/startup/LoadRouterInfoJob.java
index 9a560da5f4..e02ef04aa1 100644
--- a/router/java/src/net/i2p/router/startup/LoadRouterInfoJob.java
+++ b/router/java/src/net/i2p/router/startup/LoadRouterInfoJob.java
@@ -15,18 +15,28 @@ import java.io.InputStream;
 import java.io.IOException;
 import java.util.concurrent.atomic.AtomicBoolean;
 
+import net.i2p.crypto.KeyGenerator;
+import net.i2p.crypto.SigType;
+import net.i2p.data.Certificate;
 import net.i2p.data.DataFormatException;
+import net.i2p.data.DataHelper;
 import net.i2p.data.PrivateKey;
 import net.i2p.data.PublicKey;
-import net.i2p.data.RouterInfo;
 import net.i2p.data.SigningPrivateKey;
 import net.i2p.data.SigningPublicKey;
+import net.i2p.data.router.RouterIdentity;
+import net.i2p.data.router.RouterInfo;
+import net.i2p.data.router.RouterPrivateKeyFile;
 import net.i2p.router.JobImpl;
 import net.i2p.router.Router;
 import net.i2p.router.RouterContext;
 import net.i2p.util.Log;
 
-public class LoadRouterInfoJob extends JobImpl {
+/**
+ *  Run once or twice at startup by StartupJob,
+ *  and then runs BootCommSystemJob
+ */
+class LoadRouterInfoJob extends JobImpl {
     private final Log _log;
     private RouterInfo _us;
     private static final AtomicBoolean _keyLengthChecked = new AtomicBoolean();
@@ -45,6 +55,7 @@ public class LoadRouterInfoJob extends JobImpl {
         if (_us == null) {
             RebuildRouterInfoJob r = new RebuildRouterInfoJob(getContext());
             r.rebuildRouterInfo(false);
+            // run a second time
             getContext().jobQueue().addJob(this);
             return;
         } else {
@@ -54,18 +65,21 @@ public class LoadRouterInfoJob extends JobImpl {
         }
     }
     
+    /**
+     *  Loads router.info and router.keys2 or router.keys.
+     *
+     *  See CreateRouterInfoJob for file formats
+     */
     private void loadRouterInfo() {
-        String routerInfoFile = getContext().getProperty(Router.PROP_INFO_FILENAME, Router.PROP_INFO_FILENAME_DEFAULT);
         RouterInfo info = null;
-        String keyFilename = getContext().getProperty(Router.PROP_KEYS_FILENAME, Router.PROP_KEYS_FILENAME_DEFAULT);
-        
-        File rif = new File(getContext().getRouterDir(), routerInfoFile);
+        File rif = new File(getContext().getRouterDir(), CreateRouterInfoJob.INFO_FILENAME);
         boolean infoExists = rif.exists();
-        File rkf = new File(getContext().getRouterDir(), keyFilename);
+        File rkf = new File(getContext().getRouterDir(), CreateRouterInfoJob.KEYS_FILENAME);
         boolean keysExist = rkf.exists();
+        File rkf2 = new File(getContext().getRouterDir(), CreateRouterInfoJob.KEYS2_FILENAME);
+        boolean keys2Exist = rkf2.exists();
         
         InputStream fis1 = null;
-        InputStream fis2 = null;
         try {
             // if we have a routerinfo but no keys, things go bad in a hurry:
             // CRIT   ...rkdb.PublishLocalRouterInfoJob: Internal error - signing private key not known?  rescheduling publish for 30s
@@ -73,7 +87,7 @@ public class LoadRouterInfoJob extends JobImpl {
             // CRIT   ...sport.udp.EstablishmentManager: Error in the establisher java.lang.NullPointerException
             // at net.i2p.router.transport.udp.PacketBuilder.buildSessionConfirmedPacket(PacketBuilder.java:574)
             // so pretend the RI isn't there if there is no keyfile
-            if (infoExists && keysExist) {
+            if (infoExists && (keys2Exist || keysExist)) {
                 fis1 = new BufferedInputStream(new FileInputStream(rif));
                 info = new RouterInfo();
                 info.readBytes(fis1);
@@ -85,29 +99,32 @@ public class LoadRouterInfoJob extends JobImpl {
                 _us = info;
             }
             
-            if (keysExist) {
-                fis2 = new BufferedInputStream(new FileInputStream(rkf));
-                PrivateKey privkey = new PrivateKey();
-                privkey.readBytes(fis2);
-                if (shouldRebuild(privkey)) {
+            if (keys2Exist || keysExist) {
+                KeyData kd = readKeyData(rkf, rkf2);
+                PublicKey pubkey = kd.routerIdentity.getPublicKey();
+                SigningPublicKey signingPubKey = kd.routerIdentity.getSigningPublicKey();
+                PrivateKey privkey = kd.privateKey;
+                SigningPrivateKey signingPrivKey = kd.signingPrivateKey;
+                SigType stype = signingPubKey.getType();
+
+                // check if the sigtype config changed
+                SigType cstype = CreateRouterInfoJob.getSigTypeConfig(getContext());
+                boolean sigTypeChanged = stype != cstype;
+
+                if (sigTypeChanged || shouldRebuild(privkey)) {
+                    if (sigTypeChanged)
+                        _log.logAlways(Log.WARN, "Rebuilding RouterInfo with new signature type " + cstype);
                     _us = null;
                     // windows... close before deleting
                     if (fis1 != null) {
                         try { fis1.close(); } catch (IOException ioe) {}
                         fis1 = null;
                     }
-                    try { fis2.close(); } catch (IOException ioe) {}
-                    fis2 = null;
                     rif.delete();
                     rkf.delete();
+                    rkf2.delete();
                     return;
                 }
-                SigningPrivateKey signingPrivKey = new SigningPrivateKey();
-                signingPrivKey.readBytes(fis2);
-                PublicKey pubkey = new PublicKey();
-                pubkey.readBytes(fis2);
-                SigningPublicKey signingPubKey = new SigningPublicKey();
-                signingPubKey.readBytes(fis2);
                 
                 getContext().keyManager().setKeys(pubkey, privkey, signingPubKey, signingPrivKey);
             }
@@ -119,12 +136,9 @@ public class LoadRouterInfoJob extends JobImpl {
                 try { fis1.close(); } catch (IOException ioe2) {}
                 fis1 = null;
             }
-            if (fis2 != null) {
-                try { fis2.close(); } catch (IOException ioe2) {}
-                fis2 = null;
-            }
             rif.delete();
             rkf.delete();
+            rkf2.delete();
         } catch (DataFormatException dfe) {
             _log.log(Log.CRIT, "Corrupt router info or keys at " + rif.getAbsolutePath() + " / " + rkf.getAbsolutePath(), dfe);
             _us = null;
@@ -133,15 +147,11 @@ public class LoadRouterInfoJob extends JobImpl {
                 try { fis1.close(); } catch (IOException ioe) {}
                 fis1 = null;
             }
-            if (fis2 != null) {
-                try { fis2.close(); } catch (IOException ioe) {}
-                fis2 = null;
-            }
             rif.delete();
             rkf.delete();
+            rkf2.delete();
         } finally {
             if (fis1 != null) try { fis1.close(); } catch (IOException ioe) {}
-            if (fis2 != null) try { fis2.close(); } catch (IOException ioe) {}
         }
     }
 
@@ -174,4 +184,68 @@ public class LoadRouterInfoJob extends JobImpl {
             _log.logAlways(Log.WARN, "Rebuilding RouterInfo with faster key");
         return uselong != haslong;
     }
+
+    /** @since 0.9.16 */
+    public static class KeyData {
+        public final RouterIdentity routerIdentity;
+        public final PrivateKey privateKey;
+        public final SigningPrivateKey signingPrivateKey;
+
+        public KeyData(RouterIdentity ri, PrivateKey pk, SigningPrivateKey spk) {
+            routerIdentity = ri;
+            privateKey = pk;
+            signingPrivateKey = spk;
+        }
+    }
+
+    /**
+     *  @param rkf1 in router.keys format, tried second
+     *  @param rkf2 in eepPriv.dat format, tried first
+     *  @return non-null, throws IOE if neither exisits
+     *  @since 0.9.16
+     */
+    public static KeyData readKeyData(File rkf1, File rkf2) throws DataFormatException, IOException {
+        RouterIdentity ri;
+        PrivateKey privkey;
+        SigningPrivateKey signingPrivKey;
+        if (rkf2.exists()) {
+            RouterPrivateKeyFile pkf = new RouterPrivateKeyFile(rkf2);
+            ri = pkf.getRouterIdentity();
+            if (!pkf.validateKeyPairs())
+                throw new DataFormatException("Key pairs invalid");
+            privkey = pkf.getPrivKey();
+            signingPrivKey = pkf.getSigningPrivKey();
+        } else {
+            InputStream fis = null;
+            try {
+                fis = new BufferedInputStream(new FileInputStream(rkf1));
+                privkey = new PrivateKey();
+                privkey.readBytes(fis);
+                signingPrivKey = new SigningPrivateKey();
+                signingPrivKey.readBytes(fis);
+                PublicKey pubkey = new PublicKey();
+                pubkey.readBytes(fis);
+                SigningPublicKey signingPubKey = new SigningPublicKey();
+                signingPubKey.readBytes(fis);
+
+                // validate
+                try {
+                    if (!pubkey.equals(KeyGenerator.getPublicKey(privkey)))
+                        throw new DataFormatException("Key pairs invalid");
+                    if (!signingPubKey.equals(KeyGenerator.getSigningPublicKey(signingPrivKey)))
+                        throw new DataFormatException("Key pairs invalid");
+                } catch (IllegalArgumentException iae) {
+                    throw new DataFormatException("Key pairs invalid", iae);
+                }
+
+                ri = new RouterIdentity();
+                ri.setPublicKey(pubkey);
+                ri.setSigningPublicKey(signingPubKey);
+                ri.setCertificate(Certificate.NULL_CERT);
+            } finally {
+                if (fis != null) try { fis.close(); } catch (IOException ioe) {}
+            }
+        }
+        return new KeyData(ri, privkey, signingPrivKey);
+    }
 }
diff --git a/router/java/src/net/i2p/router/startup/RebuildRouterInfoJob.java b/router/java/src/net/i2p/router/startup/RebuildRouterInfoJob.java
index b611114d06..ef0826cf5f 100644
--- a/router/java/src/net/i2p/router/startup/RebuildRouterInfoJob.java
+++ b/router/java/src/net/i2p/router/startup/RebuildRouterInfoJob.java
@@ -9,22 +9,24 @@ package net.i2p.router.startup;
  */
 
 import java.io.File;
-import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.util.Properties;
 
+import net.i2p.crypto.SigType;
 import net.i2p.data.Certificate;
 import net.i2p.data.DataFormatException;
+import net.i2p.data.DataHelper;
 import net.i2p.data.PrivateKey;
 import net.i2p.data.PublicKey;
-import net.i2p.data.RouterIdentity;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterIdentity;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.data.SigningPrivateKey;
 import net.i2p.data.SigningPublicKey;
 import net.i2p.router.JobImpl;
 import net.i2p.router.Router;
 import net.i2p.router.RouterContext;
+import net.i2p.router.startup.LoadRouterInfoJob.KeyData;
 import net.i2p.util.Log;
 import net.i2p.util.SecureFileOutputStream;
 
@@ -44,7 +46,7 @@ import net.i2p.util.SecureFileOutputStream;
  * router.info.rebuild file is deleted
  *
  */
-public class RebuildRouterInfoJob extends JobImpl {
+class RebuildRouterInfoJob extends JobImpl {
     private final Log _log;
     
     private final static long REBUILD_DELAY = 45*1000; // every 30 seconds
@@ -57,11 +59,11 @@ public class RebuildRouterInfoJob extends JobImpl {
     public String getName() { return "Rebuild Router Info"; }
     
     public void runJob() {
+        throw new UnsupportedOperationException();
+/****
         _log.debug("Testing to rebuild router info");
-        String infoFile = getContext().getProperty(Router.PROP_INFO_FILENAME, Router.PROP_INFO_FILENAME_DEFAULT);
-        File info = new File(getContext().getRouterDir(), infoFile);
-        String keyFilename = getContext().getProperty(Router.PROP_KEYS_FILENAME, Router.PROP_KEYS_FILENAME_DEFAULT);
-        File keyFile = new File(getContext().getRouterDir(), keyFilename);
+        File info = new File(getContext().getRouterDir(), CreateRouterInfoJob.INFO_FILENAME);
+        File keyFile = new File(getContext().getRouterDir(), CreateRouterInfoJob.KEYS2_FILENAME);
         
         if (!info.exists() || !keyFile.exists()) {
             _log.info("Router info file [" + info.getAbsolutePath() + "] or private key file [" + keyFile.getAbsolutePath() + "] deleted, rebuilding");
@@ -71,51 +73,37 @@ public class RebuildRouterInfoJob extends JobImpl {
         }
         getTiming().setStartAfter(getContext().clock().now() + REBUILD_DELAY);
         getContext().jobQueue().addJob(this);
+****/
     }
     
     void rebuildRouterInfo() {
         rebuildRouterInfo(true);
     }
 
+    /**
+     *  @param alreadyRunning unused
+     */
     void rebuildRouterInfo(boolean alreadyRunning) {
         _log.debug("Rebuilding the new router info");
         RouterInfo info = null;
-        String infoFilename = getContext().getProperty(Router.PROP_INFO_FILENAME, Router.PROP_INFO_FILENAME_DEFAULT);
-        File infoFile = new File(getContext().getRouterDir(), infoFilename);
-        String keyFilename = getContext().getProperty(Router.PROP_KEYS_FILENAME, Router.PROP_KEYS_FILENAME_DEFAULT);
-        File keyFile = new File(getContext().getRouterDir(), keyFilename);
+        File infoFile = new File(getContext().getRouterDir(), CreateRouterInfoJob.INFO_FILENAME);
+        File keyFile = new File(getContext().getRouterDir(), CreateRouterInfoJob.KEYS_FILENAME);
+        File keyFile2 = new File(getContext().getRouterDir(), CreateRouterInfoJob.KEYS2_FILENAME);
         
-        if (keyFile.exists()) {
+        if (keyFile2.exists() || keyFile.exists()) {
             // ok, no need to rebuild a brand new identity, just update what we can
             RouterInfo oldinfo = getContext().router().getRouterInfo();
             if (oldinfo == null) {
-                info = new RouterInfo();
-                FileInputStream fis = null;
                 try {
-                    fis = new FileInputStream(keyFile);
-                    PrivateKey privkey = new PrivateKey();
-                    privkey.readBytes(fis);
-                    SigningPrivateKey signingPrivKey = new SigningPrivateKey();
-                    signingPrivKey.readBytes(fis);
-                    PublicKey pubkey = new PublicKey();
-                    pubkey.readBytes(fis);
-                    SigningPublicKey signingPubKey = new SigningPublicKey();
-                    signingPubKey.readBytes(fis);
-                    RouterIdentity ident = new RouterIdentity();
-                    Certificate cert = getContext().router().createCertificate();
-                    ident.setCertificate(cert);
-                    ident.setPublicKey(pubkey);
-                    ident.setSigningPublicKey(signingPubKey);
-                    info.setIdentity(ident);
+                    KeyData kd = LoadRouterInfoJob.readKeyData(keyFile, keyFile2);
+                    info = new RouterInfo();
+                    info.setIdentity(kd.routerIdentity);
                 } catch (Exception e) {
                     _log.log(Log.CRIT, "Error reading in the key data from " + keyFile.getAbsolutePath(), e);
-                    if (fis != null) try { fis.close(); } catch (IOException ioe) {}
-                    fis = null;
                     keyFile.delete();
+                    keyFile2.delete();
                     rebuildRouterInfo(alreadyRunning);
                     return;
-                } finally {
-                    if (fis != null) try { fis.close(); } catch (IOException ioe) {}
                 }
             } else {
                 // Make a new RI from the old identity, or else info.setAddresses() will throw an ISE
@@ -160,12 +148,14 @@ public class RebuildRouterInfoJob extends JobImpl {
             _log.warn("Private key file " + keyFile.getAbsolutePath() + " deleted!  Rebuilding a brand new router identity!");
             // this proc writes the keys and info to the file as well as builds the latest and greatest info
             CreateRouterInfoJob j = new CreateRouterInfoJob(getContext(), null);
-            info = j.createRouterInfo();
+            synchronized (getContext().router().routerInfoFileLock) {
+                info = j.createRouterInfo();
+            }
         }
         
         //MessageHistory.initialize();
         getContext().router().setRouterInfo(info);
-        _log.info("Router info rebuilt and stored at " + infoFilename + " [" + info + "]");
+        _log.info("Router info rebuilt and stored at " + infoFile + " [" + info + "]");
     }
     
 }
diff --git a/router/java/src/net/i2p/router/startup/StartAcceptingClientsJob.java b/router/java/src/net/i2p/router/startup/StartAcceptingClientsJob.java
index 50d840d432..672bf702aa 100644
--- a/router/java/src/net/i2p/router/startup/StartAcceptingClientsJob.java
+++ b/router/java/src/net/i2p/router/startup/StartAcceptingClientsJob.java
@@ -12,7 +12,7 @@ import net.i2p.router.JobImpl;
 import net.i2p.router.RouterContext;
 
 /** start I2CP interface */
-public class StartAcceptingClientsJob extends JobImpl {
+class StartAcceptingClientsJob extends JobImpl {
     
     public StartAcceptingClientsJob(RouterContext context) {
         super(context);
diff --git a/router/java/src/net/i2p/router/startup/WorkingDir.java b/router/java/src/net/i2p/router/startup/WorkingDir.java
index d44b2a5397..23149c7599 100644
--- a/router/java/src/net/i2p/router/startup/WorkingDir.java
+++ b/router/java/src/net/i2p/router/startup/WorkingDir.java
@@ -147,7 +147,7 @@ public class WorkingDir {
         // Check for a router.keys file or logs dir, if either exists it's an old install,
         // and only migrate the data files if told to do so
         // (router.keys could be deleted later by a killkeys())
-        test = new File(oldDirf, "router.keys");
+        test = new File(oldDirf, CreateRouterInfoJob.KEYS_FILENAME);
         boolean oldInstall = test.exists();
         if (!oldInstall) {
             test = new File(oldDirf, "logs");
diff --git a/router/java/src/net/i2p/router/tasks/GracefulShutdown.java b/router/java/src/net/i2p/router/tasks/GracefulShutdown.java
index c2ded9b491..5d86a291b0 100644
--- a/router/java/src/net/i2p/router/tasks/GracefulShutdown.java
+++ b/router/java/src/net/i2p/router/tasks/GracefulShutdown.java
@@ -31,7 +31,7 @@ public class GracefulShutdown implements Runnable {
                     else if (gracefulExitCode == Router.EXIT_HARD_RESTART)
                         log.log(Log.CRIT, "Restarting after a brief delay");
                     else
-                        log.log(Log.CRIT, "Graceful shutdown progress - no more tunnels, safe to die");
+                        log.log(Log.CRIT, "Graceful shutdown progress: No more tunnels, starting final shutdown");
                     // Allow time for a UI reponse
                     try {
                         synchronized (Thread.currentThread()) {
diff --git a/router/java/src/net/i2p/router/tasks/PersistRouterInfoJob.java b/router/java/src/net/i2p/router/tasks/PersistRouterInfoJob.java
index c29dd25772..6a14a51346 100644
--- a/router/java/src/net/i2p/router/tasks/PersistRouterInfoJob.java
+++ b/router/java/src/net/i2p/router/tasks/PersistRouterInfoJob.java
@@ -13,10 +13,10 @@ import java.io.FileOutputStream;
 import java.io.IOException;
 
 import net.i2p.data.DataFormatException;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.router.JobImpl;
-import net.i2p.router.Router;
 import net.i2p.router.RouterContext;
+import net.i2p.router.startup.CreateRouterInfoJob;
 import net.i2p.util.Log;
 import net.i2p.util.SecureFileOutputStream;
 
@@ -37,8 +37,7 @@ public class PersistRouterInfoJob extends JobImpl {
         if (_log.shouldLog(Log.DEBUG))
             _log.debug("Persisting updated router info");
 
-        String infoFilename = getContext().getProperty(Router.PROP_INFO_FILENAME, Router.PROP_INFO_FILENAME_DEFAULT);
-        File infoFile = new File(getContext().getRouterDir(), infoFilename);
+        File infoFile = new File(getContext().getRouterDir(), CreateRouterInfoJob.INFO_FILENAME);
 
         RouterInfo info = getContext().router().getRouterInfo();
 
diff --git a/router/java/src/net/i2p/router/transport/CommSystemFacadeImpl.java b/router/java/src/net/i2p/router/transport/CommSystemFacadeImpl.java
index bc3ad9543a..b9e38f9da3 100644
--- a/router/java/src/net/i2p/router/transport/CommSystemFacadeImpl.java
+++ b/router/java/src/net/i2p/router/transport/CommSystemFacadeImpl.java
@@ -17,8 +17,8 @@ import java.util.Locale;
 import java.util.Vector;
 
 import net.i2p.data.Hash;
-import net.i2p.data.RouterAddress;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterAddress;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.router.CommSystemFacade;
 import net.i2p.router.OutNetMessage;
 import net.i2p.router.RouterContext;
diff --git a/router/java/src/net/i2p/router/transport/Transport.java b/router/java/src/net/i2p/router/transport/Transport.java
index e232fa7e46..459c846806 100644
--- a/router/java/src/net/i2p/router/transport/Transport.java
+++ b/router/java/src/net/i2p/router/transport/Transport.java
@@ -14,8 +14,8 @@ import java.util.List;
 import java.util.Vector;
 
 import net.i2p.data.Hash;
-import net.i2p.data.RouterAddress;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterAddress;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.router.OutNetMessage;
 
 /**
diff --git a/router/java/src/net/i2p/router/transport/TransportEventListener.java b/router/java/src/net/i2p/router/transport/TransportEventListener.java
index 9a6d80d1ca..b8437efd1f 100644
--- a/router/java/src/net/i2p/router/transport/TransportEventListener.java
+++ b/router/java/src/net/i2p/router/transport/TransportEventListener.java
@@ -9,7 +9,7 @@ package net.i2p.router.transport;
  */
 
 import net.i2p.data.Hash;
-import net.i2p.data.RouterIdentity;
+import net.i2p.data.router.RouterIdentity;
 import net.i2p.data.i2np.I2NPMessage;
 
 public interface TransportEventListener {
diff --git a/router/java/src/net/i2p/router/transport/TransportImpl.java b/router/java/src/net/i2p/router/transport/TransportImpl.java
index 194ae3016d..6dd93b4c59 100644
--- a/router/java/src/net/i2p/router/transport/TransportImpl.java
+++ b/router/java/src/net/i2p/router/transport/TransportImpl.java
@@ -30,9 +30,9 @@ import java.util.concurrent.CopyOnWriteArrayList;
 
 import net.i2p.data.DataHelper;
 import net.i2p.data.Hash;
-import net.i2p.data.RouterAddress;
-import net.i2p.data.RouterIdentity;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterAddress;
+import net.i2p.data.router.RouterIdentity;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.data.i2np.I2NPMessage;
 import net.i2p.router.CommSystemFacade;
 import net.i2p.router.Job;
diff --git a/router/java/src/net/i2p/router/transport/TransportManager.java b/router/java/src/net/i2p/router/transport/TransportManager.java
index d9e0c37985..75e843707c 100644
--- a/router/java/src/net/i2p/router/transport/TransportManager.java
+++ b/router/java/src/net/i2p/router/transport/TransportManager.java
@@ -22,8 +22,8 @@ import java.util.Vector;
 import java.util.concurrent.ConcurrentHashMap;
 
 import net.i2p.data.Hash;
-import net.i2p.data.RouterAddress;
-import net.i2p.data.RouterIdentity;
+import net.i2p.data.router.RouterAddress;
+import net.i2p.data.router.RouterIdentity;
 import net.i2p.data.i2np.I2NPMessage;
 import net.i2p.router.CommSystemFacade;
 import net.i2p.router.OutNetMessage;
@@ -59,6 +59,7 @@ public class TransportManager implements TransportEventListener {
         _context = context;
         _log = _context.logManager().getLog(TransportManager.class);
         _context.statManager().createRateStat("transport.banlistOnUnreachable", "Add a peer to the banlist since none of the transports can reach them", "Transport", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
+        _context.statManager().createRateStat("transport.banlistOnUsupportedSigType", "Add a peer to the banlist since signature type is unsupported", "Transport", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
         _context.statManager().createRateStat("transport.noBidsYetNotAllUnreachable", "Add a peer to the banlist since none of the transports can reach them", "Transport", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
         _context.statManager().createRateStat("transport.bidFailBanlisted", "Could not attempt to bid on message, as they were banlisted", "Transport", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
         _context.statManager().createRateStat("transport.bidFailSelf", "Could not attempt to bid on message, as it targeted ourselves", "Transport", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
@@ -499,8 +500,11 @@ public class TransportManager implements TransportEventListener {
             }
         }
         if (unreachableTransports >= _transports.size()) {
-            // Don't banlist if we aren't talking to anybody, as we may have a network connection issue
-            if (unreachableTransports >= _transports.size() && countActivePeers() > 0) {
+            if (msg.getTarget().getIdentity().getSigningPublicKey().getType() == null) {
+                _context.statManager().addRateData("transport.banlistOnUnsupportedSigType", 1);
+                _context.banlist().banlistRouterForever(peer, _x("Unsupported signature type"));
+            } else if (unreachableTransports >= _transports.size() && countActivePeers() > 0) {
+                // Don't banlist if we aren't talking to anybody, as we may have a network connection issue
                 _context.statManager().addRateData("transport.banlistOnUnreachable", msg.getLifetime(), msg.getLifetime());
                 _context.banlist().banlistRouter(peer, _x("Unreachable on any transport"));
             }
diff --git a/router/java/src/net/i2p/router/transport/TransportUtil.java b/router/java/src/net/i2p/router/transport/TransportUtil.java
index 1682abd91c..648119f91d 100644
--- a/router/java/src/net/i2p/router/transport/TransportUtil.java
+++ b/router/java/src/net/i2p/router/transport/TransportUtil.java
@@ -13,7 +13,7 @@ import java.net.UnknownHostException;
 import java.util.HashMap;
 import java.util.Map;
 
-import net.i2p.data.RouterAddress;
+import net.i2p.data.router.RouterAddress;
 import net.i2p.router.RouterContext;
 
 /**
diff --git a/router/java/src/net/i2p/router/transport/ntcp/EstablishState.java b/router/java/src/net/i2p/router/transport/ntcp/EstablishState.java
index bc6e37bc4d..0e2ea066dd 100644
--- a/router/java/src/net/i2p/router/transport/ntcp/EstablishState.java
+++ b/router/java/src/net/i2p/router/transport/ntcp/EstablishState.java
@@ -1,5 +1,6 @@
 package net.i2p.router.transport.ntcp;
 
+import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.net.InetAddress;
@@ -7,11 +8,12 @@ import java.net.UnknownHostException;
 import java.nio.ByteBuffer;
 
 import net.i2p.I2PAppContext;
+import net.i2p.crypto.SigType;
 import net.i2p.data.Base64;
 import net.i2p.data.DataFormatException;
 import net.i2p.data.DataHelper;
 import net.i2p.data.Hash;
-import net.i2p.data.RouterIdentity;
+import net.i2p.data.router.RouterIdentity;
 import net.i2p.data.Signature;
 import net.i2p.router.Router;
 import net.i2p.router.RouterContext;
@@ -70,6 +72,7 @@ class EstablishState {
     private final byte _X[];
     private final byte _hX_xor_bobIdentHash[];
     private int _aliceIdentSize;
+    private RouterIdentity _aliceIdent;
     /** contains the decrypted aliceIndexSize + aliceIdent + tsA + padding + aliceSig */
     private ByteArrayOutputStream _sz_aliceIdent_tsA_padding_aliceSig;
     /** how long we expect _sz_aliceIdent_tsA_padding_aliceSig to be when its full */
@@ -112,6 +115,9 @@ class EstablishState {
     private boolean _confirmWritten;
     private boolean _failedBySkew;
     
+    private static final int MIN_RI_SIZE = 387;
+    private static final int MAX_RI_SIZE = 2048;
+
     private EstablishState() {
         _context = null;
         _log = null;
@@ -156,7 +162,8 @@ class EstablishState {
      */
     public void receive(ByteBuffer src) {
         if (_corrupt || _verified)
-            throw new IllegalStateException(prefix() + "received after completion [corrupt?" + _corrupt + " verified? " + _verified + "] on " + _con);
+            throw new IllegalStateException(prefix() + "received after completion [corrupt?" +
+                                            _corrupt + " verified? " + _verified + "] on " + _con);
         if (!src.hasRemaining())
             return; // nothing to receive
 
@@ -185,7 +192,8 @@ class EstablishState {
      */
     private void receiveInbound(ByteBuffer src) {
         if (_log.shouldLog(Log.DEBUG))
-            _log.debug(prefix()+"Receiving inbound: prev received=" + _received + " src.remaining=" + src.remaining());
+            _log.debug(prefix() + "Receiving inbound: prev received=" + _received +
+                       " src.remaining=" + src.remaining());
         while (_received < _X.length && src.hasRemaining()) {
             byte c = src.get();
             _X[_received++] = c;
@@ -269,7 +277,8 @@ class EstablishState {
                     }
                     SimpleByteCache.release(hxy);
                     _e_hXY_tsB = new byte[toEncrypt.length];
-                    _context.aes().encrypt(toEncrypt, 0, _e_hXY_tsB, 0, _dh.getSessionKey(), _Y, _Y.length-16, toEncrypt.length);
+                    _context.aes().encrypt(toEncrypt, 0, _e_hXY_tsB, 0, _dh.getSessionKey(),
+                                           _Y, _Y.length-16, toEncrypt.length);
                     if (_log.shouldLog(Log.DEBUG))
                         _log.debug(prefix()+"encrypted H(X+Y)+tsB+padding: " + Base64.encode(_e_hXY_tsB));
                     byte write[] = new byte[_Y.length + _e_hXY_tsB.length];
@@ -286,7 +295,7 @@ class EstablishState {
                 }
             }
 
-            // ok, we are onto the encrypted area
+            // ok, we are onto the encrypted area, i.e. Message #3
             while (src.hasRemaining() && !_corrupt) {
                 //if (_log.shouldLog(Log.DEBUG))
                 //    _log.debug(prefix()+"Encrypted bytes available (" + src.hasRemaining() + ")");
@@ -295,7 +304,8 @@ class EstablishState {
                     _received++;
                 }
                 if (_curEncryptedOffset >= _curEncrypted.length) {
-                    _context.aes().decrypt(_curEncrypted, 0, _curDecrypted, 0, _dh.getSessionKey(), _prevEncrypted, 0, _curEncrypted.length);
+                    _context.aes().decrypt(_curEncrypted, 0, _curDecrypted, 0, _dh.getSessionKey(),
+                                           _prevEncrypted, 0, _curEncrypted.length);
                     //if (_log.shouldLog(Log.DEBUG))
                     //    _log.debug(prefix()+"full block read and decrypted: " + Base64.encode(_curDecrypted));
 
@@ -305,31 +315,59 @@ class EstablishState {
                     _curEncryptedOffset = 0;
 
                     if (_aliceIdentSize <= 0) { // we are on the first decrypted block
-                        _aliceIdentSize = (int)DataHelper.fromLong(_curDecrypted, 0, 2);
-                        _sz_aliceIdent_tsA_padding_aliceSigSize = 2 + _aliceIdentSize + 4 + Signature.SIGNATURE_BYTES;
+                        int sz = (int)DataHelper.fromLong(_curDecrypted, 0, 2);
+                        if (sz < MIN_RI_SIZE || sz > MAX_RI_SIZE) {
+                            _context.statManager().addRateData("ntcp.invalidInboundSize", sz);
+                            fail("size is invalid", new Exception("size is " + sz));
+                            return;
+                        }
+                        _aliceIdentSize  = sz;
+
+                        // We must defer the calculations for total size of the message until
+                        //  we get the full alice ident so
+                        // we can determine how long the signature is.
+                        // See below
+
+                    }
+                    try {
+                        _sz_aliceIdent_tsA_padding_aliceSig.write(_curDecrypted);
+                    } catch (IOException ioe) {
+                        if (_log.shouldLog(Log.ERROR)) _log.error(prefix()+"Error writing to the baos?", ioe);
+                    }
+                    //if (_log.shouldLog(Log.DEBUG))
+                    //    _log.debug(prefix()+"subsequent block decrypted (" + _sz_aliceIdent_tsA_padding_aliceSig.size() + ")");
+
+                    if (_aliceIdent == null &&
+                        _sz_aliceIdent_tsA_padding_aliceSig.size() >= 2 + _aliceIdentSize) {
+                        // we have enough to get Alice's RI and determine the sig+padding length
+                        readAliceRouterIdentity();
+                        if (_aliceIdent == null) {
+                            // readAliceRouterIdentity already called fail
+                            return;
+                        }
+                        SigType type = _aliceIdent.getSigningPublicKey().getType();
+                        if (type == null) {
+                            fail("Unsupported sig type");
+                            return;
+                        }
+                        // handle variable signature size
+                        _sz_aliceIdent_tsA_padding_aliceSigSize = 2 + _aliceIdentSize + 4 + type.getSigLen();
                         int rem = (_sz_aliceIdent_tsA_padding_aliceSigSize % 16);
                         int padding = 0;
                         if (rem > 0)
                             padding = 16-rem;
                         _sz_aliceIdent_tsA_padding_aliceSigSize += padding;
-                        try {
-                            _sz_aliceIdent_tsA_padding_aliceSig.write(_curDecrypted);
-                        } catch (IOException ioe) {
-                            if (_log.shouldLog(Log.ERROR)) _log.error(prefix()+"Error writing to the baos?", ioe);
-                        }
                         if (_log.shouldLog(Log.DEBUG))
-                            _log.debug(prefix()+"alice ident size decrypted as " + _aliceIdentSize + ", making the padding at " + padding + " and total size at " + _sz_aliceIdent_tsA_padding_aliceSigSize);
-                    } else {
-                        // subsequent block...
-                        try {
-                            _sz_aliceIdent_tsA_padding_aliceSig.write(_curDecrypted);
-                        } catch (IOException ioe) {
-                            if (_log.shouldLog(Log.ERROR)) _log.error(prefix()+"Error writing to the baos?", ioe);
-                        }
-                        //if (_log.shouldLog(Log.DEBUG))
-                        //    _log.debug(prefix()+"subsequent block decrypted (" + _sz_aliceIdent_tsA_padding_aliceSig.size() + ")");
+                            _log.debug(prefix() + "alice ident size decrypted as " + _aliceIdentSize +
+                                       ", making the padding at " + padding + " and total size at " +
+                                       _sz_aliceIdent_tsA_padding_aliceSigSize);
+                    }
+
+                    if (_aliceIdent != null &&
+                        _sz_aliceIdent_tsA_padding_aliceSig.size() >= _sz_aliceIdent_tsA_padding_aliceSigSize) {
+                        // we have the remainder of Message #3, i.e. the padding+signature
+                        // Time to verify.
 
-                        if (_sz_aliceIdent_tsA_padding_aliceSig.size() >= _sz_aliceIdent_tsA_padding_aliceSigSize) {
                             verifyInbound();
                             if (!_corrupt && _verified && src.hasRemaining())
                                 prepareExtra(src);
@@ -339,13 +377,13 @@ class EstablishState {
                                            + " corrupt=" + _corrupt
                                            + " verified=" + _verified + " extra=" + (_extra != null ? _extra.length : 0) + ")");
                             return;
-                        }
                     }
                 } else {
                     // no more bytes available in the buffer, and only a partial
                     // block was read, so we can't decrypt it.
                     if (_log.shouldLog(Log.DEBUG))
-                        _log.debug(prefix()+"end of available data with only a partial block read (" + _curEncryptedOffset + ", " + _received + ")");
+                        _log.debug(prefix() + "end of available data with only a partial block read (" +
+                                   _curEncryptedOffset + ", " + _received + ")");
                 }
             }
             if (_log.shouldLog(Log.DEBUG))
@@ -458,7 +496,8 @@ class EstablishState {
                 //}
 
                 byte ident[] = _context.router().getRouterInfo().getIdentity().toByteArray();
-                int min = 2+ident.length+4+Signature.SIGNATURE_BYTES;
+                // handle variable signature size
+                int min = 2 + ident.length + 4 + sig.length();
                 int rem = min % 16;
                 int padding = 0;
                 if (rem > 0)
@@ -469,10 +508,11 @@ class EstablishState {
                 DataHelper.toLong(preEncrypt, 2+ident.length, 4, _tsA);
                 if (padding > 0)
                     _context.random().nextBytes(preEncrypt, 2 + ident.length + 4, padding);
-                System.arraycopy(sig.getData(), 0, preEncrypt, 2+ident.length+4+padding, Signature.SIGNATURE_BYTES);
+                System.arraycopy(sig.getData(), 0, preEncrypt, 2+ident.length+4+padding, sig.length());
 
                 _prevEncrypted = new byte[preEncrypt.length];
-                _context.aes().encrypt(preEncrypt, 0, _prevEncrypted, 0, _dh.getSessionKey(), _hX_xor_bobIdentHash, _hX_xor_bobIdentHash.length-16, preEncrypt.length);
+                _context.aes().encrypt(preEncrypt, 0, _prevEncrypted, 0, _dh.getSessionKey(),
+                                       _hX_xor_bobIdentHash, _hX_xor_bobIdentHash.length-16, preEncrypt.length);
 
                 //if (_log.shouldLog(Log.DEBUG)) {
                     //_log.debug(prefix() + "unencrypted response to Bob: " + Base64.encode(preEncrypt));
@@ -488,13 +528,23 @@ class EstablishState {
             // recv E(S(X+Y+Alice.identHash+tsA+tsB)+padding, sk, prev)
             int off = 0;
             if (_e_bobSig == null) {
-                _e_bobSig = new byte[48];
+                // handle variable signature size
+                int siglen = _con.getRemotePeer().getSigningPublicKey().getType().getSigLen();
+                int rem = siglen % 16;
+                int padding;
+                if (rem > 0)
+                    padding = 16 - rem;
+                else
+                    padding = 0;
+                _e_bobSig = new byte[siglen + padding];
                 if (_log.shouldLog(Log.DEBUG))
-                    _log.debug(prefix() + "receiving E(S(X+Y+Alice.identHash+tsA+tsB)+padding, sk, prev) (remaining? " + src.hasRemaining() + ")");
+                    _log.debug(prefix() + "receiving E(S(X+Y+Alice.identHash+tsA+tsB)+padding, sk, prev) (remaining? " +
+                               src.hasRemaining() + ")");
             } else {
                 off = _received - _Y.length - _e_hXY_tsB.length;
                 if (_log.shouldLog(Log.DEBUG))
-                    _log.debug(prefix() + "continuing to receive E(S(X+Y+Alice.identHash+tsA+tsB)+padding, sk, prev) (remaining? " + src.hasRemaining() + " off=" + off + " recv=" + _received + ")");
+                    _log.debug(prefix() + "continuing to receive E(S(X+Y+Alice.identHash+tsA+tsB)+padding, sk, prev) (remaining? " +
+                               src.hasRemaining() + " off=" + off + " recv=" + _received + ")");
             }
             while (src.hasRemaining() && off < _e_bobSig.length) {
                 if (_log.shouldLog(Log.DEBUG)) _log.debug(prefix()+"recv bobSig received=" + _received);
@@ -505,11 +555,15 @@ class EstablishState {
                     //if (_log.shouldLog(Log.DEBUG))
                     //    _log.debug(prefix() + "received E(S(X+Y+Alice.identHash+tsA+tsB)+padding, sk, prev): " + Base64.encode(_e_bobSig));
                     byte bobSig[] = new byte[_e_bobSig.length];
-                    _context.aes().decrypt(_e_bobSig, 0, bobSig, 0, _dh.getSessionKey(), _e_hXY_tsB, _e_hXY_tsB.length-16, _e_bobSig.length);
+                    _context.aes().decrypt(_e_bobSig, 0, bobSig, 0, _dh.getSessionKey(),
+                                           _e_hXY_tsB, _e_hXY_tsB.length-16, _e_bobSig.length);
                     // ignore the padding
-                    byte bobSigData[] = new byte[Signature.SIGNATURE_BYTES];
-                    System.arraycopy(bobSig, 0, bobSigData, 0, Signature.SIGNATURE_BYTES);
-                    Signature sig = new Signature(bobSigData);
+                    // handle variable signature size
+                    SigType type = _con.getRemotePeer().getSigningPublicKey().getType();
+                    int siglen = type.getSigLen();
+                    byte bobSigData[] = new byte[siglen];
+                    System.arraycopy(bobSig, 0, bobSigData, 0, siglen);
+                    Signature sig = new Signature(type, bobSigData);
 
                     byte toVerify[] = new byte[_X.length+_Y.length+Hash.HASH_LENGTH+4+4];
                     int voff = 0;
@@ -568,9 +622,60 @@ class EstablishState {
         }
     }
 
+    /**
+     * We are Bob. We have received enough of message #3 from Alice
+     * to get Alice's RouterIdentity.
+     *
+     * _aliceIdentSize must be set.
+     * _sz_aliceIdent_tsA_padding_aliceSig must contain at least 2 + _aliceIdentSize bytes.
+     *
+     * Sets _aliceIdent so that we
+     * may determine the signature and padding sizes.
+     *
+     * After all of message #3 is received including the signature and
+     * padding, verifyIdentity() must be called.
+     *
+     * @since 0.9.16 pulled out of verifyInbound()
+     */
+    private void readAliceRouterIdentity() {
+        if (_corrupt) return;
+        byte b[] = _sz_aliceIdent_tsA_padding_aliceSig.toByteArray();
+        //if (_log.shouldLog(Log.DEBUG))
+        //    _log.debug(prefix()+"decrypted sz(etc) data: " + Base64.encode(b));
+
+        try {
+            int sz = _aliceIdentSize;
+            if (sz < MIN_RI_SIZE || sz > MAX_RI_SIZE ||
+                sz > b.length-2) {
+                _context.statManager().addRateData("ntcp.invalidInboundSize", sz);
+                fail("size is invalid", new Exception("size is " + sz));
+                return;
+            }
+            RouterIdentity alice = new RouterIdentity();
+            ByteArrayInputStream bais = new ByteArrayInputStream(b, 2, sz);
+            alice.readBytes(bais);
+            _aliceIdent = alice;
+        } catch (IOException ioe) {
+            _context.statManager().addRateData("ntcp.invalidInboundIOE", 1);
+            fail("Error verifying peer", ioe);
+        } catch (DataFormatException dfe) {
+            _context.statManager().addRateData("ntcp.invalidInboundDFE", 1);
+            fail("Error verifying peer", dfe);
+        }
+    }
+
+
     /**
      * We are Bob. Verify message #3 from Alice, then send message #4 to Alice.
      *
+     * _aliceIdentSize and _aliceIdent must be set.
+     * _sz_aliceIdent_tsA_padding_aliceSig must contain at least
+     *  (2 + _aliceIdentSize + 4 + padding + sig) bytes.
+     *
+     * Sets _aliceIdent so that we
+     *
+     * readAliceRouterIdentity() must have been called previously
+     *
      * Make sure the signatures are correct, and if they are, update the
      * NIOConnection with the session key / peer ident / clock skew / iv.
      * The NIOConnection itself is responsible for registering with the
@@ -579,22 +684,9 @@ class EstablishState {
     private void verifyInbound() {
         if (_corrupt) return;
         byte b[] = _sz_aliceIdent_tsA_padding_aliceSig.toByteArray();
-        //if (_log.shouldLog(Log.DEBUG))
-        //    _log.debug(prefix()+"decrypted sz(etc) data: " + Base64.encode(b));
-
         try {
-            RouterIdentity alice = new RouterIdentity();
-            int sz = (int)DataHelper.fromLong(b, 0, 2); // TO-DO: Hey zzz... Throws an NPE for me... see below, for my "quick fix", need to find out the real reason
-            if ( (sz <= 0) || (sz > b.length-2-4-Signature.SIGNATURE_BYTES) ) {
-                _context.statManager().addRateData("ntcp.invalidInboundSize", sz);
-                fail("size is invalid", new Exception("size is " + sz));
-                return;
-            }
-            byte aliceData[] = new byte[sz];
-            System.arraycopy(b, 2, aliceData, 0, sz);
-            alice.fromByteArray(aliceData);
+            int sz = _aliceIdentSize;
             long tsA = DataHelper.fromLong(b, 2+sz, 4);
-
             ByteArrayOutputStream baos = new ByteArrayOutputStream(768);
             baos.write(_X);
             baos.write(_Y);
@@ -609,26 +701,32 @@ class EstablishState {
                 //_log.debug(prefix()+"check pad " + Base64.encode(b, 2+sz+4, 12));
             }
 
-            byte s[] = new byte[Signature.SIGNATURE_BYTES];
+            // handle variable signature size
+            SigType type = _aliceIdent.getSigningPublicKey().getType();
+            if (type == null) {
+                fail("unsupported sig type");
+                return;
+            }
+            byte s[] = new byte[type.getSigLen()];
             System.arraycopy(b, b.length-s.length, s, 0, s.length);
-            Signature sig = new Signature(s);
-            _verified = _context.dsa().verifySignature(sig, toVerify, alice.getSigningPublicKey());
+            Signature sig = new Signature(type, s);
+            _verified = _context.dsa().verifySignature(sig, toVerify, _aliceIdent.getSigningPublicKey());
             if (_verified) {
                 // get inet-addr
                 InetAddress addr = this._con.getChannel().socket().getInetAddress();
                 byte[] ip = (addr == null) ? null : addr.getAddress();
-                if (_context.banlist().isBanlistedForever(alice.calculateHash())) {
+                if (_context.banlist().isBanlistedForever(_aliceIdent.calculateHash())) {
                     if (_log.shouldLog(Log.WARN))
-                        _log.warn("Dropping inbound connection from permanently banlisted peer: " + alice.calculateHash().toBase64());
+                        _log.warn("Dropping inbound connection from permanently banlisted peer: " + _aliceIdent.calculateHash());
                     // So next time we will not accept the con from this IP,
                     // rather than doing the whole handshake
                     if(ip != null)
                        _context.blocklist().add(ip);
-                    fail("Peer is banlisted forever: " + alice.calculateHash().toBase64());
+                    fail("Peer is banlisted forever: " + _aliceIdent.calculateHash());
                     return;
                 }
                 if(ip != null)
-                   _transport.setIP(alice.calculateHash(), ip);
+                   _transport.setIP(_aliceIdent.calculateHash(), ip);
                 if (_log.shouldLog(Log.DEBUG))
                     _log.debug(prefix() + "verification successful for " + _con);
 
@@ -642,10 +740,10 @@ class EstablishState {
                         _log.logAlways(Log.WARN, "NTP failure, NTCP adjusting clock by " + DataHelper.formatDuration(diff));
                 } else if (diff >= Router.CLOCK_FUDGE_FACTOR) {
                     _context.statManager().addRateData("ntcp.invalidInboundSkew", diff);
-                    _transport.markReachable(alice.calculateHash(), true);
+                    _transport.markReachable(_aliceIdent.calculateHash(), true);
                     // Only banlist if we know what time it is
                     _context.banlist().banlistRouter(DataHelper.formatDuration(diff),
-                                                       alice.calculateHash(),
+                                                       _aliceIdent.calculateHash(),
                                                        _x("Excessive clock skew: {0}"));
                     _transport.setLastBadSkew(tsA- _tsB);
                     fail("Clocks too skewed (" + diff + " ms)", null, true);
@@ -654,27 +752,22 @@ class EstablishState {
                     _log.debug(prefix()+"Clock skew: " + diff + " ms");
                 }
 
-                sendInboundConfirm(alice, tsA);
-                _con.setRemotePeer(alice);
+                sendInboundConfirm(_aliceIdent, tsA);
+                _con.setRemotePeer(_aliceIdent);
                 if (_log.shouldLog(Log.DEBUG))
                     _log.debug(prefix()+"e_bobSig is " + _e_bobSig.length + " bytes long");
                 byte iv[] = new byte[16];
                 System.arraycopy(_e_bobSig, _e_bobSig.length-16, iv, 0, 16);
                 _con.finishInboundEstablishment(_dh.getSessionKey(), (tsA-_tsB), iv, _prevEncrypted); // skew in seconds
                 if (_log.shouldLog(Log.INFO))
-                    _log.info(prefix()+"Verified remote peer as " + alice.calculateHash().toBase64());
+                    _log.info(prefix()+"Verified remote peer as " + _aliceIdent.calculateHash());
             } else {
                 _context.statManager().addRateData("ntcp.invalidInboundSignature", 1);
-                fail("Peer verification failed - spoof of " + alice.calculateHash().toBase64() + "?");
+                fail("Peer verification failed - spoof of " + _aliceIdent.calculateHash() + "?");
             }
         } catch (IOException ioe) {
             _context.statManager().addRateData("ntcp.invalidInboundIOE", 1);
             fail("Error verifying peer", ioe);
-        } catch (DataFormatException dfe) {
-            _context.statManager().addRateData("ntcp.invalidInboundDFE", 1);
-            fail("Error verifying peer", dfe);
-        } catch(NullPointerException npe) {
-            fail("Error verifying peer", npe); // TO-DO: zzz This is that quick-fix. -- Sponge
         }
     }
 
@@ -692,10 +785,19 @@ class EstablishState {
         DataHelper.toLong(toSign, off, 4, tsA); off += 4;
         DataHelper.toLong(toSign, off, 4, _tsB); off += 4;
 
+        // handle variable signature size
         Signature sig = _context.dsa().sign(toSign, _context.keyManager().getSigningPrivateKey());
-        byte preSig[] = new byte[Signature.SIGNATURE_BYTES+8];
-        System.arraycopy(sig.getData(), 0, preSig, 0, Signature.SIGNATURE_BYTES);
-        _context.random().nextBytes(preSig, Signature.SIGNATURE_BYTES, 8);
+        int siglen = sig.length();
+        int rem = siglen % 16;
+        int padding;
+        if (rem > 0)
+            padding = 16 - rem;
+        else
+            padding = 0;
+        byte preSig[] = new byte[siglen + padding];
+        System.arraycopy(sig.getData(), 0, preSig, 0, siglen);
+        if (padding > 0)
+            _context.random().nextBytes(preSig, siglen, padding);
         _e_bobSig = new byte[preSig.length];
         _context.aes().encrypt(preSig, 0, _e_bobSig, 0, _dh.getSessionKey(), _e_hXY_tsB, _e_hXY_tsB.length-16, _e_bobSig.length);
 
diff --git a/router/java/src/net/i2p/router/transport/ntcp/EventPumper.java b/router/java/src/net/i2p/router/transport/ntcp/EventPumper.java
index d4fd7521ac..607234ae70 100644
--- a/router/java/src/net/i2p/router/transport/ntcp/EventPumper.java
+++ b/router/java/src/net/i2p/router/transport/ntcp/EventPumper.java
@@ -20,8 +20,8 @@ import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.LinkedBlockingQueue;
 
 import net.i2p.I2PAppContext;
-import net.i2p.data.RouterAddress;
-import net.i2p.data.RouterIdentity;
+import net.i2p.data.router.RouterAddress;
+import net.i2p.data.router.RouterIdentity;
 import net.i2p.router.CommSystemFacade;
 import net.i2p.router.RouterContext;
 import net.i2p.router.transport.FIFOBandwidthLimiter;
diff --git a/router/java/src/net/i2p/router/transport/ntcp/NTCPConnection.java b/router/java/src/net/i2p/router/transport/ntcp/NTCPConnection.java
index 2b2ae6a442..acd5570e38 100644
--- a/router/java/src/net/i2p/router/transport/ntcp/NTCPConnection.java
+++ b/router/java/src/net/i2p/router/transport/ntcp/NTCPConnection.java
@@ -17,9 +17,9 @@ import java.util.zip.Adler32;
 import net.i2p.data.Base64;
 import net.i2p.data.ByteArray;
 import net.i2p.data.DataHelper;
-import net.i2p.data.RouterAddress;
-import net.i2p.data.RouterIdentity;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterAddress;
+import net.i2p.data.router.RouterIdentity;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.data.SessionKey;
 import net.i2p.data.i2np.DatabaseStoreMessage;
 import net.i2p.data.i2np.I2NPMessage;
diff --git a/router/java/src/net/i2p/router/transport/ntcp/NTCPTransport.java b/router/java/src/net/i2p/router/transport/ntcp/NTCPTransport.java
index 0ad8d88a94..c7e4703a4d 100644
--- a/router/java/src/net/i2p/router/transport/ntcp/NTCPTransport.java
+++ b/router/java/src/net/i2p/router/transport/ntcp/NTCPTransport.java
@@ -25,9 +25,9 @@ import java.util.concurrent.ConcurrentHashMap;
 
 import net.i2p.data.DataHelper;
 import net.i2p.data.Hash;
-import net.i2p.data.RouterAddress;
-import net.i2p.data.RouterIdentity;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterAddress;
+import net.i2p.data.router.RouterIdentity;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.data.i2np.DatabaseStoreMessage;
 import net.i2p.data.i2np.I2NPMessage;
 import net.i2p.router.CommSystemFacade;
@@ -362,6 +362,12 @@ public class NTCPTransport extends TransportImpl {
             return null;
         }
 
+        // Check for supported sig type
+        if (toAddress.getIdentity().getSigningPublicKey().getType() == null) {
+            markUnreachable(peer);
+            return null;
+        }
+
         if (!allowConnection()) {
             if (_log.shouldLog(Log.WARN))
                 _log.warn("no bid when trying to send to " + peer + ", max connection limit reached");
diff --git a/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java b/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java
index 267c4fc2a5..92696ff9ce 100644
--- a/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java
+++ b/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java
@@ -10,9 +10,9 @@ import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 
 import net.i2p.data.Hash;
-import net.i2p.data.RouterAddress;
-import net.i2p.data.RouterIdentity;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterAddress;
+import net.i2p.data.router.RouterIdentity;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.data.SessionKey;
 import net.i2p.data.i2np.DatabaseStoreMessage;
 import net.i2p.data.i2np.DeliveryStatusMessage;
diff --git a/router/java/src/net/i2p/router/transport/udp/InboundEstablishState.java b/router/java/src/net/i2p/router/transport/udp/InboundEstablishState.java
index abdd59e15d..6d1f177554 100644
--- a/router/java/src/net/i2p/router/transport/udp/InboundEstablishState.java
+++ b/router/java/src/net/i2p/router/transport/udp/InboundEstablishState.java
@@ -5,11 +5,12 @@ import java.io.IOException;
 import java.util.Queue;
 import java.util.concurrent.LinkedBlockingQueue;
 
+import net.i2p.crypto.SigType;
 import net.i2p.data.Base64;
 import net.i2p.data.ByteArray;
 import net.i2p.data.DataFormatException;
 import net.i2p.data.DataHelper;
-import net.i2p.data.RouterIdentity;
+import net.i2p.data.router.RouterIdentity;
 import net.i2p.data.SessionKey;
 import net.i2p.data.Signature;
 import net.i2p.router.OutNetMessage;
@@ -47,6 +48,9 @@ class InboundEstablishState {
     private long _receivedSignedOnTime;
     private byte _receivedSignature[];
     private boolean _verificationAttempted;
+    // sig not verified
+    private RouterIdentity _receivedUnconfirmedIdentity;
+    // identical to uncomfirmed, but sig now verified
     private RouterIdentity _receivedConfirmedIdentity;
     // general status 
     private final long _establishBegin;
@@ -295,9 +299,28 @@ class InboundEstablishState {
         
         if (cur == _receivedIdentity.length-1) {
             _receivedSignedOnTime = conf.readFinalFragmentSignedOnTime();
-            if (_receivedSignature == null)
-                _receivedSignature = new byte[Signature.SIGNATURE_BYTES];
-            conf.readFinalSignature(_receivedSignature, 0);
+            // TODO verify time to prevent replay attacks
+            buildIdentity();
+            if (_receivedUnconfirmedIdentity != null) {
+                SigType type = _receivedUnconfirmedIdentity.getSigningPublicKey().getType();
+                if (type != null) {
+                    int sigLen = type.getSigLen();
+                    if (_receivedSignature == null)
+                        _receivedSignature = new byte[sigLen];
+                    conf.readFinalSignature(_receivedSignature, 0, sigLen);
+                } else {
+                    if (_log.shouldLog(Log.WARN))
+                        _log.warn("Unsupported sig type from: " + toString());
+                    // _x() in UDPTransport
+                    _context.banlist().banlistRouterForever(_receivedUnconfirmedIdentity.calculateHash(),
+                                                            "Unsupported signature type");
+                    fail();
+                }
+            } else {
+                if (_log.shouldLog(Log.WARN))
+                    _log.warn("Bad ident from: " + toString());
+                fail();
+            }
         }
         
         if ( (_currentState == InboundState.IB_STATE_UNKNOWN) || 
@@ -318,9 +341,10 @@ class InboundEstablishState {
      */
     private boolean confirmedFullyReceived() {
         if (_receivedIdentity != null) {
-            for (int i = 0; i < _receivedIdentity.length; i++)
+            for (int i = 0; i < _receivedIdentity.length; i++) {
                 if (_receivedIdentity[i] == null)
                     return false;
+            }
             return true;
         } else {
             return false;
@@ -339,7 +363,51 @@ class InboundEstablishState {
         }
         return _receivedConfirmedIdentity;
     }
-    
+
+    /**
+     *  Construct Alice's RouterIdentity.
+     *  Must have received all fragments.
+     *  Sets _receivedUnconfirmedIdentity, unless invalid.
+     *
+     *  Caller must synch on this.
+     *
+     *  @since 0.9.16 was in verifyIdentity()
+     */
+    private void buildIdentity() {
+        if (_receivedUnconfirmedIdentity != null)
+            return;   // dup pkt?
+        int frags = _receivedIdentity.length;
+        byte[] ident;
+        if (frags > 1) {
+            int identSize = 0;
+            for (int i = 0; i < _receivedIdentity.length; i++)
+                identSize += _receivedIdentity[i].length;
+            ident = new byte[identSize];
+            int off = 0;
+            for (int i = 0; i < _receivedIdentity.length; i++) {
+                int len = _receivedIdentity[i].length;
+                System.arraycopy(_receivedIdentity[i], 0, ident, off, len);
+                off += len;
+            }
+        } else {
+            // no need to copy
+            ident = _receivedIdentity[0];
+        }
+        ByteArrayInputStream in = new ByteArrayInputStream(ident); 
+        RouterIdentity peer = new RouterIdentity();
+        try {
+            peer.readBytes(in);
+            _receivedUnconfirmedIdentity = peer;
+        } catch (DataFormatException dfe) {
+            if (_log.shouldLog(Log.WARN))
+                _log.warn("Improperly formatted yet fully received ident", dfe);
+        } catch (IOException ioe) {
+            if (_log.shouldLog(Log.WARN))
+                _log.warn("Improperly formatted yet fully received ident", ioe);
+        }
+    }
+            
+
     /**
      * Determine if Alice sent us a valid confirmation packet.  The 
      * identity signs: Alice's IP + Alice's port + Bob's IP + Bob's port
@@ -351,21 +419,11 @@ class InboundEstablishState {
      * Caller must synch on this.
      */
     private void verifyIdentity() {
-        int identSize = 0;
-        for (int i = 0; i < _receivedIdentity.length; i++)
-            identSize += _receivedIdentity[i].length;
-        byte ident[] = new byte[identSize];
-        int off = 0;
-        for (int i = 0; i < _receivedIdentity.length; i++) {
-            int len = _receivedIdentity[i].length;
-            System.arraycopy(_receivedIdentity[i], 0, ident, off, len);
-            off += len;
-        }
-        ByteArrayInputStream in = new ByteArrayInputStream(ident); 
-        RouterIdentity peer = new RouterIdentity();
-        try {
-            peer.readBytes(in);
-            
+            if (_receivedUnconfirmedIdentity == null)
+                return;   // either not yet recvd or bad ident
+            if (_receivedSignature == null)
+                return;   // either not yet recvd or bad sig
+
             byte signed[] = new byte[256+256 // X + Y
                                      + _aliceIP.length + 2
                                      + _bobIP.length + 2
@@ -373,7 +431,7 @@ class InboundEstablishState {
                                      + 4 // signed on time
                                      ];
 
-            off = 0;
+            int off = 0;
             System.arraycopy(_receivedX, 0, signed, off, _receivedX.length);
             off += _receivedX.length;
             getSentY();
@@ -391,22 +449,15 @@ class InboundEstablishState {
             off += 4;
             DataHelper.toLong(signed, off, 4, _receivedSignedOnTime);
             Signature sig = new Signature(_receivedSignature);
-            boolean ok = _context.dsa().verifySignature(sig, signed, peer.getSigningPublicKey());
+            boolean ok = _context.dsa().verifySignature(sig, signed, _receivedUnconfirmedIdentity.getSigningPublicKey());
             if (ok) {
                 // todo partial spoof detection - get peer.calculateHash(),
                 // lookup in netdb locally, if not equal, fail?
-                _receivedConfirmedIdentity = peer;
+                _receivedConfirmedIdentity = _receivedUnconfirmedIdentity;
             } else {
                 if (_log.shouldLog(Log.WARN))
-                    _log.warn("Signature failed from " + peer);
+                    _log.warn("Signature failed from " + _receivedUnconfirmedIdentity);
             }
-        } catch (DataFormatException dfe) {
-            if (_log.shouldLog(Log.WARN))
-                _log.warn("Improperly formatted yet fully received ident", dfe);
-        } catch (IOException ioe) {
-            if (_log.shouldLog(Log.WARN))
-                _log.warn("Improperly formatted yet fully received ident", ioe);
-        }
     }
     
     private void packetReceived() {
diff --git a/router/java/src/net/i2p/router/transport/udp/IntroductionManager.java b/router/java/src/net/i2p/router/transport/udp/IntroductionManager.java
index 37db458801..7e55cf1ce3 100644
--- a/router/java/src/net/i2p/router/transport/udp/IntroductionManager.java
+++ b/router/java/src/net/i2p/router/transport/udp/IntroductionManager.java
@@ -11,8 +11,8 @@ import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 
 import net.i2p.data.Base64;
-import net.i2p.data.RouterAddress;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterAddress;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.data.SessionKey;
 import net.i2p.router.RouterContext;
 import net.i2p.util.Addresses;
diff --git a/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState.java b/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState.java
index ba4375dbdb..ff94852559 100644
--- a/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState.java
+++ b/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState.java
@@ -3,10 +3,11 @@ package net.i2p.router.transport.udp;
 import java.util.Queue;
 import java.util.concurrent.LinkedBlockingQueue;
 
+import net.i2p.crypto.SigType;
 import net.i2p.data.Base64;
 import net.i2p.data.ByteArray;
 import net.i2p.data.DataHelper;
-import net.i2p.data.RouterIdentity;
+import net.i2p.data.router.RouterIdentity;
 import net.i2p.data.SessionKey;
 import net.i2p.data.Signature;
 import net.i2p.data.i2np.DatabaseStoreMessage;
@@ -41,6 +42,7 @@ class OutboundEstablishState {
     private SessionKey _sessionKey;
     private SessionKey _macKey;
     private Signature _receivedSignature;
+    // includes trailing padding to mod 16
     private byte[] _receivedEncryptedSignature;
     private byte[] _receivedIV;
     // SessionConfirmed messages
@@ -104,6 +106,7 @@ class OutboundEstablishState {
     /**
      *  @param claimedAddress an IP/port based RemoteHostId, or null if unknown
      *  @param remoteHostId non-null, == claimedAddress if direct, or a hash-based one if indirect
+     *  @param remotePeer must have supported sig type
      *  @param introKey Bob's introduction key, as published in the netdb
      *  @param addr non-null
      */
@@ -247,8 +250,20 @@ class OutboundEstablishState {
         _alicePort = reader.readPort();
         _receivedRelayTag = reader.readRelayTag();
         _receivedSignedOnTime = reader.readSignedOnTime();
-        _receivedEncryptedSignature = new byte[Signature.SIGNATURE_BYTES + 8];
-        reader.readEncryptedSignature(_receivedEncryptedSignature, 0);
+        // handle variable signature size
+        SigType type = _remotePeer.getSigningPublicKey().getType();
+        if (type == null) {
+            // shouldn't happen, we only connect to supported peers
+            fail();
+            packetReceived();
+            return;
+        }
+        int sigLen = type.getSigLen();
+        int mod = sigLen % 16;
+        int pad = (mod == 0) ? 0 : (16 - mod);
+        int esigLen = sigLen + pad;
+        _receivedEncryptedSignature = new byte[esigLen];
+        reader.readEncryptedSignature(_receivedEncryptedSignature, 0, esigLen);
         _receivedIV = new byte[UDPPacket.IV_SIZE];
         reader.readIV(_receivedIV, 0);
         
@@ -353,7 +368,9 @@ class OutboundEstablishState {
      * decrypt the signature (and subsequent pad bytes) with the 
      * additional layer of encryption using the negotiated key along side
      * the packet's IV
+     *
      *  Caller must synch on this.
+     *  Only call this once! Decrypts in-place.
      */
     private void decryptSignature() {
         if (_receivedEncryptedSignature == null) throw new NullPointerException("encrypted signature is null! this=" + this.toString());
@@ -361,11 +378,20 @@ class OutboundEstablishState {
         if (_receivedIV == null) throw new NullPointerException("IV is null!");
         _context.aes().decrypt(_receivedEncryptedSignature, 0, _receivedEncryptedSignature, 0, 
                                _sessionKey, _receivedIV, _receivedEncryptedSignature.length);
-        byte signatureBytes[] = new byte[Signature.SIGNATURE_BYTES];
-        System.arraycopy(_receivedEncryptedSignature, 0, signatureBytes, 0, Signature.SIGNATURE_BYTES);
-        _receivedSignature = new Signature(signatureBytes);
+        // handle variable signature size
+        SigType type = _remotePeer.getSigningPublicKey().getType();
+        // if type == null throws NPE
+        int sigLen = type.getSigLen();
+        int mod = sigLen % 16;
+        if (mod != 0) {
+            byte signatureBytes[] = new byte[sigLen];
+            System.arraycopy(_receivedEncryptedSignature, 0, signatureBytes, 0, sigLen);
+            _receivedSignature = new Signature(type, signatureBytes);
+        } else {
+            _receivedSignature = new Signature(type, _receivedEncryptedSignature);
+        }
         if (_log.shouldLog(Log.DEBUG))
-            _log.debug("Decrypted received signature: " + Base64.encode(signatureBytes));
+            _log.debug("Decrypted received signature: " + Base64.encode(_receivedSignature.getData()));
     }
 
     /**
diff --git a/router/java/src/net/i2p/router/transport/udp/OutboundMessageFragments.java b/router/java/src/net/i2p/router/transport/udp/OutboundMessageFragments.java
index 034f3e6054..ab91478f17 100644
--- a/router/java/src/net/i2p/router/transport/udp/OutboundMessageFragments.java
+++ b/router/java/src/net/i2p/router/transport/udp/OutboundMessageFragments.java
@@ -1,14 +1,19 @@
 package net.i2p.router.transport.udp;
 
+import java.io.Serializable;
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Set;
 
+import net.i2p.data.DataHelper;
 import net.i2p.data.Hash;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.router.OutNetMessage;
 import net.i2p.router.RouterContext;
+import net.i2p.router.transport.udp.PacketBuilder.Fragment;
 import net.i2p.util.ConcurrentHashSet;
 import net.i2p.util.Log;
 
@@ -74,6 +79,7 @@ class OutboundMessageFragments {
         _context.statManager().createRateStat("udp.sendVolleyTime", "Long it takes to send a full volley", "udp", UDPTransport.RATES);
         _context.statManager().createRateStat("udp.sendConfirmTime", "How long it takes to send a message and get the ACK", "udp", UDPTransport.RATES);
         _context.statManager().createRateStat("udp.sendConfirmFragments", "How many fragments are included in a fully ACKed message", "udp", UDPTransport.RATES);
+        _context.statManager().createRateStat("udp.sendFragmentsPerPacket", "How many fragments are sent in a data packet", "udp", UDPTransport.RATES);
         _context.statManager().createRateStat("udp.sendConfirmVolley", "How many times did fragments need to be sent before ACK", "udp", UDPTransport.RATES);
         _context.statManager().createRateStat("udp.sendFailed", "How many sends a failed message was pushed", "udp", UDPTransport.RATES);
         _context.statManager().createRateStat("udp.sendAggressiveFailed", "How many volleys was a packet sent before we gave up", "udp", UDPTransport.RATES);
@@ -81,7 +87,7 @@ class OutboundMessageFragments {
         _context.statManager().createRateStat("udp.outboundActivePeers", "How many peers we are actively sending to", "udp", UDPTransport.RATES);
         _context.statManager().createRateStat("udp.sendRejected", "What volley are we on when the peer was throttled", "udp", UDPTransport.RATES);
         _context.statManager().createRateStat("udp.partialACKReceived", "How many fragments were partially ACKed", "udp", UDPTransport.RATES);
-        _context.statManager().createRateStat("udp.sendSparse", "How many fragments were partially ACKed and hence not resent (time == message lifetime)", "udp", UDPTransport.RATES);
+        //_context.statManager().createRateStat("udp.sendSparse", "How many fragments were partially ACKed and hence not resent (time == message lifetime)", "udp", UDPTransport.RATES);
         _context.statManager().createRateStat("udp.sendPiggyback", "How many acks were piggybacked on a data packet (time == message lifetime)", "udp", UDPTransport.RATES);
         _context.statManager().createRateStat("udp.sendPiggybackPartial", "How many partial acks were piggybacked on a data packet (time == message lifetime)", "udp", UDPTransport.RATES);
         _context.statManager().createRequiredRateStat("udp.packetsRetransmitted", "Lifetime of packets during retransmission (ms)", "udp", UDPTransport.RATES);
@@ -236,19 +242,17 @@ class OutboundMessageFragments {
     /**
      * Fetch all the packets for a message volley, blocking until there is a
      * message which can be fully transmitted (or the transport is shut down).
-     * The returned array may be sparse, with null packets taking the place of
-     * already ACKed fragments.
      *
      * NOT thread-safe. Called by the PacketPusher thread only.
      *
      * @return null only on shutdown
      */
-    public UDPPacket[] getNextVolley() {
+    public List getNextVolley() {
         PeerState peer = null;
-        OutboundMessageState state = null;
+        List states = null;
         // Keep track of how many we've looked at, since we don't start the iterator at the beginning.
         int peersProcessed = 0;
-        while (_alive && (state == null) ) {
+        while (_alive && (states == null) ) {
             int nextSendDelay = Integer.MAX_VALUE;
             // no, not every time - O(n**2) - do just before waiting below
             //finishMessages();
@@ -275,8 +279,8 @@ class OutboundMessageFragments {
                             continue;
                         }
                         peersProcessed++;
-                        state = peer.allocateSend();
-                        if (state != null) {
+                        states = peer.allocateSend();
+                        if (states != null) {
                             // we have something to send and we will be returning it
                             break;
                         } else if (peersProcessed >= _activePeers.size()) {
@@ -292,13 +296,13 @@ class OutboundMessageFragments {
                         }
                     }
 
-                    if (peer != null && _log.shouldLog(Log.DEBUG))
-                        _log.debug("Done looping, next peer we are sending for: " +
-                                   peer.getRemotePeer());
+                    //if (peer != null && _log.shouldLog(Log.DEBUG))
+                    //    _log.debug("Done looping, next peer we are sending for: " +
+                    //               peer.getRemotePeer());
 
                     // if we've gone all the way through the loop, wait
                     // ... unless nextSendDelay says we have more ready now
-                    if (state == null && peersProcessed >= _activePeers.size() && nextSendDelay > 0) {
+                    if (states == null && peersProcessed >= _activePeers.size() && nextSendDelay > 0) {
                         _isWaiting = true;
                         peersProcessed = 0;
                         // why? we do this in the loop one at a time
@@ -328,9 +332,9 @@ class OutboundMessageFragments {
         } // while alive && state == null
 
         if (_log.shouldLog(Log.DEBUG))
-            _log.debug("Sending " + state);
+            _log.debug("Sending " + DataHelper.toString(states));
 
-        UDPPacket packets[] = preparePackets(state, peer);
+        List packets = preparePackets(states, peer);
 
       /****
         if ( (state != null) && (state.getMessage() != null) ) {
@@ -352,58 +356,108 @@ class OutboundMessageFragments {
     /**
      *  @return null if state or peer is null
      */
-    private UDPPacket[] preparePackets(OutboundMessageState state, PeerState peer) {
-        if ( (state != null) && (peer != null) ) {
-            int fragments = state.getFragmentCount();
-            if (fragments < 0)
-                return null;
+    private List preparePackets(List states, PeerState peer) {
+        if (states == null || peer == null)
+            return null;
 
-            // ok, simplest possible thing is to always tack on the bitfields if
-            List msgIds = peer.getCurrentFullACKs();
-            int newFullAckCount = msgIds.size();
-            msgIds.addAll(peer.getCurrentResendACKs());
-            List partialACKBitfields = new ArrayList();
-            peer.fetchPartialACKs(partialACKBitfields);
-            int piggybackedPartialACK = partialACKBitfields.size();
-            // getCurrentFullACKs() already makes a copy, do we need to copy again?
-            // YES because buildPacket() now removes them (maybe)
-            List remaining = new ArrayList(msgIds);
-            int sparseCount = 0;
-            UDPPacket rv[] = new UDPPacket[fragments]; //sparse
+        // ok, simplest possible thing is to always tack on the bitfields if
+        List msgIds = peer.getCurrentFullACKs();
+        int newFullAckCount = msgIds.size();
+        msgIds.addAll(peer.getCurrentResendACKs());
+        List partialACKBitfields = new ArrayList();
+        peer.fetchPartialACKs(partialACKBitfields);
+        int piggybackedPartialACK = partialACKBitfields.size();
+        // getCurrentFullACKs() already makes a copy, do we need to copy again?
+        // YES because buildPacket() now removes them (maybe)
+        List remaining = new ArrayList(msgIds);
+
+        // build the list of fragments to send
+        List toSend = new ArrayList(8);
+        for (OutboundMessageState state : states) {
+            int fragments = state.getFragmentCount();
+            int queued = 0;
             for (int i = 0; i < fragments; i++) {
                 if (state.needsSending(i)) {
-                    int before = remaining.size();
-                    try {
-                        rv[i] = _builder.buildPacket(state, i, peer, remaining, newFullAckCount, partialACKBitfields);
-                    } catch (ArrayIndexOutOfBoundsException aioobe) {
-                        _log.log(Log.CRIT, "Corrupt trying to build a packet - please tell jrandom: " +
-                                 partialACKBitfields + " / " + remaining + " / " + msgIds);
-                        sparseCount++;
-                        continue;
-                    }
-                    int after = remaining.size();
-                    newFullAckCount = Math.max(0, newFullAckCount - (before - after));
-                    if (rv[i] == null) {
-                        sparseCount++;
-                        continue;
-                    }
-                    rv[i].setFragmentCount(fragments);
-                    OutNetMessage msg = state.getMessage();
-                    if (msg != null)
-                        rv[i].setMessageType(msg.getMessageTypeId());
-                    else
-                        rv[i].setMessageType(-1);
-                } else {
-                    sparseCount++;
+                    toSend.add(new Fragment(state, i));
+                    queued++;
                 }
             }
-            if (sparseCount > 0)
-                remaining.clear();
+            // per-state stats
+            if (queued > 0 && state.getPushCount() > 1) {
+                peer.messageRetransmitted(queued);
+                // _packetsRetransmitted += toSend; // lifetime for the transport
+                _context.statManager().addRateData("udp.peerPacketsRetransmitted", peer.getPacketsRetransmitted(), peer.getPacketsTransmitted());
+                _context.statManager().addRateData("udp.packetsRetransmitted", state.getLifetime(), peer.getPacketsTransmitted());
+                if (_log.shouldLog(Log.INFO))
+                    _log.info("Retransmitting " + state + " to " + peer);
+                _context.statManager().addRateData("udp.sendVolleyTime", state.getLifetime(), queued);
+            }
+        }
+
+        if (toSend.isEmpty())
+            return null;
+
+        int fragmentsToSend = toSend.size();
+        // sort by size, biggest first
+        // don't bother unless more than one state (fragments are already sorted within a state)
+        if (fragmentsToSend > 1 && states.size() > 1)
+            Collections.sort(toSend, new FragmentComparator());
+
+        List sendNext = new ArrayList(Math.min(toSend.size(), 4));
+        List rv = new ArrayList(toSend.size());
+        for (int i = 0; i < toSend.size(); i++) {
+            Fragment next = toSend.get(i);
+            sendNext.add(next);
+            OutboundMessageState state = next.state;
+            OutNetMessage msg = state.getMessage();
+            int msgType = (msg != null) ? msg.getMessageTypeId() : -1;
+            if (_log.shouldLog(Log.INFO))
+                _log.info("Building packet for " + next + " to " + peer);
+            int curTotalDataSize = state.fragmentSize(next.num);
+            // now stuff in more fragments if they fit
+            if (i +1 < toSend.size()) {
+                int maxAvail = PacketBuilder.getMaxAdditionalFragmentSize(peer, sendNext.size(), curTotalDataSize);
+                for (int j = i + 1; j < toSend.size(); j++) {
+                    next = toSend.get(j);
+                    int nextDataSize = next.state.fragmentSize(next.num);
+                    //if (PacketBuilder.canFitAnotherFragment(peer, sendNext.size(), curTotalDataSize, nextDataSize)) {
+                    //if (_builder.canFitAnotherFragment(peer, sendNext.size(), curTotalDataSize, nextDataSize)) {
+                    if (nextDataSize <= maxAvail) {
+                        // add it
+                        toSend.remove(j);
+                        j--;
+                        sendNext.add(next);
+                        curTotalDataSize += nextDataSize;
+                        maxAvail = PacketBuilder.getMaxAdditionalFragmentSize(peer, sendNext.size(), curTotalDataSize);
+                        if (_log.shouldLog(Log.INFO))
+                            _log.info("Adding in additional " + next + " to " + peer);
+                    }  // else too big
+                }
+            }
+
+            int before = remaining.size();
+            UDPPacket pkt = _builder.buildPacket(sendNext, peer, remaining, newFullAckCount, partialACKBitfields);
+            if (pkt != null) {
+                if (_log.shouldLog(Log.INFO))
+                    _log.info("Built packet with " + sendNext.size() + " fragments totalling " + curTotalDataSize +
+                              " data bytes to " + peer);
+                _context.statManager().addRateData("udp.sendFragmentsPerPacket", sendNext.size());
+            }
+            sendNext.clear();
+            if (pkt == null) {
+                if (_log.shouldLog(Log.WARN))
+                    _log.info("Build packet FAIL for " + DataHelper.toString(sendNext) + " to " + peer);
+                continue;
+            }
+            rv.add(pkt);
+
+            int after = remaining.size();
+            newFullAckCount = Math.max(0, newFullAckCount - (before - after));
 
             int piggybackedAck = 0;
             if (msgIds.size() != remaining.size()) {
-                for (int i = 0; i < msgIds.size(); i++) {
-                    Long id = msgIds.get(i);
+                for (int j = 0; j < msgIds.size(); j++) {
+                    Long id = msgIds.get(j);
                     if (!remaining.contains(id)) {
                         peer.removeACKMessage(id);
                         piggybackedAck++;
@@ -411,29 +465,36 @@ class OutboundMessageFragments {
                 }
             }
 
-            if (sparseCount > 0)
-                _context.statManager().addRateData("udp.sendSparse", sparseCount, state.getLifetime());
             if (piggybackedAck > 0)
-                _context.statManager().addRateData("udp.sendPiggyback", piggybackedAck, state.getLifetime());
+                _context.statManager().addRateData("udp.sendPiggyback", piggybackedAck);
             if (piggybackedPartialACK - partialACKBitfields.size() > 0)
                 _context.statManager().addRateData("udp.sendPiggybackPartial", piggybackedPartialACK - partialACKBitfields.size(), state.getLifetime());
-            if (_log.shouldLog(Log.INFO))
-                _log.info("Building packet for " + state + " to " + peer + " with sparse count: " + sparseCount);
-            peer.packetsTransmitted(fragments - sparseCount);
-            if (state.getPushCount() > 1) {
-                int toSend = fragments-sparseCount;
-                peer.messageRetransmitted(toSend);
-                // _packetsRetransmitted += toSend; // lifetime for the transport
-                _context.statManager().addRateData("udp.peerPacketsRetransmitted", peer.getPacketsRetransmitted(), peer.getPacketsTransmitted());
-                _context.statManager().addRateData("udp.packetsRetransmitted", state.getLifetime(), peer.getPacketsTransmitted());
-                if (_log.shouldLog(Log.INFO))
-                    _log.info("Retransmitting " + state + " to " + peer);
-                _context.statManager().addRateData("udp.sendVolleyTime", state.getLifetime(), toSend);
-            }
-            return rv;
-        } else {
-            // !alive
-            return null;
+
+            // following for debugging and stats
+            pkt.setFragmentCount(sendNext.size());
+            pkt.setMessageType(msgType);  //type of first fragment
+        }
+
+
+
+        int sent = rv.size();
+        peer.packetsTransmitted(sent);
+        if (_log.shouldLog(Log.INFO))
+            _log.info("Sent " + fragmentsToSend + " fragments of " + states.size() +
+                      " messages in " + sent + " packets to " + peer);
+
+        return rv;
+    }
+
+    /**
+     *  Biggest first
+     *  @since 0.9.16
+     */
+    private static class FragmentComparator implements Comparator, Serializable {
+
+        public int compare(Fragment l, Fragment r) {
+            // reverse
+            return r.state.fragmentSize(r.num) - l.state.fragmentSize(l.num);
         }
     }
 
diff --git a/router/java/src/net/i2p/router/transport/udp/OutboundMessageState.java b/router/java/src/net/i2p/router/transport/udp/OutboundMessageState.java
index 39746e737f..0a10bc4a81 100644
--- a/router/java/src/net/i2p/router/transport/udp/OutboundMessageState.java
+++ b/router/java/src/net/i2p/router/transport/udp/OutboundMessageState.java
@@ -30,7 +30,10 @@ class OutboundMessageState implements CDPQEntry {
     private int _fragmentSize;
     /** size of the I2NP message */
     private int _totalSize;
-    /** sends[i] is how many times the fragment has been sent, or -1 if ACKed */
+    /** sends[i] is how many times the fragment has been sent, or -1 if ACKed
+     *  TODO this may not accurately track the number of retransmissions per-fragment,
+     *  and we don't make any use of it anyway, so we should just make it a bitfield.
+     */
     private short _fragmentSends[];
     private final long _startedOn;
     private long _nextSendTime;
@@ -205,7 +208,6 @@ class OutboundMessageState implements CDPQEntry {
     }
 
     public boolean needsSending(int fragment) {
-        
         short sends[] = _fragmentSends;
         if ( (sends == null) || (fragment >= sends.length) || (fragment < 0) )
             return false;
@@ -225,10 +227,12 @@ class OutboundMessageState implements CDPQEntry {
     public boolean acked(ACKBitfield bitfield) {
         // stupid brute force, but the cardinality should be trivial
         short sends[] = _fragmentSends;
-        if (sends != null)
-            for (int i = 0; i < bitfield.fragmentCount() && i < sends.length; i++)
+        if (sends != null) {
+            for (int i = 0; i < bitfield.fragmentCount() && i < sends.length; i++) {
                 if (bitfield.received(i))
                     sends[i] = (short)-1;
+            }
+        }
         
         boolean rv = isComplete();
       /****
@@ -263,7 +267,10 @@ class OutboundMessageState implements CDPQEntry {
      */
     public int getPushCount() { return _pushCount; }
 
-    /** note that we have pushed the message fragments */
+    /**
+     * Note that we have pushed the message fragments.
+     * Increments push count (and max sends... why?)
+     */
     public void push() { 
         // these will never be different...
         _pushCount++; 
@@ -272,7 +279,7 @@ class OutboundMessageState implements CDPQEntry {
         if (_fragmentSends != null)
             for (int i = 0; i < _fragmentSends.length; i++)
                 if (_fragmentSends[i] >= (short)0)
-                    _fragmentSends[i] = (short)(1 + _fragmentSends[i]);
+                    _fragmentSends[i]++;
         
     }
 
@@ -342,12 +349,15 @@ class OutboundMessageState implements CDPQEntry {
      * Throws NPE before then.
      *
      * Caller should synchronize
+     *
+     * @return true if fragment is not acked yet
      */
     public boolean shouldSend(int fragmentNum) { return _fragmentSends[fragmentNum] >= (short)0; }
 
     /**
      * This assumes fragment(int size) has been called
      * @param fragmentNum the number of the fragment 
+     *
      * @return the size of the fragment specified by the number
      */
     public int fragmentSize(int fragmentNum) {
diff --git a/router/java/src/net/i2p/router/transport/udp/PacketBuilder.java b/router/java/src/net/i2p/router/transport/udp/PacketBuilder.java
index 053703cb1c..a68fe9dd5c 100644
--- a/router/java/src/net/i2p/router/transport/udp/PacketBuilder.java
+++ b/router/java/src/net/i2p/router/transport/udp/PacketBuilder.java
@@ -13,7 +13,7 @@ import net.i2p.I2PAppContext;
 import net.i2p.data.Base64;
 import net.i2p.data.DataHelper;
 import net.i2p.data.Hash;
-import net.i2p.data.RouterIdentity;
+import net.i2p.data.router.RouterIdentity;
 import net.i2p.data.SessionKey;
 import net.i2p.data.Signature;
 import net.i2p.util.Addresses;
@@ -130,8 +130,10 @@ class PacketBuilder {
     /** if no extended options or rekey data, which we don't support  = 37 */
     public static final int HEADER_SIZE = UDPPacket.MAC_SIZE + UDPPacket.IV_SIZE + 1 + 4;
 
+    /** 4 byte msg ID + 3 byte fragment info */
+    public static final int FRAGMENT_HEADER_SIZE = 7;
     /** not including acks. 46 */
-    public static final int DATA_HEADER_SIZE = HEADER_SIZE + 9;
+    public static final int DATA_HEADER_SIZE = HEADER_SIZE + 2 + FRAGMENT_HEADER_SIZE;
 
     /** IPv4 only */
     public static final int IP_HEADER_SIZE = 20;
@@ -178,6 +180,49 @@ class PacketBuilder {
     }
 ****/
 
+    /**
+     *  Class for passing multiple fragments to buildPacket()
+     *
+     *  @since 0.9.16
+     */
+    public static class Fragment {
+        public final OutboundMessageState state;
+        public final int num;
+
+        public Fragment(OutboundMessageState state, int num) {
+            this.state = state;
+            this.num = num;
+        }
+
+        @Override
+        public String toString() {
+            return "Fragment " + num + " (" + state.fragmentSize(num) + " bytes) of " + state;
+        }
+    }
+
+    /**
+     *  Will a packet to 'peer' that already has 'numFragments' fragments
+     *  totalling 'curDataSize' bytes fit another fragment of size 'newFragSize' ??
+     *
+     *  This doesn't leave anything for acks.
+     *
+     *  @param numFragments >= 1
+     *  @since 0.9.16
+     */
+    public static int getMaxAdditionalFragmentSize(PeerState peer, int numFragments, int curDataSize) {
+        int available = peer.getMTU() - curDataSize;
+        if (peer.isIPv6())
+            available -= MIN_IPV6_DATA_PACKET_OVERHEAD;
+        else
+            available -= MIN_DATA_PACKET_OVERHEAD;
+        // OVERHEAD above includes 1 * FRAGMENT+HEADER_SIZE;
+        // this adds for the others, plus the new one.
+        available -= numFragments * FRAGMENT_HEADER_SIZE;
+        //if (_log.shouldLog(Log.DEBUG))
+        //    _log.debug("now: " + numFragments + " / " + curDataSize + " avail: " + available);
+        return available;
+    }
+
     /**
      * This builds a data packet (PAYLOAD_TYPE_DATA).
      * See the methods below for the other message types.
@@ -231,37 +276,65 @@ class PacketBuilder {
     public UDPPacket buildPacket(OutboundMessageState state, int fragment, PeerState peer,
                                  List ackIdsRemaining, int newAckCount,
                                  List partialACKsRemaining) {
+        List frags = Collections.singletonList(new Fragment(state, fragment));
+        return buildPacket(frags, peer, ackIdsRemaining, newAckCount, partialACKsRemaining);
+    }
+
+    /*
+     *  Multiple fragments
+     *
+     *  @since 0.9.16
+     */
+    public UDPPacket buildPacket(List fragments, PeerState peer,
+                                 List ackIdsRemaining, int newAckCount,
+                                 List partialACKsRemaining) {
+        StringBuilder msg = null;
+        if (_log.shouldLog(Log.INFO)) {
+            msg = new StringBuilder(256);
+            msg.append("Data pkt to ").append(peer.getRemotePeer().toBase64());
+        }
+
+        // calculate data size
+        int numFragments = fragments.size();
+        int dataSize = 0;
+        for (int i = 0; i < numFragments; i++) {
+            Fragment frag = fragments.get(i);
+            OutboundMessageState state = frag.state;
+            int fragment = frag.num;
+            int sz = state.fragmentSize(fragment);
+            dataSize += sz;
+            if (msg != null) {
+                msg.append(" Fragment ").append(i);
+                msg.append(": msg ").append(state.getMessageId()).append(' ').append(fragment);
+                msg.append('/').append(state.getFragmentCount());
+                msg.append(' ').append(sz);
+            }
+        }
+        
+        if (dataSize < 0)
+            return null;
+
+        // calculate size available for acks
+        int currentMTU = peer.getMTU();
+        int availableForAcks = currentMTU - dataSize;
+        int ipHeaderSize;
+        if (peer.isIPv6()) {
+            availableForAcks -= MIN_IPV6_DATA_PACKET_OVERHEAD;
+            ipHeaderSize = IPV6_HEADER_SIZE;
+        } else {
+            availableForAcks -= MIN_DATA_PACKET_OVERHEAD;
+            ipHeaderSize = IP_HEADER_SIZE;
+        }
+        if (numFragments > 1)
+            availableForAcks -= (numFragments - 1) * FRAGMENT_HEADER_SIZE;
+        int availableForExplicitAcks = availableForAcks;
+
+        // make the packet
         UDPPacket packet = buildPacketHeader((byte)(UDPPacket.PAYLOAD_TYPE_DATA << 4));
         DatagramPacket pkt = packet.getPacket();
         byte data[] = pkt.getData();
         int off = HEADER_SIZE;
 
-        StringBuilder msg = null;
-        if (_log.shouldLog(Log.INFO)) {
-            msg = new StringBuilder(128);
-            msg.append("Data pkt to ").append(peer.getRemotePeer().toBase64());
-            msg.append(" msg ").append(state.getMessageId()).append(" frag:").append(fragment);
-            msg.append('/').append(state.getFragmentCount());
-        }
-        
-        int dataSize = state.fragmentSize(fragment);
-        if (dataSize < 0) {
-            packet.release();
-            return null;
-        }
-
-        int currentMTU = peer.getMTU();
-        int availableForAcks = currentMTU - dataSize;
-        int ipHeaderSize;
-        if (peer.getRemoteIP().length == 4) {
-            availableForAcks -= MIN_DATA_PACKET_OVERHEAD;
-            ipHeaderSize = IP_HEADER_SIZE;
-        } else {
-            availableForAcks -= MIN_IPV6_DATA_PACKET_OVERHEAD;
-            ipHeaderSize = IPV6_HEADER_SIZE;
-        }
-        int availableForExplicitAcks = availableForAcks;
-
         // ok, now for the body...
         
         // just always ask for an ACK for now...
@@ -299,7 +372,7 @@ class PacketBuilder {
         off++;
 
         if (msg != null) {
-            msg.append(" data: ").append(dataSize).append(" bytes, mtu: ")
+            msg.append(" Total data: ").append(dataSize).append(" bytes, mtu: ")
                .append(currentMTU).append(", ")
                .append(newAckCount).append(" new full acks requested, ")
                .append(ackIdsRemaining.size() - newAckCount).append(" resend acks requested, ")
@@ -325,7 +398,7 @@ class PacketBuilder {
                 DataHelper.toLong(data, off, 4, ackId.longValue());
                 off += 4;        
                 if (msg != null) // logging it
-                    msg.append(" full ack: ").append(ackId.longValue());
+                    msg.append(' ').append(ackId.longValue());
             }
             //acksIncluded = true;
         }
@@ -357,7 +430,7 @@ class PacketBuilder {
                 }
                 iter.remove();
                 if (msg != null) // logging it
-                    msg.append(" partial ack: ").append(bitfield);
+                    msg.append(' ').append(bitfield);
             }
             //acksIncluded = true;
             // now jump back and fill in the number of bitfields *actually* included
@@ -367,30 +440,42 @@ class PacketBuilder {
         //if ( (msg != null) && (acksIncluded) )
         //  _log.debug(msg.toString());
         
-        DataHelper.toLong(data, off, 1, 1); // only one fragment in this message
+        DataHelper.toLong(data, off, 1, numFragments);
         off++;
         
-        DataHelper.toLong(data, off, 4, state.getMessageId());
-        off += 4;
+        // now write each fragment
+        int sizeWritten = 0;
+        for (int i = 0; i < numFragments; i++) {
+            Fragment frag = fragments.get(i);
+            OutboundMessageState state = frag.state;
+            int fragment = frag.num;
+
+            DataHelper.toLong(data, off, 4, state.getMessageId());
+            off += 4;
         
-        data[off] |= fragment << 1;
-        if (fragment == state.getFragmentCount() - 1)
-            data[off] |= 1; // isLast
-        off++;
+            data[off] |= fragment << 1;
+            if (fragment == state.getFragmentCount() - 1)
+                data[off] |= 1; // isLast
+            off++;
         
-        DataHelper.toLong(data, off, 2, dataSize);
-        data[off] &= (byte)0x3F; // 2 highest bits are reserved
-        off += 2;
+            int fragSize = state.fragmentSize(fragment);
+            DataHelper.toLong(data, off, 2, fragSize);
+            data[off] &= (byte)0x3F; // 2 highest bits are reserved
+            off += 2;
         
-        int sizeWritten = state.writeFragment(data, off, fragment);
+            int sz = state.writeFragment(data, off, fragment);
+            off += sz;
+            sizeWritten += sz;
+        }
+
         if (sizeWritten != dataSize) {
             if (sizeWritten < 0) {
                 // probably already freed from OutboundMessageState
                 if (_log.shouldLog(Log.WARN))
-                    _log.warn("Write failed for fragment " + fragment + " of " + state.getMessageId());
+                    _log.warn("Write failed for " + DataHelper.toString(fragments));
             } else {
-                _log.error("Size written: " + sizeWritten + " but size: " + dataSize 
-                           + " for fragment " + fragment + " of " + state.getMessageId());
+                _log.error("Size written: " + sizeWritten + " but size: " + dataSize +
+                           " for " + DataHelper.toString(fragments));
             }
             packet.release();
             return null;
@@ -398,31 +483,44 @@ class PacketBuilder {
         //    _log.debug("Size written: " + sizeWritten + " for fragment " + fragment 
         //               + " of " + state.getMessageId());
         }
+
         // put this after writeFragment() since dataSize will be zero for use-after-free
         if (dataSize == 0) {
             // OK according to the protocol but if we send it, it's a bug
-            _log.error("Sending zero-size fragment " + fragment + " of " + state + " for " + peer);
+            _log.error("Sending zero-size fragment??? for " + DataHelper.toString(fragments));
         }
-        off += dataSize;
-
         
         // pad up so we're on the encryption boundary
         off = pad1(data, off);
         off = pad2(data, off, currentMTU - (ipHeaderSize + UDP_HEADER_SIZE));
         pkt.setLength(off);
 
-        authenticate(packet, peer.getCurrentCipherKey(), peer.getCurrentMACKey());
-        setTo(packet, peer.getRemoteIPAddress(), peer.getRemotePort());
-        
-        if (_log.shouldLog(Log.INFO)) {
+        if (msg != null) {
+            // verify multi-fragment packet
+            //if (numFragments > 1) {
+            //    msg.append("\nDataReader dump\n:");
+            //    UDPPacketReader reader = new UDPPacketReader(_context);
+            //    reader.initialize(packet);
+            //    UDPPacketReader.DataReader dreader = reader.getDataReader();
+            //    try {
+            //        msg.append(dreader.toString());
+            //    } catch (Exception e) {
+            //        _log.info("blowup, dump follows", e);
+            //        msg.append('\n');
+            //        msg.append(net.i2p.util.HexDump.dump(data, 0, off));
+            //    }
+            //}
             msg.append(" pkt size ").append(off + (ipHeaderSize + UDP_HEADER_SIZE));
             _log.info(msg.toString());
         }
+
+        authenticate(packet, peer.getCurrentCipherKey(), peer.getCurrentMACKey());
+        setTo(packet, peer.getRemoteIPAddress(), peer.getRemotePort());
+        
         // the packet could have been built before the current mtu got lowered, so
         // compare to LARGE_MTU
         if (off + (ipHeaderSize + UDP_HEADER_SIZE) > PeerState.LARGE_MTU) {
             _log.error("Size is " + off + " for " + packet +
-                       " fragment " + fragment +
                        " data size " + dataSize +
                        " pkt size " + (off + (ipHeaderSize + UDP_HEADER_SIZE)) +
                        " MTU " + currentMTU +
@@ -430,7 +528,7 @@ class PacketBuilder {
                        availableForExplicitAcks + " for full acks " + 
                        explicitToSend + " full acks included " +
                        partialAcksToSend + " partial acks included " +
-                       " OMS " + state, new Exception());
+                       " Fragments: " + DataHelper.toString(fragments), new Exception());
         }
         
         return packet;
@@ -596,14 +694,22 @@ class PacketBuilder {
         off += 4;
         DataHelper.toLong(data, off, 4, state.getSentSignedOnTime());
         off += 4;
-        System.arraycopy(state.getSentSignature().getData(), 0, data, off, Signature.SIGNATURE_BYTES);
-        off += Signature.SIGNATURE_BYTES;
-        // ok, we need another 8 bytes of random padding
-        // (ok, this only gives us 63 bits, not 64)
-        long l = _context.random().nextLong();
-        if (l < 0) l = 0 - l;
-        DataHelper.toLong(data, off, 8, l);
-        off += 8;
+
+        // handle variable signature size
+        Signature sig = state.getSentSignature();
+        int siglen = sig.length();
+        System.arraycopy(sig.getData(), 0, data, off, siglen);
+        off += siglen;
+        // ok, we need another few bytes of random padding
+        int rem = siglen % 16;
+        int padding;
+        if (rem > 0) {
+            padding = 16 - rem;
+            _context.random().nextBytes(data, off, padding);
+            off += padding;
+        } else {
+            padding = 0;
+        }
         
         if (_log.shouldLog(Log.DEBUG)) {
             StringBuilder buf = new StringBuilder(128);
@@ -612,9 +718,9 @@ class PacketBuilder {
             buf.append(" Bob: ").append(Addresses.toString(state.getReceivedOurIP(), externalPort));
             buf.append(" RelayTag: ").append(state.getSentRelayTag());
             buf.append(" SignedOn: ").append(state.getSentSignedOnTime());
-            buf.append(" signature: ").append(Base64.encode(state.getSentSignature().getData()));
+            buf.append(" signature: ").append(Base64.encode(sig.getData()));
             buf.append("\nRawCreated: ").append(Base64.encode(data, 0, off)); 
-            buf.append("\nsignedTime: ").append(Base64.encode(data, off-8-Signature.SIGNATURE_BYTES-4, 4));
+            buf.append("\nsignedTime: ").append(Base64.encode(data, off - padding - siglen - 4, 4));
             _log.debug(buf.toString());
         }
         
@@ -623,7 +729,7 @@ class PacketBuilder {
         byte[] iv = SimpleByteCache.acquire(UDPPacket.IV_SIZE);
         _context.random().nextBytes(iv);
         
-        int encrWrite = Signature.SIGNATURE_BYTES + 8;
+        int encrWrite = siglen + padding;
         int sigBegin = off - encrWrite;
         _context.aes().encrypt(data, sigBegin, data, sigBegin, state.getCipherKey(), iv, encrWrite);
         
@@ -774,8 +880,11 @@ class PacketBuilder {
             DataHelper.toLong(data, off, 4, state.getSentSignedOnTime());
             off += 4;
             
+            // handle variable signature size
             // we need to pad this so we're at the encryption boundary
-            int mod = (off + Signature.SIGNATURE_BYTES) & 0x0f;
+            Signature sig = state.getSentSignature();
+            int siglen = sig.length();
+            int mod = (off + siglen) & 0x0f;
             if (mod != 0) {
                 int paddingRequired = 16 - mod;
                 // add an arbitrary number of 16byte pad blocks too ???
@@ -787,8 +896,8 @@ class PacketBuilder {
             // so trailing non-mod-16 data is ignored. That truncates the sig.
             
             // BUG: NPE here if null signature
-            System.arraycopy(state.getSentSignature().getData(), 0, data, off, Signature.SIGNATURE_BYTES);
-            off += Signature.SIGNATURE_BYTES;
+            System.arraycopy(sig.getData(), 0, data, off, siglen);
+            off += siglen;
         } else {
             // We never get here (see above)
 
diff --git a/router/java/src/net/i2p/router/transport/udp/PacketPusher.java b/router/java/src/net/i2p/router/transport/udp/PacketPusher.java
index db7fb4ea34..f28ee96450 100644
--- a/router/java/src/net/i2p/router/transport/udp/PacketPusher.java
+++ b/router/java/src/net/i2p/router/transport/udp/PacketPusher.java
@@ -37,11 +37,10 @@ class PacketPusher implements Runnable {
     public void run() {
         while (_alive) {
             try {
-                UDPPacket packets[] = _fragments.getNextVolley();
+                List packets = _fragments.getNextVolley();
                 if (packets != null) {
-                    for (int i = 0; i < packets.length; i++) {
-                        if (packets[i] != null) // null for ACKed fragments
-                            send(packets[i]);
+                    for (int i = 0; i < packets.size(); i++) {
+                         send(packets.get(i));
                     }
                 }
             } catch (Exception e) {
diff --git a/router/java/src/net/i2p/router/transport/udp/PeerState.java b/router/java/src/net/i2p/router/transport/udp/PeerState.java
index 06a440ada5..8a5d41d802 100644
--- a/router/java/src/net/i2p/router/transport/udp/PeerState.java
+++ b/router/java/src/net/i2p/router/transport/udp/PeerState.java
@@ -242,6 +242,9 @@ class PeerState {
     private static final int MINIMUM_WINDOW_BYTES = DEFAULT_SEND_WINDOW_BYTES;
     private static final int MAX_SEND_WINDOW_BYTES = 1024*1024;
 
+    /** max number of msgs returned from allocateSend() */
+    private static final int MAX_ALLOCATE_SEND = 2;
+
     /**
      *  Was 32 before 0.9.2, but since the streaming lib goes up to 128,
      *  we would just drop our own msgs right away during slow start.
@@ -1563,15 +1566,16 @@ class PeerState {
     }
     
     /**
-     * Pick a message we want to send and allocate it out of our window
+     * Pick one or more messages we want to send and allocate them out of our window
      * High usage -
      * OutboundMessageFragments.getNextVolley() calls this 2nd, if finishMessages() returned > 0.
      * TODO combine finishMessages(), allocateSend(), and getNextDelay() so we don't iterate 3 times.
      *
-     * @return allocated message to send, or null if no messages or no resources
+     * @return allocated messages to send (never empty), or null if no messages or no resources
      */
-    public OutboundMessageState allocateSend() {
+    public List allocateSend() {
         if (_dead) return null;
+        List rv = null;
         synchronized (_outboundMessages) {
             for (OutboundMessageState state : _outboundMessages) {
                 // We have 3 return values, because if allocateSendingBytes() returns false,
@@ -1588,44 +1592,54 @@ class PeerState {
                             msg.timestamp("not reached for allocation " + msgs.size() + " other peers");
                     }
                      */
-                    return state;
+                    if (rv == null)
+                        rv = new ArrayList(MAX_ALLOCATE_SEND);
+                    rv.add(state);
+                    if (rv.size() >= MAX_ALLOCATE_SEND)
+                        return rv;
                 } else if (should == ShouldSend.NO_BW) {
                     // no more bandwidth available
                     // we don't bother looking for a smaller msg that would fit.
                     // By not looking further, we keep strict sending order, and that allows
                     // some efficiency in acked() below.
-                    if (_log.shouldLog(Log.DEBUG))
+                    if (rv == null && _log.shouldLog(Log.DEBUG))
                         _log.debug("Nothing to send (BW) to " + _remotePeer + ", with " + _outboundMessages.size() +
                                    " / " + _outboundQueue.size() + " remaining");
-                    return null;
+                    return rv;
                 } /* else {
                     OutNetMessage msg = state.getMessage();
                     if (msg != null)
                         msg.timestamp("passed over for allocation with " + msgs.size() + " peers");
                 } */
             }
+
             // Peek at head of _outboundQueue and see if we can send it.
             // If so, pull it off, put it in _outbundMessages, test
             // again for bandwidth if necessary, and return it.
-            OutboundMessageState state = _outboundQueue.peek();
-            if (state != null && ShouldSend.YES == locked_shouldSend(state)) {
+            OutboundMessageState state;
+            while ((state = _outboundQueue.peek()) != null &&
+                   ShouldSend.YES == locked_shouldSend(state)) {
                 // we could get a different state, or null, when we poll,
                 // due to AQM drops, so we test again if necessary
                 OutboundMessageState dequeuedState = _outboundQueue.poll();
                 if (dequeuedState != null) {
                     _outboundMessages.add(dequeuedState);
-                    if (dequeuedState == state || ShouldSend.YES == locked_shouldSend(dequeuedState)) {
+                    if (dequeuedState == state || ShouldSend.YES == locked_shouldSend(state)) {
                         if (_log.shouldLog(Log.DEBUG))
                             _log.debug("Allocate sending (NEW) to " + _remotePeer + ": " + dequeuedState.getMessageId());
-                        return dequeuedState;
+                        if (rv == null)
+                            rv = new ArrayList(MAX_ALLOCATE_SEND);
+                        rv.add(state);
+                        if (rv.size() >= MAX_ALLOCATE_SEND)
+                            return rv;
                     }
                 }
             }
         }
-        if (_log.shouldLog(Log.DEBUG))
+        if ( rv == null && _log.shouldLog(Log.DEBUG))
             _log.debug("Nothing to send to " + _remotePeer + ", with " + _outboundMessages.size() +
                        " / " + _outboundQueue.size() + " remaining");
-        return null;
+        return rv;
     }
     
     /**
diff --git a/router/java/src/net/i2p/router/transport/udp/PeerTestManager.java b/router/java/src/net/i2p/router/transport/udp/PeerTestManager.java
index 48a5e08225..bc47fba87c 100644
--- a/router/java/src/net/i2p/router/transport/udp/PeerTestManager.java
+++ b/router/java/src/net/i2p/router/transport/udp/PeerTestManager.java
@@ -9,8 +9,8 @@ import java.util.concurrent.LinkedBlockingQueue;
 
 import net.i2p.data.Base64;
 import net.i2p.data.DataHelper;
-import net.i2p.data.RouterAddress;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterAddress;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.data.SessionKey;
 import net.i2p.router.CommSystemFacade;
 import net.i2p.router.RouterContext;
diff --git a/router/java/src/net/i2p/router/transport/udp/UDPAddress.java b/router/java/src/net/i2p/router/transport/udp/UDPAddress.java
index f05205582d..88551d588c 100644
--- a/router/java/src/net/i2p/router/transport/udp/UDPAddress.java
+++ b/router/java/src/net/i2p/router/transport/udp/UDPAddress.java
@@ -5,7 +5,7 @@ import java.net.UnknownHostException;
 import java.util.Map;
 
 import net.i2p.data.Base64;
-import net.i2p.data.RouterAddress;
+import net.i2p.data.router.RouterAddress;
 import net.i2p.data.SessionKey;
 import net.i2p.util.LHMCache;
 import net.i2p.util.SystemVersion;
diff --git a/router/java/src/net/i2p/router/transport/udp/UDPPacket.java b/router/java/src/net/i2p/router/transport/udp/UDPPacket.java
index 252490cfc6..1f5f65a6a7 100644
--- a/router/java/src/net/i2p/router/transport/udp/UDPPacket.java
+++ b/router/java/src/net/i2p/router/transport/udp/UDPPacket.java
@@ -166,7 +166,11 @@ class UDPPacket implements CDQEntry {
     int getMessageType() { return _messageType; }
     /** only for debugging and stats, does not go on the wire */
     void setMessageType(int type) { _messageType = type; }
+
+    /** only for debugging and stats */
     int getFragmentCount() { return _fragmentCount; }
+
+    /** only for debugging and stats */
     void setFragmentCount(int count) { _fragmentCount = count; }
 
     RemoteHostId getRemoteHost() {
diff --git a/router/java/src/net/i2p/router/transport/udp/UDPPacketReader.java b/router/java/src/net/i2p/router/transport/udp/UDPPacketReader.java
index affaf09df8..2b838eaab2 100644
--- a/router/java/src/net/i2p/router/transport/udp/UDPPacketReader.java
+++ b/router/java/src/net/i2p/router/transport/udp/UDPPacketReader.java
@@ -203,9 +203,10 @@ class UDPPacketReader {
             return rv;
         }
         
-        public void readEncryptedSignature(byte target[], int targetOffset) {
+        /** @param size the amount to be copied, including padding to mod 16 */
+        public void readEncryptedSignature(byte target[], int targetOffset, int size) {
             int offset = readBodyOffset() + Y_LENGTH + 1 + readIPSize() + 2 + 4 + 4;
-            System.arraycopy(_message, offset, target, targetOffset, Signature.SIGNATURE_BYTES + 8);
+            System.arraycopy(_message, offset, target, targetOffset, size);
         }
         
         public void readIV(byte target[], int targetOffset) {
@@ -239,7 +240,11 @@ class UDPPacketReader {
             System.arraycopy(_message, readOffset, target, targetOffset, len);
         }
         
-        /** read the time at which the signature was generated */
+        /**
+         *  Read the time at which the signature was generated.
+         *  TODO must be completely in final fragment.
+         *  Time and sig cannot be split across fragments.
+         */
         public long readFinalFragmentSignedOnTime() {
             if (readCurrentFragmentNum() != readTotalFragmentNum()-1)
                 throw new IllegalStateException("This is not the final fragment");
@@ -247,12 +252,19 @@ class UDPPacketReader {
             return DataHelper.fromLong(_message, readOffset, 4);
         }
         
-        /** read the signature from the final sessionConfirmed packet */
-        public void readFinalSignature(byte target[], int targetOffset) {
+        /**
+         *  Read the signature from the final sessionConfirmed packet.
+         *  TODO must be completely in final fragment.
+         *  Time and sig cannot be split across fragments.
+         *  @param size not including padding
+         */
+        public void readFinalSignature(byte target[], int targetOffset, int size) {
             if (readCurrentFragmentNum() != readTotalFragmentNum()-1)
                 throw new IllegalStateException("This is not the final fragment");
-            int readOffset = _payloadBeginOffset + _payloadLength - Signature.SIGNATURE_BYTES;
-            System.arraycopy(_message, readOffset, target, targetOffset, Signature.SIGNATURE_BYTES);
+            int readOffset = _payloadBeginOffset + _payloadLength - size;
+            if (readOffset < readBodyOffset() + (1 + 2 + 4))
+                throw new IllegalStateException("Sig split across fragments");
+            System.arraycopy(_message, readOffset, target, targetOffset, size);
         }
     }
     
@@ -267,26 +279,33 @@ class UDPPacketReader {
         public boolean readACKsIncluded() {
             return flagSet(UDPPacket.DATA_FLAG_EXPLICIT_ACK);
         }
+
         public boolean readACKBitfieldsIncluded() {
             return flagSet(UDPPacket.DATA_FLAG_ACK_BITFIELDS);
         }
+
         public boolean readECN() {
             return flagSet(UDPPacket.DATA_FLAG_ECN);
         }
+
         public boolean readWantPreviousACKs() {
             return flagSet(UDPPacket.DATA_FLAG_WANT_ACKS);
         }
+
         public boolean readReplyRequested() { 
             return flagSet(UDPPacket.DATA_FLAG_WANT_REPLY);
         }
+
         public boolean readExtendedDataIncluded() {
             return flagSet(UDPPacket.DATA_FLAG_EXTENDED);
         }
+
         public int readACKCount() {
             if (!readACKsIncluded()) return 0;
             int off = readBodyOffset() + 1;
             return (int)DataHelper.fromLong(_message, off, 1);
         }
+
         public long readACK(int index) {
             if (!readACKsIncluded()) return -1;
             int off = readBodyOffset() + 1;
@@ -294,6 +313,7 @@ class UDPPacketReader {
             off++;
             return DataHelper.fromLong(_message, off + (4 * index), 4);
         }
+
         public ACKBitfield[] readACKBitfields() {
             if (!readACKBitfieldsIncluded()) return null;
             int off = readBodyOffset() + 1;
@@ -342,28 +362,29 @@ class UDPPacketReader {
             int fragmentBegin = getFragmentBegin(fragmentNum);
             return DataHelper.fromLong(_message, fragmentBegin, 4);
         }
+
         public int readMessageFragmentNum(int fragmentNum) {
             int off = getFragmentBegin(fragmentNum);
             off += 4; // messageId
             return (_message[off] & 0xFF) >>> 1;
         }
+
         public boolean readMessageIsLast(int fragmentNum) {
             int off = getFragmentBegin(fragmentNum);
             off += 4; // messageId
             return ((_message[off] & 1) != 0);
         }
+
         public int readMessageFragmentSize(int fragmentNum) {
             int off = getFragmentBegin(fragmentNum);
-            off += 4; // messageId
-            off++; // fragment info
+            off += 5; // messageId + fragment info
             return ((int)DataHelper.fromLong(_message, off, 2)) & 0x3FFF;
         }
 
         public void readMessageFragment(int fragmentNum, byte target[], int targetOffset)
                                                       throws ArrayIndexOutOfBoundsException {
             int off = getFragmentBegin(fragmentNum);
-            off += 4; // messageId
-            off++; // fragment info
+            off += 5; // messageId + fragment info
             int size = ((int)DataHelper.fromLong(_message, off, 2)) & 0x3FFF;
             off += 2;
             System.arraycopy(_message, off, target, targetOffset, size);
@@ -393,16 +414,14 @@ class UDPPacketReader {
             }
             off++; // # fragments
             
-            if (fragmentNum == 0) {
-                return off;
-            } else {
+            if (fragmentNum > 0) {
                 for (int i = 0; i < fragmentNum; i++) {
                     off += 5; // messageId+info
                     off += ((int)DataHelper.fromLong(_message, off, 2)) & 0x3FFF;
                     off += 2;
                 }
-                return off;
             }
+            return off;
         }
 
         private boolean flagSet(byte flag) {
@@ -465,10 +484,9 @@ class UDPPacketReader {
                 buf.append(" isLast? ").append(isLast);
                 buf.append(" info ").append(_message[off-1]);
                 int size = ((int)DataHelper.fromLong(_message, off, 2)) & 0x3FFF;
-                buf.append(" with ").append(size).append(" bytes");
-                buf.append(' ');
-                off += size;
                 off += 2;
+                buf.append(" with ").append(size).append(" bytes; ");
+                off += size;
             }
             
             return buf.toString();
diff --git a/router/java/src/net/i2p/router/transport/udp/UDPTransport.java b/router/java/src/net/i2p/router/transport/udp/UDPTransport.java
index b9c2ad8024..63fc957600 100644
--- a/router/java/src/net/i2p/router/transport/udp/UDPTransport.java
+++ b/router/java/src/net/i2p/router/transport/udp/UDPTransport.java
@@ -25,9 +25,9 @@ import java.util.concurrent.CopyOnWriteArrayList;
 import net.i2p.data.DatabaseEntry;
 import net.i2p.data.DataHelper;
 import net.i2p.data.Hash;
-import net.i2p.data.RouterAddress;
-import net.i2p.data.RouterIdentity;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterAddress;
+import net.i2p.data.router.RouterIdentity;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.data.SessionKey;
 import net.i2p.data.i2np.DatabaseStoreMessage;
 import net.i2p.data.i2np.I2NPMessage;
@@ -1550,6 +1550,12 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
                 return null;
             }
 
+            // Check for supported sig type
+            if (toAddress.getIdentity().getSigningPublicKey().getType() == null) {
+                markUnreachable(to);
+                return null;
+            }
+
             if (!allowConnection())
                 return _cachedBid[TRANSIENT_FAIL_BID];
 
diff --git a/router/java/src/net/i2p/router/tunnel/InboundGatewayReceiver.java b/router/java/src/net/i2p/router/tunnel/InboundGatewayReceiver.java
index 3fa40c984e..bb18c44b22 100644
--- a/router/java/src/net/i2p/router/tunnel/InboundGatewayReceiver.java
+++ b/router/java/src/net/i2p/router/tunnel/InboundGatewayReceiver.java
@@ -1,7 +1,7 @@
 package net.i2p.router.tunnel;
 
 import net.i2p.data.Hash;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.data.i2np.TunnelDataMessage;
 import net.i2p.router.JobImpl;
 import net.i2p.router.OutNetMessage;
diff --git a/router/java/src/net/i2p/router/tunnel/InboundMessageDistributor.java b/router/java/src/net/i2p/router/tunnel/InboundMessageDistributor.java
index ec3529b48f..660aedab81 100644
--- a/router/java/src/net/i2p/router/tunnel/InboundMessageDistributor.java
+++ b/router/java/src/net/i2p/router/tunnel/InboundMessageDistributor.java
@@ -4,7 +4,7 @@ import net.i2p.data.DatabaseEntry;
 import net.i2p.data.Hash;
 import net.i2p.data.LeaseSet;
 import net.i2p.data.Payload;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.data.TunnelId;
 import net.i2p.data.i2np.DataMessage;
 import net.i2p.data.i2np.DatabaseSearchReplyMessage;
diff --git a/router/java/src/net/i2p/router/tunnel/OutboundMessageDistributor.java b/router/java/src/net/i2p/router/tunnel/OutboundMessageDistributor.java
index 364c5970ab..3961af80fc 100644
--- a/router/java/src/net/i2p/router/tunnel/OutboundMessageDistributor.java
+++ b/router/java/src/net/i2p/router/tunnel/OutboundMessageDistributor.java
@@ -4,7 +4,7 @@ import java.util.HashSet;
 import java.util.Set;
 
 import net.i2p.data.Hash;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.data.TunnelId;
 import net.i2p.data.i2np.I2NPMessage;
 import net.i2p.data.i2np.TunnelGatewayMessage;
diff --git a/router/java/src/net/i2p/router/tunnel/OutboundReceiver.java b/router/java/src/net/i2p/router/tunnel/OutboundReceiver.java
index 03923ecfeb..4aef149028 100644
--- a/router/java/src/net/i2p/router/tunnel/OutboundReceiver.java
+++ b/router/java/src/net/i2p/router/tunnel/OutboundReceiver.java
@@ -1,7 +1,7 @@
 package net.i2p.router.tunnel;
 
 import net.i2p.data.Hash;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.data.i2np.TunnelDataMessage;
 import net.i2p.router.JobImpl;
 import net.i2p.router.OutNetMessage;
diff --git a/router/java/src/net/i2p/router/tunnel/TunnelParticipant.java b/router/java/src/net/i2p/router/tunnel/TunnelParticipant.java
index b53d8a6430..d580602793 100644
--- a/router/java/src/net/i2p/router/tunnel/TunnelParticipant.java
+++ b/router/java/src/net/i2p/router/tunnel/TunnelParticipant.java
@@ -1,7 +1,7 @@
 package net.i2p.router.tunnel;
 
 import net.i2p.data.Hash;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.data.TunnelId;
 import net.i2p.data.i2np.I2NPMessage;
 import net.i2p.data.i2np.TunnelDataMessage;
diff --git a/router/java/src/net/i2p/router/tunnel/pool/BuildExecutor.java b/router/java/src/net/i2p/router/tunnel/pool/BuildExecutor.java
index b771589a1f..42cff28c7a 100644
--- a/router/java/src/net/i2p/router/tunnel/pool/BuildExecutor.java
+++ b/router/java/src/net/i2p/router/tunnel/pool/BuildExecutor.java
@@ -10,7 +10,7 @@ import java.util.concurrent.ConcurrentHashMap;
 
 import net.i2p.data.Hash;
 import net.i2p.data.i2np.I2NPMessage;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.router.CommSystemFacade;
 import net.i2p.router.RouterContext;
 import net.i2p.router.TunnelManagerFacade;
diff --git a/router/java/src/net/i2p/router/tunnel/pool/BuildHandler.java b/router/java/src/net/i2p/router/tunnel/pool/BuildHandler.java
index afe3940bff..0f914fbcc0 100644
--- a/router/java/src/net/i2p/router/tunnel/pool/BuildHandler.java
+++ b/router/java/src/net/i2p/router/tunnel/pool/BuildHandler.java
@@ -9,8 +9,8 @@ import net.i2p.data.Base64;
 import net.i2p.data.ByteArray;
 import net.i2p.data.DataHelper;
 import net.i2p.data.Hash;
-import net.i2p.data.RouterIdentity;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterIdentity;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.data.TunnelId;
 import net.i2p.data.i2np.BuildRequestRecord;
 import net.i2p.data.i2np.BuildResponseRecord;
@@ -85,6 +85,11 @@ class BuildHandler implements Runnable {
      */
     private static final int NEXT_HOP_SEND_TIMEOUT = 25*1000;
 
+    private static final long MAX_REQUEST_FUTURE = 5*60*1000;
+    /** must be > 1 hour due to rouding down */
+    private static final long MAX_REQUEST_AGE = 65*60*1000;
+
+
     public BuildHandler(RouterContext ctx, TunnelPoolManager manager, BuildExecutor exec) {
         _context = ctx;
         _log = ctx.logManager().getLog(getClass());
@@ -101,8 +106,10 @@ class BuildHandler implements Runnable {
         _context.statManager().createRateStat("tunnel.reject.50", "How often we reject a tunnel because of a critical issue (shutdown, etc)", "Tunnels", new long[] { 60*1000, 10*60*1000 });
 
         _context.statManager().createRequiredRateStat("tunnel.decryptRequestTime", "Time to decrypt a build request (ms)", "Tunnels", new long[] { 60*1000, 10*60*1000 });
-        _context.statManager().createRequiredRateStat("tunnel.rejectTimeout", "Reject tunnel count (unknown next hop)", "Tunnels", new long[] { 60*1000, 10*60*1000 });
-        _context.statManager().createRequiredRateStat("tunnel.rejectTimeout2", "Reject tunnel count (can't contact next hop)", "Tunnels", new long[] { 60*1000, 10*60*1000 });
+        _context.statManager().createRateStat("tunnel.rejectTooOld", "Reject tunnel count (too old)", "Tunnels", new long[] { 3*60*60*1000 });
+        _context.statManager().createRateStat("tunnel.rejectFuture", "Reject tunnel count (time in future)", "Tunnels", new long[] { 3*60*60*1000 });
+        _context.statManager().createRateStat("tunnel.rejectTimeout", "Reject tunnel count (unknown next hop)", "Tunnels", new long[] { 60*60*1000 });
+        _context.statManager().createRateStat("tunnel.rejectTimeout2", "Reject tunnel count (can't contact next hop)", "Tunnels", new long[] { 60*60*1000 });
         _context.statManager().createRequiredRateStat("tunnel.rejectDupID", "Part. tunnel dup ID", "Tunnels", new long[] { 24*60*60*1000 });
         _context.statManager().createRequiredRateStat("tunnel.ownDupID", "Our tunnel dup. ID", "Tunnels", new long[] { 24*60*60*1000 });
         _context.statManager().createRequiredRateStat("tunnel.rejectHostile", "Reject malicious tunnel", "Tunnels", new long[] { 24*60*60*1000 });
@@ -587,11 +594,24 @@ class BuildHandler implements Runnable {
             }
         }
 
-        // time is in hours, and only for log below - what's the point?
-        // tunnel-alt-creation.html specifies that this is enforced +/- 1 hour but it is not.
+        // time is in hours, rounded down.
+        // tunnel-alt-creation.html specifies that this is enforced +/- 1 hour but it was not.
+        // As of 0.9.16, allow + 5 minutes to - 65 minutes.
         long time = req.readRequestTime();
         long now = (_context.clock().now() / (60l*60l*1000l)) * (60*60*1000);
-        int ourSlot = -1;
+        long timeDiff = now - time;
+        if (timeDiff > MAX_REQUEST_AGE) {
+            _context.statManager().addRateData("tunnel.rejectTooOld", 1);
+            if (_log.shouldLog(Log.WARN))
+                _log.warn("Dropping build request too old... replay attack? " + DataHelper.formatDuration(timeDiff));
+            return;
+        }
+        if (timeDiff < 0 - MAX_REQUEST_FUTURE) {
+            _context.statManager().addRateData("tunnel.rejectFuture", 1);
+            if (_log.shouldLog(Log.WARN))
+                _log.warn("Dropping build request too far in future " + DataHelper.formatDuration(0 - timeDiff));
+            return;
+        }
 
         int response;
         if (_context.router().isHidden()) {
@@ -764,6 +784,7 @@ class BuildHandler implements Runnable {
 
         byte reply[] = BuildResponseRecord.create(_context, response, req.readReplyKey(), req.readReplyIV(), state.msg.getUniqueId());
         int records = state.msg.getRecordCount();
+        int ourSlot = -1;
         for (int j = 0; j < records; j++) {
             if (state.msg.getRecord(j) == null) {
                 ourSlot = j;
@@ -780,7 +801,7 @@ class BuildHandler implements Runnable {
                       + " accepted? " + response + " receiving on " + ourId 
                       + " sending to " + nextId
                       + " on " + nextPeer
-                      + " inGW? " + isInGW + " outEnd? " + isOutEnd + " time difference " + (now-time)
+                      + " inGW? " + isInGW + " outEnd? " + isOutEnd
                       + " recvDelay " + recvDelay + " replyMessage " + req.readReplyMessageId()
                       + " replyKey " + req.readReplyKey() + " replyIV " + Base64.encode(req.readReplyIV()));
 
diff --git a/router/java/src/net/i2p/router/tunnel/pool/BuildRequestor.java b/router/java/src/net/i2p/router/tunnel/pool/BuildRequestor.java
index fbfca2bcc9..31aaa86654 100644
--- a/router/java/src/net/i2p/router/tunnel/pool/BuildRequestor.java
+++ b/router/java/src/net/i2p/router/tunnel/pool/BuildRequestor.java
@@ -8,7 +8,7 @@ import net.i2p.data.ByteArray;
 import net.i2p.data.DataHelper;
 import net.i2p.data.Hash;
 import net.i2p.data.PublicKey;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.data.TunnelId;
 import net.i2p.data.i2np.TunnelBuildMessage;
 import net.i2p.data.i2np.VariableTunnelBuildMessage;
diff --git a/router/java/src/net/i2p/router/tunnel/pool/TunnelPeerSelector.java b/router/java/src/net/i2p/router/tunnel/pool/TunnelPeerSelector.java
index 14eab47e2c..8bb657831c 100644
--- a/router/java/src/net/i2p/router/tunnel/pool/TunnelPeerSelector.java
+++ b/router/java/src/net/i2p/router/tunnel/pool/TunnelPeerSelector.java
@@ -16,7 +16,7 @@ import net.i2p.I2PAppContext;
 import net.i2p.crypto.SHA256Generator;
 import net.i2p.data.DataFormatException;
 import net.i2p.data.Hash;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.router.Router;
 import net.i2p.router.RouterContext;
 import net.i2p.router.TunnelPoolSettings;