From 8f7b31aed346d82667275c189b8524b1196cd602 Mon Sep 17 00:00:00 2001 From: zzz Date: Wed, 23 Oct 2013 20:20:54 +0000 Subject: [PATCH] * I2PTunnel standard and IRC clients: - Allow host:port targets; set defaults in i2ptunnel.config (ticket #1066) - Don't fail start if hostname is unresolvable; retry at connect time (ticket #946) - Output IRC message on connect fail - Update target list on-the-fly when configuration changes --- .../net/i2p/i2ptunnel/I2PTunnelClient.java | 111 ++++++++++++---- .../i2p/i2ptunnel/I2PTunnelClientBase.java | 19 ++- .../net/i2p/i2ptunnel/I2PTunnelIRCClient.java | 120 ++++++++++++------ .../net/i2p/i2ptunnel/TunnelController.java | 9 +- apps/i2ptunnel/jsp/editClient.jsp | 6 +- apps/i2ptunnel/jsp/wizard.jsp | 6 +- .../client/streaming/I2PSocketAddress.java | 55 +++++++- history.txt | 5 + installer/resources/i2ptunnel.config | 8 +- .../src/net/i2p/router/RouterVersion.java | 2 +- 10 files changed, 265 insertions(+), 76 deletions(-) diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClient.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClient.java index fa3bfb94e..e99b9a1b6 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClient.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClient.java @@ -4,24 +4,35 @@ package net.i2p.i2ptunnel; import java.net.Socket; +import java.net.UnknownHostException; import java.util.ArrayList; import java.util.List; +import java.util.Properties; import java.util.StringTokenizer; import net.i2p.client.streaming.I2PSocket; +import net.i2p.client.streaming.I2PSocketAddress; import net.i2p.data.Destination; import net.i2p.util.EventDispatcher; import net.i2p.util.Log; public class I2PTunnelClient extends I2PTunnelClientBase { - /** list of Destination objects that we point at */ + /** + * list of Destination objects that we point at + * @deprecated why protected? Is anybody using out-of-tree? Protected from the beginning (2004) + */ protected List dests; + + /** + * replacement for dests + */ + private final List _addrs; private static final long DEFAULT_READ_TIMEOUT = 5*60*1000; // -1 protected long readTimeout = DEFAULT_READ_TIMEOUT; /** - * @param destinations comma delimited list of peers we target + * @param destinations peers we target, comma- or space-separated. Since 0.9.9, each dest may be appended with :port * @throws IllegalArgumentException if the I2PTunnel does not contain * valid config to contact the router */ @@ -32,30 +43,21 @@ public class I2PTunnelClient extends I2PTunnelClientBase { "Standard client on " + tunnel.listenHost + ':' + localPort, tunnel, pkf); + _addrs = new ArrayList(1); if (waitEventValue("openBaseClientResult").equals("error")) { notifyEvent("openClientResult", "error"); return; } - StringTokenizer tok = new StringTokenizer(destinations, ", "); dests = new ArrayList(1); - while (tok.hasMoreTokens()) { - String destination = tok.nextToken(); - Destination destN = _context.namingService().lookup(destination); - if (destN == null) - l.log("Could not resolve " + destination); - else - dests.add(destN); - } + buildAddresses(destinations); - if (dests.isEmpty()) { + if (_addrs.isEmpty()) { l.log("No valid target destinations found"); notifyEvent("openClientResult", "error"); // Nothing is listening for the above event, so it's useless // Maybe figure out where to put a waitEventValue("openClientResult") ?? // In the meantime, let's do this the easy way - // Note that b32 dests will often not be resolvable at instantiation time; - // a delayed resolution system would be even better. // Don't close() here, because it does a removeSession() and then // TunnelController can't acquire() it to release() it. @@ -72,14 +74,53 @@ public class I2PTunnelClient extends I2PTunnelClientBase { notifyEvent("openClientResult", "ok"); } + /** @since 0.9.9 moved from constructor */ + private void buildAddresses(String destinations) { + if (destinations == null) + return; + StringTokenizer tok = new StringTokenizer(destinations, ", "); + synchronized(_addrs) { + _addrs.clear(); + while (tok.hasMoreTokens()) { + String destination = tok.nextToken(); + try { + // Try to resolve here but only log if it doesn't. + // Note that b32 _addrs will often not be resolvable at instantiation time. + // We will try again to resolve in clientConnectionRun() + I2PSocketAddress addr = new I2PSocketAddress(destination); + _addrs.add(addr); + if (addr.isUnresolved()) { + String name = addr.getHostName(); + if (name.length() == 60 && name.endsWith(".b32.i2p")) + l.log("Warning - Could not resolve " + name + + ", perhaps it is not up, will retry when connecting."); + else + l.log("Warning - Could not resolve " + name + + ", you must add it to your address book for it to work."); + } else { + dests.add(addr.getAddress()); + } + } catch (IllegalArgumentException iae) { + l.log("Bad destination " + destination + " - " + iae); + } + } + } + } + public void setReadTimeout(long ms) { readTimeout = ms; } public long getReadTimeout() { return readTimeout; } protected void clientConnectionRun(Socket s) { - Destination destN = pickDestination(); I2PSocket i2ps = null; try { - i2ps = createI2PSocket(destN); + I2PSocketAddress addr = pickDestination(); + if (addr == null) + throw new UnknownHostException("No valid destination configured"); + Destination clientDest = addr.getAddress(); + if (clientDest == null) + throw new UnknownHostException("Could not resolve " + addr.getHostName()); + int port = addr.getPort(); + i2ps = createI2PSocket(clientDest, port); i2ps.setReadTimeout(readTimeout); new I2PTunnelRunner(s, i2ps, sockLock, null, mySockets); } catch (Exception ex) { @@ -95,16 +136,34 @@ public class I2PTunnelClient extends I2PTunnelClientBase { } } - private final Destination pickDestination() { - int size = dests.size(); - if (size <= 0) { - if (_log.shouldLog(Log.ERROR)) - _log.error("No client targets?!"); - return null; + private final I2PSocketAddress pickDestination() { + synchronized(_addrs) { + int size = _addrs.size(); + if (size <= 0) { + if (_log.shouldLog(Log.ERROR)) + _log.error("No client targets?!"); + return null; + } + if (size == 1) // skip the rand in the most common case + return _addrs.get(0); + int index = _context.random().nextInt(size); + return _addrs.get(index); } - if (size == 1) // skip the rand in the most common case - return dests.get(0); - int index = _context.random().nextInt(size); - return dests.get(index); + } + + /** + * Update the dests then call super. + * + * @since 0.9.9 + */ + @Override + public void optionsUpdated(I2PTunnel tunnel) { + if (getTunnel() != tunnel) + return; + Properties props = tunnel.getClientOptions(); + // see TunnelController.setSessionOptions() + String targets = props.getProperty("targetDestination"); + buildAddresses(targets); + super.optionsUpdated(tunnel); } } diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClientBase.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClientBase.java index 0f52c8fbb..793f19ee6 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClientBase.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClientBase.java @@ -521,8 +521,25 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna * @return a new I2PSocket */ public I2PSocket createI2PSocket(Destination dest) throws I2PException, ConnectException, NoRouteToHostException, InterruptedIOException { + return createI2PSocket(dest, 0); + } + + /** + * Create a new I2PSocket towards to the specified destination, + * adding it to the list of connections actually managed by this + * tunnel. + * + * @param dest The destination to connect to + * @param port The destination port to connect to 0 - 65535 + * @return a new I2PSocket + * @since 0.9.9 + */ + public I2PSocket createI2PSocket(Destination dest, int port) + throws I2PException, ConnectException, NoRouteToHostException, InterruptedIOException { verifySocketManager(); - return createI2PSocket(dest, getDefaultOptions()); + I2PSocketOptions opts = getDefaultOptions(); + opts.setPort(port); + return createI2PSocket(dest, opts); } /** diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCClient.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCClient.java index fdba9af63..41e3069fd 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCClient.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCClient.java @@ -1,13 +1,18 @@ package net.i2p.i2ptunnel; +import java.io.IOException; import java.net.Socket; +import java.net.UnknownHostException; import java.util.ArrayList; import java.util.List; +import java.util.Properties; import java.util.StringTokenizer; import net.i2p.I2PException; import net.i2p.client.streaming.I2PSocket; +import net.i2p.client.streaming.I2PSocketAddress; import net.i2p.data.Base32; +import net.i2p.data.DataHelper; import net.i2p.data.Destination; import net.i2p.i2ptunnel.irc.DCCClientManager; import net.i2p.i2ptunnel.irc.DCCHelper; @@ -28,7 +33,7 @@ public class I2PTunnelIRCClient extends I2PTunnelClientBase { private static volatile long __clientId = 0; /** list of Destination objects that we point at */ - protected List dests; + private final List _addrs; private static final long DEFAULT_READ_TIMEOUT = 5*60*1000; // -1 protected long readTimeout = DEFAULT_READ_TIMEOUT; private final boolean _dccEnabled; @@ -41,6 +46,7 @@ public class I2PTunnelIRCClient extends I2PTunnelClientBase { public static final String PROP_DCC = "i2ptunnel.ircclient.enableDCC"; /** + * @param destinations peers we target, comma- or space-separated. Since 0.9.9, each dest may be appended with :port * @throws IllegalArgumentException if the I2PTunnel does not contain * valid config to contact the router */ @@ -57,25 +63,15 @@ public class I2PTunnelIRCClient extends I2PTunnelClientBase { notifyThis, "IRC Client on " + tunnel.listenHost + ':' + localPort + " #" + (++__clientId), tunnel, pkf); - StringTokenizer tok = new StringTokenizer(destinations, ", "); - dests = new ArrayList(2); - while (tok.hasMoreTokens()) { - String destination = tok.nextToken(); - Destination destN = _context.namingService().lookup(destination); - if (destN == null) - l.log("Could not resolve " + destination); - else - dests.add(destN); - } + _addrs = new ArrayList(4); + buildAddresses(destinations); - if (dests.isEmpty()) { + if (_addrs.isEmpty()) { l.log("No target destinations found"); notifyEvent("openClientResult", "error"); // Nothing is listening for the above event, so it's useless // Maybe figure out where to put a waitEventValue("openClientResult") ?? // In the meantime, let's do this the easy way - // Note that b32 dests will often not be resolvable at instantiation time; - // a delayed resolution system would be even better. // Don't close() here, because it does a removeSession() and then // TunnelController can't acquire() it to release() it. @@ -95,14 +91,51 @@ public class I2PTunnelIRCClient extends I2PTunnelClientBase { notifyEvent("openIRCClientResult", "ok"); } + /** @since 0.9.9 moved from constructor */ + private void buildAddresses(String destinations) { + if (destinations == null) + return; + StringTokenizer tok = new StringTokenizer(destinations, ", "); + synchronized(_addrs) { + _addrs.clear(); + while (tok.hasMoreTokens()) { + String destination = tok.nextToken(); + try { + // Try to resolve here but only log if it doesn't. + // Note that b32 _addrs will often not be resolvable at instantiation time. + // We will try again to resolve in clientConnectionRun() + I2PSocketAddress addr = new I2PSocketAddress(destination); + _addrs.add(addr); + if (addr.isUnresolved()) { + String name = addr.getHostName(); + if (name.length() == 60 && name.endsWith(".b32.i2p")) + l.log("Warning - Could not resolve " + name + + ", perhaps it is not up, will retry when connecting."); + else + l.log("Warning - Could not resolve " + name + + ", you must add it to your address book for it to work."); + } + } catch (IllegalArgumentException iae) { + l.log("Bad destination " + destination + " - " + iae); + } + } + } + } + protected void clientConnectionRun(Socket s) { if (_log.shouldLog(Log.INFO)) _log.info("New connection local addr is: " + s.getLocalAddress() + " from: " + s.getInetAddress()); - Destination clientDest = pickDestination(); I2PSocket i2ps = null; + I2PSocketAddress addr = pickDestination(); try { - i2ps = createI2PSocket(clientDest); + if (addr == null) + throw new UnknownHostException("No valid destination configured"); + Destination clientDest = addr.getAddress(); + if (clientDest == null) + throw new UnknownHostException("Could not resolve " + addr.getHostName()); + int port = addr.getPort(); + i2ps = createI2PSocket(clientDest, port); i2ps.setReadTimeout(readTimeout); StringBuffer expectedPong = new StringBuffer(); DCCHelper dcc = _dccEnabled ? new DCC(s.getLocalAddress().getAddress()) : null; @@ -110,21 +143,18 @@ public class I2PTunnelIRCClient extends I2PTunnelClientBase { in.start(); Thread out = new I2PAppThread(new IrcOutboundFilter(s,i2ps, expectedPong, _log, dcc), "IRC Client " + __clientId + " out", true); out.start(); - } catch (I2PException ex) { - if (_log.shouldLog(Log.ERROR)) - _log.error("Error connecting", ex); - //l.log("Error connecting: " + ex.getMessage()); - closeSocket(s); - if (i2ps != null) { - synchronized (sockLock) { - mySockets.remove(sockLock); - } - } } catch (Exception ex) { // generally NoRouteToHostException if (_log.shouldLog(Log.WARN)) _log.warn("Error connecting", ex); //l.log("Error connecting: " + ex.getMessage()); + try { + // Send a response so the user doesn't just see a disconnect + // and blame his router or the network. + String name = addr != null ? addr.getHostName() : "undefined"; + String msg = ":" + name + " 499 you :" + ex + "\r\n"; + s.getOutputStream().write(DataHelper.getUTF8(msg)); + } catch (IOException ioe) {} closeSocket(s); if (i2ps != null) { synchronized (sockLock) { @@ -135,17 +165,35 @@ public class I2PTunnelIRCClient extends I2PTunnelClientBase { } - private final Destination pickDestination() { - int size = dests.size(); - if (size <= 0) { - if (_log.shouldLog(Log.ERROR)) - _log.error("No client targets?!"); - return null; + private final I2PSocketAddress pickDestination() { + synchronized(_addrs) { + int size = _addrs.size(); + if (size <= 0) { + if (_log.shouldLog(Log.ERROR)) + _log.error("No client targets?!"); + return null; + } + if (size == 1) // skip the rand in the most common case + return _addrs.get(0); + int index = _context.random().nextInt(size); + return _addrs.get(index); } - if (size == 1) // skip the rand in the most common case - return dests.get(0); - int index = _context.random().nextInt(size); - return dests.get(index); + } + + /** + * Update the dests then call super. + * + * @since 0.9.9 + */ + @Override + public void optionsUpdated(I2PTunnel tunnel) { + if (getTunnel() != tunnel) + return; + Properties props = tunnel.getClientOptions(); + // see TunnelController.setSessionOptions() + String targets = props.getProperty("targetDestination"); + buildAddresses(targets); + super.optionsUpdated(tunnel); } @Override diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java index f5fefdbd0..5937a9512 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java @@ -424,7 +424,14 @@ public class TunnelController implements Logging { } private void setSessionOptions() { - _tunnel.setClientOptions(getClientOptionProps()); + Properties opts = getClientOptionProps(); + // targetDestination does NOT start with "option.", but we still want + // to allow a change on the fly, so we pass it through this way, + // as a "spoofed" option. Since 0.9.9. + String target = getTargetDestination(); + if (target != null) + opts.setProperty("targetDestination", target); + _tunnel.setClientOptions(opts); } private void setI2CPOptions() { diff --git a/apps/i2ptunnel/jsp/editClient.jsp b/apps/i2ptunnel/jsp/editClient.jsp index b76b4cbb7..07b196572 100644 --- a/apps/i2ptunnel/jsp/editClient.jsp +++ b/apps/i2ptunnel/jsp/editClient.jsp @@ -165,7 +165,11 @@ input.default { width: 1px; height: 1px; visibility: hidden; } %> - (<%=intl._("name or destination")%>; <%=intl._("b32 not recommended")%>) + (<%=intl._("name, name:port, or destination")%> + <% if ("streamrclient".equals(tunnelType)) { /* deferred resolution unimplemented in streamr client */ %> + - <%=intl._("b32 not recommended")%> + <% } %> ) + <% } %> <% if (!"streamrclient".equals(tunnelType)) { %> diff --git a/apps/i2ptunnel/jsp/wizard.jsp b/apps/i2ptunnel/jsp/wizard.jsp index 4fdfc34b3..b52f6c537 100644 --- a/apps/i2ptunnel/jsp/wizard.jsp +++ b/apps/i2ptunnel/jsp/wizard.jsp @@ -268,7 +268,11 @@ <%=intl._("Tunnel Destination")%>(T): " class="freetext" /> - (<%=intl._("name or destination")%>; <%=intl._("b32 not recommended")%>) + (<%=intl._("name, name:port, or destination")%> + <% if ("streamrclient".equals(tunnelType)) { /* deferred resolution unimplemented in streamr client */ %> + - <%=intl._("b32 not recommended")%> + <% } %> ) + <% } else { %>" /><% diff --git a/apps/ministreaming/java/src/net/i2p/client/streaming/I2PSocketAddress.java b/apps/ministreaming/java/src/net/i2p/client/streaming/I2PSocketAddress.java index 4ba55bdb1..24bdefa10 100644 --- a/apps/ministreaming/java/src/net/i2p/client/streaming/I2PSocketAddress.java +++ b/apps/ministreaming/java/src/net/i2p/client/streaming/I2PSocketAddress.java @@ -11,22 +11,52 @@ import net.i2p.data.DataHelper; * Ports are not widely used in I2P, in most cases the port will be zero. * See InetSocketAddress for javadocs. * - * Warning, this interface and implementation is preliminary and subject to change without notice. - * * @since 0.9.1 */ public class I2PSocketAddress extends SocketAddress { private final int _port; - private final Destination _dest; + private Destination _dest; private final String _host; - // no constructor for port-only "wildcard" address + /** + * Convenience constructor that parses host:port. + * + * Does a naming service lookup to resolve the dest. + * May take several seconds for b32. + * @param host hostname or b64 dest or b32, may have :port appended + * @throws IllegalArgumentException for port < 0 or port > 65535 or invalid port + * @since 0.9.9 + */ + public I2PSocketAddress(String host) { + int port = 0; + int colon = host.indexOf(":"); + if (colon > 0) { + try { + port = Integer.parseInt(host.substring(colon + 1)); + host = host.substring(0, colon); + if (port < 0 || port > 65535) + throw new IllegalArgumentException("bad port " + port); + } catch (IndexOutOfBoundsException ioobe) { + throw new IllegalArgumentException("bad port " + host); + } catch (NumberFormatException nfe) { + throw new IllegalArgumentException("bad port " + host); + } + } + _port = port; + _dest = I2PAppContext.getGlobalContext().namingService().lookup(host); + _host = host; + } /** * Does not do a reverse lookup. Host will be null. + * @throws IllegalArgumentException for port < 0 or port > 65535 */ public I2PSocketAddress(Destination dest, int port) { + if (dest == null) + throw new NullPointerException(); + if (port < 0 || port > 65535) + throw new IllegalArgumentException("bad port " + port); _port = port; _dest = dest; _host = null; @@ -35,19 +65,27 @@ public class I2PSocketAddress extends SocketAddress { /** * Does a naming service lookup to resolve the dest. * May take several seconds for b32. + * @throws IllegalArgumentException for port < 0 or port > 65535 */ public I2PSocketAddress(String host, int port) { + if (port < 0 || port > 65535) + throw new IllegalArgumentException("bad port " + port); _port = port; _dest = I2PAppContext.getGlobalContext().namingService().lookup(host); _host = host; } + /** + * @throws IllegalArgumentException for port < 0 or port > 65535 + */ public static I2PSocketAddress createUnresolved(String host, int port) { return new I2PSocketAddress(port, host); } /** unresolved */ private I2PSocketAddress(int port, String host) { + if (port < 0 || port > 65535) + throw new IllegalArgumentException("bad port " + port); _port = port; _dest = null; _host = host; @@ -57,7 +95,14 @@ public class I2PSocketAddress extends SocketAddress { return _port; } - public Destination getAddress() { + /** + * Does a naming service lookup to resolve the dest if this was created unresolved + * or if the resolution failed in the constructor. + * If unresolved, this may take several seconds for b32. + */ + public synchronized Destination getAddress() { + if (_dest == null) + _dest = I2PAppContext.getGlobalContext().namingService().lookup(_host); return _dest; } diff --git a/history.txt b/history.txt index 8dba45018..1f6c617e1 100644 --- a/history.txt +++ b/history.txt @@ -1,4 +1,9 @@ 2013-10-23 zzz + * I2PTunnel standard and IRC clients: + - Allow host:port targets; set defaults in i2ptunnel.config (ticket #1066) + - Don't fail start if hostname is unresolvable; retry at connect time (ticket #946) + - Output IRC message on connect exception + - Update target list on-the-fly when configuration changes * NetDB: - Increase RI publish interval to reduce the connection load on ffs - Save RI-last-published time; check it before publishing diff --git a/installer/resources/i2ptunnel.config b/installer/resources/i2ptunnel.config index 2e7a20cda..c0eb5aa00 100644 --- a/installer/resources/i2ptunnel.config +++ b/installer/resources/i2ptunnel.config @@ -35,7 +35,7 @@ tunnel.1.type=ircclient tunnel.1.sharedClient=false tunnel.1.interface=127.0.0.1 tunnel.1.listenPort=6668 -tunnel.1.targetDestination=irc.postman.i2p,irc.freshcoffee.i2p,irc.echelon.i2p +tunnel.1.targetDestination=irc.postman.i2p:6667,irc.freshcoffee.i2p:6667,irc.echelon.i2p:6667 tunnel.1.i2cpHost=127.0.0.1 tunnel.1.i2cpPort=7654 tunnel.1.option.inbound.nickname=Irc2P @@ -63,7 +63,7 @@ tunnel.2.type=client tunnel.2.sharedClient=true tunnel.2.interface=127.0.0.1 tunnel.2.listenPort=8998 -tunnel.2.targetDestination=mtn.i2p2.i2p +tunnel.2.targetDestination=mtn.i2p2.i2p:4691 tunnel.2.i2cpHost=127.0.0.1 tunnel.2.i2cpPort=7654 tunnel.2.option.inbound.nickname=shared clients @@ -113,7 +113,7 @@ tunnel.4.option.inbound.lengthVariance=0 tunnel.4.option.outbound.length=3 tunnel.4.option.outbound.lengthVariance=0 tunnel.4.startOnLoad=true -tunnel.4.targetDestination=smtp.postman.i2p +tunnel.4.targetDestination=smtp.postman.i2p:25 tunnel.4.type=client tunnel.4.sharedClient=true @@ -135,7 +135,7 @@ tunnel.5.option.inbound.lengthVariance=0 tunnel.5.option.outbound.length=3 tunnel.5.option.outbound.lengthVariance=0 tunnel.5.startOnLoad=true -tunnel.5.targetDestination=pop.postman.i2p +tunnel.5.targetDestination=pop.postman.i2p:110 tunnel.5.type=client tunnel.5.sharedClient=true diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java index fecba78d6..a6204817e 100644 --- a/router/java/src/net/i2p/router/RouterVersion.java +++ b/router/java/src/net/i2p/router/RouterVersion.java @@ -18,7 +18,7 @@ public class RouterVersion { /** deprecated */ public final static String ID = "Monotone"; public final static String VERSION = CoreVersion.VERSION; - public final static long BUILD = 7; + public final static long BUILD = 8; /** for example "-test" */ public final static String EXTRA = "";