diff --git a/LICENSE.txt b/LICENSE.txt index b6213a2616..fdc899aa39 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -207,7 +207,7 @@ Applications: FatCow icons: See licenses/LICENSE-FatCowIcons.txt GeoIP Data: - Copyright (c) 2008 MaxMind, Inc. All Rights Reserved. + This product includes GeoLite data created by MaxMind, available from http://www.maxmind.com/ See licenses/LICENSE-GeoIP.txt Router Console and I2PSnark themes: diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigNetHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigNetHandler.java index 67c47fbb7f..827cf47f61 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigNetHandler.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigNetHandler.java @@ -7,7 +7,7 @@ import java.util.Map; import net.i2p.router.Router; import net.i2p.router.transport.FIFOBandwidthRefiller; -import net.i2p.router.transport.TransportImpl; +import net.i2p.router.transport.TransportUtil; import net.i2p.router.transport.TransportManager; import net.i2p.router.transport.udp.UDPTransport; import net.i2p.router.web.ConfigServiceHandler; @@ -370,7 +370,8 @@ public class ConfigNetHandler extends FormHandler { addFormError(_("Invalid address") + ": " + addr); return false; } - boolean rv = TransportImpl.isPubliclyRoutable(iab); + // TODO set IPv6 arg based on configuration? + boolean rv = TransportUtil.isPubliclyRoutable(iab, true); if (!rv) addFormError(_("The hostname or IP {0} is not publicly routable", addr)); return rv; 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 cbf8c859c7..11971fb147 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigNetHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigNetHelper.java @@ -7,7 +7,6 @@ import net.i2p.data.RouterAddress; import net.i2p.router.CommSystemFacade; import net.i2p.router.Router; import net.i2p.router.transport.TransportManager; -import net.i2p.router.transport.udp.UDPAddress; import net.i2p.router.transport.udp.UDPTransport; import net.i2p.util.Addresses; @@ -32,22 +31,15 @@ public class ConfigNetHelper extends HelperBase { return _context.getProperty(PROP_I2NP_NTCP_PORT, ""); } - public String getUdpAddress() { - RouterAddress addr = _context.router().getRouterInfo().getTargetAddress("SSU"); - if (addr == null) - return _("unknown"); - UDPAddress ua = new UDPAddress(addr); - return ua.toString(); - } - + /** @return host or "unknown" */ public String getUdpIP() { RouterAddress addr = _context.router().getRouterInfo().getTargetAddress("SSU"); if (addr == null) return _("unknown"); - UDPAddress ua = new UDPAddress(addr); - if (ua.getHost() == null) + String rv = addr.getHost(); + if (rv == null) return _("unknown"); - return ua.getHost(); + return rv; } /** diff --git a/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java index bff1abdd54..7d75790aed 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java @@ -24,7 +24,7 @@ import net.i2p.router.RouterContext; import net.i2p.router.RouterVersion; import net.i2p.router.TunnelPoolSettings; import net.i2p.router.networkdb.kademlia.FloodfillNetworkDatabaseFacade; -import net.i2p.router.transport.ntcp.NTCPAddress; +import net.i2p.router.transport.TransportUtil; import net.i2p.stat.Rate; import net.i2p.stat.RateStat; import net.i2p.util.PortMapper; @@ -159,7 +159,8 @@ public class SummaryHelper extends HelperBase { switch (status) { case CommSystemFacade.STATUS_OK: RouterAddress ra = routerInfo.getTargetAddress("NTCP"); - if (ra == null || (new NTCPAddress(ra)).isPubliclyRoutable()) + // TODO set IPv6 arg based on configuration? + if (ra == null || TransportUtil.isPubliclyRoutable(ra.getIP(), true)) return _("OK"); return _("ERR-Private TCP Address"); case CommSystemFacade.STATUS_DIFFERENT: diff --git a/build.xml b/build.xml index 9c27cd37f2..443e6194bc 100644 --- a/build.xml +++ b/build.xml @@ -1121,7 +1121,9 @@ - + + + @@ -1137,6 +1139,7 @@ + @@ -1292,20 +1295,29 @@ - + - - + + + + + + + + + + + diff --git a/core/java/src/net/i2p/data/RouterAddress.java b/core/java/src/net/i2p/data/RouterAddress.java index c21f0f5bf1..0b6fde6e0f 100644 --- a/core/java/src/net/i2p/data/RouterAddress.java +++ b/core/java/src/net/i2p/data/RouterAddress.java @@ -38,7 +38,7 @@ import net.i2p.util.OrderedProperties; * @author jrandom */ public class RouterAddress extends DataStructureImpl { - private int _cost; + private short _cost; //private Date _expiration; private String _transportStyle; private final Properties _options; @@ -50,16 +50,30 @@ public class RouterAddress extends DataStructureImpl { public static final String PROP_PORT = "port"; public RouterAddress() { - _cost = -1; _options = new OrderedProperties(); } + /** + * For efficiency when created by a Transport. + * @param options not copied; do not reuse or modify + * @param cost 0-255 + * @since IPv6 + */ + public RouterAddress(String style, OrderedProperties options, int cost) { + _transportStyle = style; + _options = options; + if (cost < 0 || cost > 255) + throw new IllegalArgumentException(); + _cost = (short) cost; + } + /** * Retrieve the weighted cost of this address, relative to other methods of * contacting this router. The value 0 means free and 255 means really expensive. * No value above 255 is allowed. * * Unused before 0.7.12 + * @return 0-255 */ public int getCost() { return _cost; @@ -67,12 +81,18 @@ public class RouterAddress extends DataStructureImpl { /** * Configure the weighted cost of using the address. - * No value above 255 is allowed. + * No value negative or above 255 is allowed. + * + * WARNING - do not change cost on a published address or it will break the RI sig. + * There is no check here. + * Rarely used, use 3-arg constructor. * * NTCP is set to 10 and SSU to 5 by default, unused before 0.7.12 */ public void setCost(int cost) { - _cost = cost; + if (cost < 0 || cost > 255) + throw new IllegalArgumentException(); + _cost = (short) cost; } /** @@ -113,6 +133,7 @@ public class RouterAddress extends DataStructureImpl { * Configure the type of transport that must be used to communicate on this address * * @throws IllegalStateException if was already set + * @deprecated unused, use 3-arg constructor */ public void setTransportStyle(String transportStyle) { if (_transportStyle != null) @@ -152,6 +173,7 @@ public class RouterAddress extends DataStructureImpl { * Makes a copy. * @param options non-null * @throws IllegalStateException if was already set + * @deprecated unused, use 3-arg constructor */ public void setOptions(Properties options) { if (!_options.isEmpty()) @@ -171,7 +193,7 @@ public class RouterAddress extends DataStructureImpl { if (_ip != null) return _ip; byte[] rv = null; - String host = _options.getProperty(PROP_HOST); + String host = getHost(); if (host != null) { rv = Addresses.getIP(host); if (rv != null && @@ -183,6 +205,17 @@ public class RouterAddress extends DataStructureImpl { return rv; } + /** + * Convenience, same as getOption("host"). + * Does no parsing, so faster than getIP(). + * + * @return host string or null + * @since IPv6 + */ + public String getHost() { + return _options.getProperty(PROP_HOST); + } + /** * Caching version of Integer.parseInt(getOption("port")) * Caches valid ports 1-65535 only. @@ -212,7 +245,7 @@ public class RouterAddress extends DataStructureImpl { public void readBytes(InputStream in) throws DataFormatException, IOException { if (_transportStyle != null) throw new IllegalStateException(); - _cost = (int) DataHelper.readLong(in, 1); + _cost = (short) DataHelper.readLong(in, 1); //_expiration = DataHelper.readDate(in); DataHelper.readDate(in); _transportStyle = DataHelper.readString(in); @@ -229,8 +262,8 @@ public class RouterAddress extends DataStructureImpl { * readin and the signature will fail. */ public void writeBytes(OutputStream out) throws DataFormatException, IOException { - if ((_cost < 0) || (_transportStyle == null)) - throw new DataFormatException("Not enough data to write a router address"); + if (_transportStyle == null) + throw new DataFormatException("uninitialized"); DataHelper.writeLong(out, 1, _cost); //DataHelper.writeDate(out, _expiration); DataHelper.writeDate(out, null); @@ -238,28 +271,44 @@ public class RouterAddress extends DataStructureImpl { DataHelper.writeProperties(out, _options); } + /** + * Transport, host, and port only. + * Never look at cost or other properties. + */ @Override public boolean equals(Object object) { if (object == this) return true; if ((object == null) || !(object instanceof RouterAddress)) return false; RouterAddress addr = (RouterAddress) object; - // let's keep this fast as we are putting an address into the RouterInfo set frequently return - _cost == addr._cost && + getPort() == addr.getPort() && + DataHelper.eq(getHost(), addr.getHost()) && DataHelper.eq(_transportStyle, addr._transportStyle); //DataHelper.eq(_options, addr._options) && //DataHelper.eq(_expiration, addr._expiration); } + /** + * Everything, including Transport, host, port, options, and cost + * @param addr may be null + * @since IPv6 + */ + public boolean deepEquals(RouterAddress addr) { + return + equals(addr) && + _cost == addr._cost && + _options.equals(addr._options); + } + /** * Just use a few items for speed (expiration is always null). + * Never look at cost or other properties. */ @Override public int hashCode() { return DataHelper.hashCode(_transportStyle) ^ DataHelper.hashCode(getIP()) ^ - getPort() ^ - _cost; + getPort(); } /** @@ -271,10 +320,10 @@ public class RouterAddress extends DataStructureImpl { public String toString() { StringBuilder buf = new StringBuilder(128); buf.append("[RouterAddress: "); - buf.append("\n\tTransportStyle: ").append(_transportStyle); + buf.append("\n\tType: ").append(_transportStyle); buf.append("\n\tCost: ").append(_cost); //buf.append("\n\tExpiration: ").append(_expiration); - buf.append("\n\tOptions: #: ").append(_options.size()); + buf.append("\n\tOptions (").append(_options.size()).append("):"); for (Map.Entry e : _options.entrySet()) { String key = (String) e.getKey(); String val = (String) e.getValue(); diff --git a/core/java/src/net/i2p/data/RouterInfo.java b/core/java/src/net/i2p/data/RouterInfo.java index 264495fede..b6234d7ad8 100644 --- a/core/java/src/net/i2p/data/RouterInfo.java +++ b/core/java/src/net/i2p/data/RouterInfo.java @@ -61,7 +61,7 @@ public class RouterInfo extends DatabaseEntry { private final Properties _options; private volatile boolean _validated; private volatile boolean _isValid; - private volatile String _stringified; + //private volatile String _stringified; private volatile byte _byteified[]; private volatile int _hashCode; private volatile boolean _hashCodeInitialized; @@ -613,30 +613,34 @@ public class RouterInfo extends DatabaseEntry { @Override public String toString() { - if (_stringified != null) return _stringified; - StringBuilder buf = new StringBuilder(5*1024); + //if (_stringified != null) return _stringified; + StringBuilder buf = new StringBuilder(1024); buf.append("[RouterInfo: "); buf.append("\n\tIdentity: ").append(_identity); buf.append("\n\tSignature: ").append(_signature); - buf.append("\n\tPublished on: ").append(new Date(_published)); - buf.append("\n\tAddresses: #: ").append(_addresses.size()); - for (RouterAddress addr : _addresses) { - buf.append("\n\t\tAddress: ").append(addr); + buf.append("\n\tPublished: ").append(new Date(_published)); + if (_peers != null) { + buf.append("\n\tPeers (").append(_peers.size()).append("):"); + for (Hash hash : _peers) { + buf.append("\n\t\tPeer hash: ").append(hash); + } } - Set peers = getPeers(); - buf.append("\n\tPeers: #: ").append(peers.size()); - for (Hash hash : peers) { - buf.append("\n\t\tPeer hash: ").append(hash); - } - buf.append("\n\tOptions: #: ").append(_options.size()); + buf.append("\n\tOptions (").append(_options.size()).append("):"); for (Map.Entry e : _options.entrySet()) { String key = (String) e.getKey(); String val = (String) e.getValue(); buf.append("\n\t\t[").append(key).append("] = [").append(val).append("]"); } + if (!_addresses.isEmpty()) { + buf.append("\n\tAddresses (").append(_addresses.size()).append("):"); + for (RouterAddress addr : _addresses) { + buf.append("\n\t").append(addr); + } + } buf.append("]"); - _stringified = buf.toString(); - return _stringified; + String rv = buf.toString(); + //_stringified = rv; + return rv; } /** diff --git a/core/java/src/net/i2p/util/Addresses.java b/core/java/src/net/i2p/util/Addresses.java index e54f93a02a..fec88eda88 100644 --- a/core/java/src/net/i2p/util/Addresses.java +++ b/core/java/src/net/i2p/util/Addresses.java @@ -34,7 +34,7 @@ public abstract class Addresses { return !getAddresses(true, false, false).isEmpty(); } - /** @return the first non-local address it finds, or null */ + /** @return the first non-local address IPv4 address it finds, or null */ public static String getAnyAddress() { SortedSet a = getAddresses(); if (!a.isEmpty()) @@ -95,7 +95,7 @@ public abstract class Addresses { haveIPv6 = true; if (shouldInclude(allMyIps[i], includeSiteLocal, includeLoopbackAndWildcard, includeIPv6)) - rv.add(allMyIps[i].getHostAddress()); + rv.add(stripScope(allMyIps[i].getHostAddress())); } } } catch (UnknownHostException e) {} @@ -113,7 +113,7 @@ public abstract class Addresses { haveIPv6 = true; if (shouldInclude(addr, includeSiteLocal, includeLoopbackAndWildcard, includeIPv6)) - rv.add(addr.getHostAddress()); + rv.add(stripScope(addr.getHostAddress())); } } } @@ -128,6 +128,17 @@ public abstract class Addresses { return rv; } + /** + * Strip the trailing "%nn" from Inet6Address.getHostAddress() + * @since IPv6 + */ + private static String stripScope(String ip) { + int pct = ip.indexOf("%"); + if (pct > 0) + ip = ip.substring(0, pct); + return ip; + } + private static boolean shouldInclude(InetAddress ia, boolean includeSiteLocal, boolean includeLoopbackAndWildcard, boolean includeIPv6) { return diff --git a/installer/resources/blocklist.txt b/installer/resources/blocklist.txt index 9f9dde18fb..02440c58ee 100644 --- a/installer/resources/blocklist.txt +++ b/installer/resources/blocklist.txt @@ -46,6 +46,7 @@ Friend of the Chinese Floodfill Flooder:159.226.40.3 The Team Cymru Bogon List v6.8 03 FEB 2011:172.16.0.0/12 The Team Cymru Bogon List v6.8 03 FEB 2011:192.0.0.0/24 The Team Cymru Bogon List v6.8 03 FEB 2011:192.0.2.0/24 +6to4 Anycast:192.88.99.0/24 The Team Cymru Bogon List v6.8 03 FEB 2011:192.168.0.0/16 The Team Cymru Bogon List v6.8 03 FEB 2011:198.18.0.0/15 The Team Cymru Bogon List v6.8 03 FEB 2011:198.51.100.0/24 diff --git a/installer/resources/geoipv6-extras.csv b/installer/resources/geoipv6-extras.csv new file mode 100644 index 0000000000..414882684b --- /dev/null +++ b/installer/resources/geoipv6-extras.csv @@ -0,0 +1,9 @@ +# Local geoIPv6 additions +# Format: from IP,to IP,,,country code[,"country name"] +#### +# common tunnel brokers +2001:5c0:1000:a::,2001:5c0:1000:a::,,,X1,"Freenet6 anonymous" +2001:5c0:1000:b::,2001:5c0:1000:b::,,,X2,"Freenet6 authenticated" +2001:5c0:1100::,2001:5c0:11ff:ffff:ffff:ffff:ffff:ffff,,,X3,"Freenet6 delegated" +# other interesting addresses +2002::,2002:ffff:ffff:ffff:ffff:ffff:ffff:ffff,,,X4,"IPv4 compatibility" diff --git a/installer/resources/geoipv6.dat.gz b/installer/resources/geoipv6.dat.gz new file mode 100644 index 0000000000..fe9273dd33 Binary files /dev/null and b/installer/resources/geoipv6.dat.gz differ diff --git a/installer/resources/makegeoipv6.sh b/installer/resources/makegeoipv6.sh new file mode 100755 index 0000000000..390e4ed4b6 --- /dev/null +++ b/installer/resources/makegeoipv6.sh @@ -0,0 +1,19 @@ +#!/bin/sh +# +# Fetch the latest file from Maxmind, merge with +# our additions, and compress. +# + +FILE1=GeoIPv6.csv.gz +FILE2=geoipv6-extras.csv +FILEOUT=geoipv6.dat.gz + +rm -f $FILE1 $FILEOUT +wget http://geolite.maxmind.com/download/geoip/database/$FILE1 +if [ "$?" -ne "0" ] +then + echo 'Cannot fetch' + exit 1 +fi +java -cp ../../build/i2p.jar:../../build/router.jar net.i2p.router.transport.GeoIPv6 $FILE1 $FILE2 $FILEOUT +exit $? diff --git a/licenses/LICENSE-GeoIP.txt b/licenses/LICENSE-GeoIP.txt index 6f5b0713d0..0c91833ef8 100644 --- a/licenses/LICENSE-GeoIP.txt +++ b/licenses/LICENSE-GeoIP.txt @@ -1,32 +1,8 @@ -OPEN DATA LICENSE (GeoLite Country and GeoLite City databases) +The GeoLite databases are distributed under the +Creative Commons Attribution-ShareAlike 3.0 Unported License +http://creativecommons.org/licenses/by-sa/3.0/ . +The attribution requirement may be met by including the following in +all advertising and documentation mentioning features of or use of this database: -Copyright (c) 2008 MaxMind, Inc. All Rights Reserved. - -All advertising materials and documentation mentioning features or use of -this database must display the following acknowledgment: "This product includes GeoLite data created by MaxMind, available from -http://maxmind.com/" - -Redistribution and use with or without modification, are permitted provided -that the following conditions are met: -1. Redistributions must retain the above copyright notice, this list of -conditions and the following disclaimer in the documentation and/or other -materials provided with the distribution. -2. All advertising materials and documentation mentioning features or use of -this database must display the following acknowledgement: -"This product includes GeoLite data created by MaxMind, available from -http://maxmind.com/" -3. "MaxMind" may not be used to endorse or promote products derived from this -database without specific prior written permission. - -THIS DATABASE IS PROVIDED BY MAXMIND, INC ``AS IS'' AND ANY -EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL MAXMIND BE LIABLE FOR ANY -DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -DATABASE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - +http://www.maxmind.com/" diff --git a/router/java/build.xml b/router/java/build.xml index f473abf0cb..46ec22e491 100644 --- a/router/java/build.xml +++ b/router/java/build.xml @@ -258,7 +258,10 @@ + + + diff --git a/router/java/src/net/i2p/router/Blocklist.java b/router/java/src/net/i2p/router/Blocklist.java index 5f8112cc07..3a28bd85a6 100644 --- a/router/java/src/net/i2p/router/Blocklist.java +++ b/router/java/src/net/i2p/router/Blocklist.java @@ -11,10 +11,12 @@ import java.io.File; import java.io.FileInputStream; import java.io.InputStreamReader; import java.io.Writer; +import java.math.BigInteger; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -31,6 +33,7 @@ import net.i2p.data.RouterInfo; import net.i2p.router.networkdb.kademlia.FloodfillNetworkDatabaseFacade; import net.i2p.util.Addresses; import net.i2p.util.ConcurrentHashSet; +import net.i2p.util.LHMCache; import net.i2p.util.Log; import net.i2p.util.Translate; @@ -62,6 +65,8 @@ import net.i2p.util.Translate; * banlist it forever, then go back to the file to get the original * entry so we can add the reason to the banlist text. * + * On-disk blocklist supports IPv4 only. + * In-memory supports both IPv4 and IPv6. */ public class Blocklist { private final Log _log; @@ -72,8 +77,20 @@ public class Blocklist { private Entry _wrapSave; private final Set _inProcess = new HashSet(4); private Map _peerBlocklist = new HashMap(4); + + /** + * Limits of transient (in-memory) blocklists. + * Note that it's impossible to prevent clogging up + * the tables by a determined attacker, esp. on IPv6 + */ + private static final int MAX_IPV4_SINGLES = 256; + private static final int MAX_IPV6_SINGLES = 512; + private final Set _singleIPBlocklist = new ConcurrentHashSet(4); - + private final Map _singleIPv6Blocklist = new LHMCache(MAX_IPV6_SINGLES); + + private static final Object DUMMY = Integer.valueOf(0); + public Blocklist(RouterContext context) { _context = context; _log = context.logManager().getLog(Blocklist.class); @@ -437,6 +454,8 @@ public class Blocklist { * Maintain a simple in-memory single-IP blocklist * This is used for new additions, NOT for the main list * of IP ranges read in from the file. + * + * @param ip IPv4 or IPv6 */ public void add(String ip) { byte[] pib = Addresses.getIP(ip); @@ -448,16 +467,24 @@ public class Blocklist { * Maintain a simple in-memory single-IP blocklist * This is used for new additions, NOT for the main list * of IP ranges read in from the file. + * + * @param ip IPv4 or IPv6 */ public void add(byte ip[]) { - if (ip.length != 4) - return; - if (add(toInt(ip))) - if (_log.shouldLog(Log.WARN)) - _log.warn("Adding IP to blocklist: " + Addresses.toString(ip)); + boolean rv; + if (ip.length == 4) + rv = add(toInt(ip)); + else if (ip.length == 16) + rv = add(new BigInteger(1, ip)); + else + rv = false; + if (rv && _log.shouldLog(Log.WARN)) + _log.warn("Adding IP to blocklist: " + Addresses.toString(ip)); } private boolean add(int ip) { + if (_singleIPBlocklist.size() >= MAX_IPV4_SINGLES) + return false; return _singleIPBlocklist.add(Integer.valueOf(ip)); } @@ -466,20 +493,41 @@ public class Blocklist { } /** - * this tries to not return duplicates - * but I suppose it could. + * @param ip IPv6 non-negative + * @since IPv6 + */ + private boolean add(BigInteger ip) { + synchronized(_singleIPv6Blocklist) { + return _singleIPv6Blocklist.put(ip, DUMMY) == null; + } + } + + /** + * @param ip IPv6 non-negative + * @since IPv6 + */ + private boolean isOnSingleList(BigInteger ip) { + synchronized(_singleIPv6Blocklist) { + return _singleIPv6Blocklist.get(ip) != null; + } + } + + /** + * Will not contain duplicates. */ private List getAddresses(Hash peer) { - List rv = new ArrayList(1); RouterInfo pinfo = _context.netDb().lookupRouterInfoLocally(peer); - if (pinfo == null) return rv; - byte[] oldpib = null; + if (pinfo == null) + return Collections.EMPTY_LIST; + List rv = new ArrayList(4); // for each peer address for (RouterAddress pa : pinfo.getAddresses()) { byte[] pib = pa.getIP(); if (pib == null) continue; - if (DataHelper.eq(oldpib, pib)) continue; - oldpib = pib; + // O(n**2) + for (int i = 0; i < rv.size(); i++) { + if (DataHelper.eq(rv.get(i), pib)) continue; + } rv.add(pib); } return rv; @@ -491,8 +539,9 @@ public class Blocklist { */ public boolean isBlocklisted(Hash peer) { List ips = getAddresses(peer); - for (Iterator iter = ips.iterator(); iter.hasNext(); ) { - byte ip[] = iter.next(); + if (ips.isEmpty()) + return false; + for (byte[] ip : ips) { if (isBlocklisted(ip)) { if (! _context.banlist().isBanlisted(peer)) // nice knowing you... @@ -505,6 +554,8 @@ public class Blocklist { /** * calling this externally won't banlist the peer, this is just an IP check + * + * @param ip IPv4 or IPv6 */ public boolean isBlocklisted(String ip) { byte[] pib = Addresses.getIP(ip); @@ -514,11 +565,15 @@ public class Blocklist { /** * calling this externally won't banlist the peer, this is just an IP check + * + * @param ip IPv4 or IPv6 */ public boolean isBlocklisted(byte ip[]) { - if (ip.length != 4) - return false; - return isBlocklisted(toInt(ip)); + if (ip.length == 4) + return isBlocklisted(toInt(ip)); + if (ip.length == 16) + return isOnSingleList(new BigInteger(1, ip)); + return false; } /** @@ -760,7 +815,7 @@ public class Blocklist { //out.write("

Banned IPs

"); Set singles = new TreeSet(); singles.addAll(_singleIPBlocklist); - if (!singles.isEmpty()) { + if (!(singles.isEmpty() && _singleIPv6Blocklist.isEmpty())) { out.write(""); @@ -782,6 +837,19 @@ public class Blocklist { out.write(toStr(ip)); out.write("\n"); } + // then IPv6 + if (!_singleIPv6Blocklist.isEmpty()) { + List s6; + synchronized(_singleIPv6Blocklist) { + s6 = new ArrayList(_singleIPv6Blocklist.keySet()); + } + Collections.sort(s6); + for (BigInteger bi : s6) { + out.write("\n"); + } + } out.write("
"); out.write(_("IPs Banned Until Restart")); out.write("
 
"); + out.write(Addresses.toString(toIPBytes(bi))); + out.write(" 
"); } if (_blocklistSize > 0) { @@ -832,6 +900,23 @@ public class Blocklist { out.flush(); } + /** + * Convert a (non-negative) two's complement IP to exactly 16 bytes + * @since IPv6 + */ + private static byte[] toIPBytes(BigInteger bi) { + byte[] ba = bi.toByteArray(); + int len = ba.length; + if (len == 16) + return ba; + byte[] rv = new byte[16]; + if (len < 16) + System.arraycopy(ba, 0, rv, 16 - len, len); + else + System.arraycopy(ba, len - 16, rv, 0, 16); + return rv; + } + /** * Mark a string for extraction by xgettext and translation. * Use this only in static initializers. diff --git a/router/java/src/net/i2p/router/CommSystemFacade.java b/router/java/src/net/i2p/router/CommSystemFacade.java index 82e73c2e10..1386b87277 100644 --- a/router/java/src/net/i2p/router/CommSystemFacade.java +++ b/router/java/src/net/i2p/router/CommSystemFacade.java @@ -28,8 +28,8 @@ public abstract class CommSystemFacade implements Service { public void renderStatusHTML(Writer out, String urlBase, int sortFlags) throws IOException { } public void renderStatusHTML(Writer out) throws IOException { renderStatusHTML(out, null, 0); } - /** Create the set of RouterAddress structures based on the router's config */ - public Set createAddresses() { return Collections.EMPTY_SET; } + /** Create the list of RouterAddress structures based on the router's config */ + public List createAddresses() { return Collections.EMPTY_LIST; } public int countActivePeers() { return 0; } public int countActiveSendPeers() { return 0; } diff --git a/router/java/src/net/i2p/router/MessageHistory.java b/router/java/src/net/i2p/router/MessageHistory.java index 2ded9f03b8..66e13952f7 100644 --- a/router/java/src/net/i2p/router/MessageHistory.java +++ b/router/java/src/net/i2p/router/MessageHistory.java @@ -91,8 +91,13 @@ public class MessageHistory { */ public synchronized void initialize(boolean forceReinitialize) { if (!forceReinitialize) return; + Router router = _context.router(); + if (router == null) { + // unit testing, presumably + return; + } - if (_context.router().getRouterInfo() == null) { + if (router.getRouterInfo() == null) { _reinitializeJob.getTiming().setStartAfter(_context.clock().now() + 15*1000); _context.jobQueue().addJob(_reinitializeJob); } else { diff --git a/router/java/src/net/i2p/router/NetworkDatabaseFacade.java b/router/java/src/net/i2p/router/NetworkDatabaseFacade.java index f1fbd1b9fd..4ed10ebe88 100644 --- a/router/java/src/net/i2p/router/NetworkDatabaseFacade.java +++ b/router/java/src/net/i2p/router/NetworkDatabaseFacade.java @@ -77,4 +77,13 @@ public abstract class NetworkDatabaseFacade implements Service { /** @since 0.9 */ public ReseedChecker reseedChecker() { return null; }; + + /** + * For convenience, so users don't have to cast to FNDF, and unit tests using + * Dummy NDF will work. + * + * @return false; FNDF overrides to return actual setting + * @since IPv6 + */ + public boolean floodfillEnabled() { return false; }; } diff --git a/router/java/src/net/i2p/router/Router.java b/router/java/src/net/i2p/router/Router.java index 125d2e075c..1ae539ab8d 100644 --- a/router/java/src/net/i2p/router/Router.java +++ b/router/java/src/net/i2p/router/Router.java @@ -601,7 +601,7 @@ public class Router implements RouterClock.ClockShiftListener { } // if prop set to true, don't tell people we are ff even if we are - if (FloodfillNetworkDatabaseFacade.floodfillEnabled(_context) && + if (_context.netDb().floodfillEnabled() && !_context.getBooleanProperty("router.hideFloodfillParticipant")) ri.addCapability(FloodfillNetworkDatabaseFacade.CAPABILITY_FLOODFILL); diff --git a/router/java/src/net/i2p/router/RouterContext.java b/router/java/src/net/i2p/router/RouterContext.java index 990f087da2..5101076a8c 100644 --- a/router/java/src/net/i2p/router/RouterContext.java +++ b/router/java/src/net/i2p/router/RouterContext.java @@ -38,7 +38,8 @@ import net.i2p.util.I2PProperties.I2PPropertyCallback; */ public class RouterContext extends I2PAppContext { private final Router _router; - private ClientManagerFacadeImpl _clientManagerFacade; + private ClientManagerFacade _clientManagerFacade; + private InternalClientManager _internalClientManager; private ClientMessagePool _clientMessagePool; private JobQueue _jobQueue; private InNetMessagePool _inNetMessagePool; @@ -152,15 +153,29 @@ public class RouterContext extends I2PAppContext { } + /** + * The following properties may be used to replace various parts + * of the context with dummy implementations for testing, by setting + * the property to "true": + *
+     *  i2p.dummyClientFacade
+     *  i2p.dummyNetDb
+     *  i2p.dummyPeerManager
+     *  i2p.dummyTunnelManager
+     *  i2p.vmCommSystem (transport)
+     *
+ */ public synchronized void initAll() { if (_initialized) throw new IllegalStateException(); - if (getBooleanProperty("i2p.dummyClientFacade")) - System.err.println("i2p.dummyClientFacade currently unsupported"); - _clientManagerFacade = new ClientManagerFacadeImpl(this); - // removed since it doesn't implement InternalClientManager for now - //else - // _clientManagerFacade = new DummyClientManagerFacade(this); + if (!getBooleanProperty("i2p.dummyClientFacade")) { + ClientManagerFacadeImpl cmfi = new ClientManagerFacadeImpl(this); + _clientManagerFacade = cmfi; + _internalClientManager = cmfi; + } else { + _clientManagerFacade = new DummyClientManagerFacade(this); + // internal client manager is null + } _clientMessagePool = new ClientMessagePool(this); _jobQueue = new JobQueue(this); _inNetMessagePool = new InNetMessagePool(this); @@ -168,23 +183,23 @@ public class RouterContext extends I2PAppContext { _messageHistory = new MessageHistory(this); _messageRegistry = new OutboundMessageRegistry(this); //_messageStateMonitor = new MessageStateMonitor(this); - if ("false".equals(getProperty("i2p.dummyNetDb", "false"))) + if (!getBooleanProperty("i2p.dummyNetDb")) _netDb = new FloodfillNetworkDatabaseFacade(this); // new KademliaNetworkDatabaseFacade(this); else _netDb = new DummyNetworkDatabaseFacade(this); _keyManager = new KeyManager(this); - if ("false".equals(getProperty("i2p.vmCommSystem", "false"))) + if (!getBooleanProperty("i2p.vmCommSystem")) _commSystem = new CommSystemFacadeImpl(this); else _commSystem = new VMCommSystem(this); _profileOrganizer = new ProfileOrganizer(this); - if ("false".equals(getProperty("i2p.dummyPeerManager", "false"))) + if (!getBooleanProperty("i2p.dummyPeerManager")) _peerManagerFacade = new PeerManagerFacadeImpl(this); else _peerManagerFacade = new DummyPeerManagerFacade(); _profileManager = new ProfileManagerImpl(this); _bandwidthLimiter = new FIFOBandwidthLimiter(this); - if ("false".equals(getProperty("i2p.dummyTunnelManager", "false"))) + if (!getBooleanProperty("i2p.dummyTunnelManager")) _tunnelManager = new TunnelPoolManager(this); else _tunnelManager = new DummyTunnelManagerFacade(); @@ -529,7 +544,7 @@ public class RouterContext extends I2PAppContext { */ @Override public InternalClientManager internalClientManager() { - return _clientManagerFacade; + return _internalClientManager; } /** diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java index f2522cd4ed..f5bf678088 100644 --- a/router/java/src/net/i2p/router/RouterVersion.java +++ b/router/java/src/net/i2p/router/RouterVersion.java @@ -21,7 +21,7 @@ public class RouterVersion { public final static long BUILD = 9; /** for example "-test" */ - public final static String EXTRA = ""; + public final static String EXTRA = "-ipv6"; public final static String FULL_VERSION = VERSION + "-" + BUILD + EXTRA; public static void main(String args[]) { System.out.println("I2P Router version: " + FULL_VERSION); diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/ExploreKeySelectorJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/ExploreKeySelectorJob.java index 6fa0416603..f11937571a 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/ExploreKeySelectorJob.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/ExploreKeySelectorJob.java @@ -38,7 +38,7 @@ class ExploreKeySelectorJob extends JobImpl { public String getName() { return "Explore Key Selector Job"; } public void runJob() { - if (((FloodfillNetworkDatabaseFacade)_facade).floodfillEnabled()) { + if (_facade.floodfillEnabled()) { requeue(30*RERUN_DELAY_MS); return; } 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 26e8d7b4d3..83d0e296b1 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillNetworkDatabaseFacade.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillNetworkDatabaseFacade.java @@ -260,10 +260,8 @@ public class FloodfillNetworkDatabaseFacade extends KademliaNetworkDatabaseFacad } } + @Override public boolean floodfillEnabled() { return _floodfillEnabled; } - public static boolean floodfillEnabled(RouterContext ctx) { - return ((FloodfillNetworkDatabaseFacade)ctx.netDb()).floodfillEnabled(); - } /** * @param peer may be null, returns false if null 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 7567e79b31..86170d237b 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillPeerSelector.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillPeerSelector.java @@ -303,7 +303,7 @@ class FloodfillPeerSelector extends PeerSelector { * @since 0.9.5 modified from ProfileOrganizer */ private Set maskedIPSet(Hash peer, RouterInfo pinfo, int mask) { - Set rv = new HashSet(2); + Set rv = new HashSet(4); byte[] commIP = _context.commSystem().getIP(peer); if (commIP != null) rv.add(maskedIP(commIP, mask)); @@ -322,12 +322,22 @@ class FloodfillPeerSelector extends PeerSelector { /** * generate an arbitrary unique value for this ip/mask (mask = 1-4) + * If IPv6, force mask = 8. * @since 0.9.5 copied from ProfileOrganizer */ private static Integer maskedIP(byte[] ip, int mask) { - int rv = 0; - for (int i = 0; i < mask; i++) - rv = (rv << 8) | (ip[i] & 0xff); + int rv = ip[0]; + if (ip.length == 16) { + for (int i = 1; i < 8; i++) { + rv <<= i * 4; + rv ^= ip[i]; + } + } else { + for (int i = 1; i < mask; i++) { + rv <<= 8; + rv ^= ip[i]; + } + } return Integer.valueOf(rv); } 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 1409ecc2a1..0d03d7d4fe 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/HandleFloodfillDatabaseLookupMessageJob.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/HandleFloodfillDatabaseLookupMessageJob.java @@ -38,7 +38,7 @@ public class HandleFloodfillDatabaseLookupMessageJob extends HandleDatabaseLooku */ @Override protected boolean answerAllQueries() { - if (!FloodfillNetworkDatabaseFacade.floodfillEnabled(getContext())) return false; + if (!getContext().netDb().floodfillEnabled()) return false; return FloodfillNetworkDatabaseFacade.isFloodfill(getContext().router().getRouterInfo()); } @@ -52,7 +52,7 @@ public class HandleFloodfillDatabaseLookupMessageJob extends HandleDatabaseLooku super.sendClosest(key, routerInfoSet, toPeer, replyTunnel); // go away, you got the wrong guy, send our RI back unsolicited - if (!FloodfillNetworkDatabaseFacade.floodfillEnabled(getContext())) { + if (!getContext().netDb().floodfillEnabled()) { // We could just call sendData(myhash, myri, toPeer, replyTunnel) but // that would increment the netDb.lookupsHandled and netDb.lookupsMatched stats DatabaseStoreMessage msg = new DatabaseStoreMessage(getContext()); 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 98a41874b7..a56f23098d 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/HandleFloodfillDatabaseStoreMessageJob.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/HandleFloodfillDatabaseStoreMessageJob.java @@ -187,7 +187,7 @@ public class HandleFloodfillDatabaseStoreMessageJob extends JobImpl { // flood it if (invalidMessage == null && - FloodfillNetworkDatabaseFacade.floodfillEnabled(getContext()) && + getContext().netDb().floodfillEnabled() && _message.getReplyToken() > 0) { if (wasNew) { // DOS prevention 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 553c734cff..dc21ff9432 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/KademliaNetworkDatabaseFacade.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/KademliaNetworkDatabaseFacade.java @@ -763,7 +763,7 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { // As the net grows this won't be sufficient, and we'll have to implement // flushing some from memory, while keeping all on disk. long adjustedExpiration; - if (FloodfillNetworkDatabaseFacade.floodfillEnabled(_context)) + if (floodfillEnabled()) adjustedExpiration = ROUTER_INFO_EXPIRATION_FLOODFILL; else // _kb.size() includes leasesets but that's ok 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 7df231726f..80815ea820 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/SearchJob.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/SearchJob.java @@ -134,7 +134,7 @@ class SearchJob extends JobImpl { // The other two places this was called (one below and one in FNDF) // have been commented out. // Returning false essentially enables kademlia as a backup to floodfill for search responses. - if (FloodfillNetworkDatabaseFacade.floodfillEnabled(ctx)) + if (ctx.netDb().floodfillEnabled()) return false; return ctx.getProperty("netDb.floodfillOnly", DEFAULT_FLOODFILL_ONLY); } 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 0f9f4557b5..49198ad9f6 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/StartExplorersJob.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/StartExplorersJob.java @@ -51,7 +51,7 @@ class StartExplorersJob extends JobImpl { public String getName() { return "Start Explorers Job"; } public void runJob() { - if (! (((FloodfillNetworkDatabaseFacade)_facade).floodfillEnabled() || + if (! (_facade.floodfillEnabled() || getContext().router().gracefulShutdownInProgress())) { int num = MAX_PER_RUN; if (_facade.getDataStore().size() < LOW_ROUTERS) @@ -93,7 +93,7 @@ class StartExplorersJob extends JobImpl { */ private long getNextRunDelay() { // we don't explore if floodfill - if (((FloodfillNetworkDatabaseFacade)_facade).floodfillEnabled()) + if (_facade.floodfillEnabled()) return MAX_RERUN_DELAY_MS; // If we don't know too many peers, or just started, explore aggressively diff --git a/router/java/src/net/i2p/router/peermanager/ProfileOrganizer.java b/router/java/src/net/i2p/router/peermanager/ProfileOrganizer.java index 55b57bae93..4ded9f81c1 100644 --- a/router/java/src/net/i2p/router/peermanager/ProfileOrganizer.java +++ b/router/java/src/net/i2p/router/peermanager/ProfileOrganizer.java @@ -1264,7 +1264,7 @@ public class ProfileOrganizer { * @return an opaque set of masked IPs for this peer */ private Set maskedIPSet(Hash peer, int mask) { - Set rv = new HashSet(2); + Set rv = new HashSet(4); byte[] commIP = _context.commSystem().getIP(peer); if (commIP != null) rv.add(maskedIP(commIP, mask)); @@ -1282,11 +1282,23 @@ public class ProfileOrganizer { return rv; } - /** generate an arbitrary unique value for this ip/mask (mask = 1-4) */ + /** + * generate an arbitrary unique value for this ip/mask (mask = 1-4) + * If IPv6, force mask = 8. + */ private static Integer maskedIP(byte[] ip, int mask) { - int rv = 0; - for (int i = 0; i < mask; i++) - rv = (rv << 8) | (ip[i] & 0xff); + int rv = ip[0]; + if (ip.length == 16) { + for (int i = 1; i < 8; i++) { + rv <<= i * 4; + rv ^= ip[i]; + } + } else { + for (int i = 1; i < mask; i++) { + rv <<= 8; + rv ^= ip[i]; + } + } return Integer.valueOf(rv); } diff --git a/router/java/src/net/i2p/router/transport/CommSystemFacadeImpl.java b/router/java/src/net/i2p/router/transport/CommSystemFacadeImpl.java index 6bed29d73c..bb60b978ad 100644 --- a/router/java/src/net/i2p/router/transport/CommSystemFacadeImpl.java +++ b/router/java/src/net/i2p/router/transport/CommSystemFacadeImpl.java @@ -10,15 +10,14 @@ package net.i2p.router.transport; import java.io.IOException; import java.io.Writer; +import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Properties; -import java.util.Set; import java.util.Vector; import net.i2p.data.Hash; @@ -27,9 +26,6 @@ import net.i2p.data.RouterInfo; import net.i2p.router.CommSystemFacade; import net.i2p.router.OutNetMessage; import net.i2p.router.RouterContext; -import net.i2p.router.transport.ntcp.NTCPAddress; -import net.i2p.router.transport.ntcp.NTCPTransport; -import net.i2p.router.transport.udp.UDPAddress; import net.i2p.router.transport.udp.UDPTransport; import net.i2p.util.Addresses; import net.i2p.util.Log; @@ -45,6 +41,12 @@ public class CommSystemFacadeImpl extends CommSystemFacade { private final GeoIP _geoIP; private volatile boolean _netMonitorStatus; private boolean _wasStarted; + + /** + * Disable connections for testing + * @since IPv6 + */ + private static final String PROP_DISABLED = "i2np.disable"; public CommSystemFacadeImpl(RouterContext context) { _context = context; @@ -125,23 +127,17 @@ public class CommSystemFacadeImpl extends CommSystemFacade { return sum * 1000 / frameSize; } - public List getBids(OutNetMessage msg) { - return _manager.getBids(msg); - } - public TransportBid getBid(OutNetMessage msg) { - return _manager.getBid(msg); - } - public TransportBid getNextBid(OutNetMessage msg) { - return _manager.getNextBid(msg); - } - int getTransportCount() { return _manager.getTransportCount(); } - /** Send the message out */ public void processMessage(OutNetMessage msg) { + if (isDummy()) { + // testing + GetBidsJob.fail(_context, msg); + return; + } //GetBidsJob j = new GetBidsJob(_context, this, msg); //j.runJob(); //long before = _context.clock().now(); - GetBidsJob.getBids(_context, this, msg); + GetBidsJob.getBids(_context, _manager, msg); // < 0.4 ms //_context.statManager().addRateData("transport.getBidsJobTime", _context.clock().now() - before); } @@ -167,7 +163,7 @@ public class CommSystemFacadeImpl extends CommSystemFacade { } @Override - public List getMostRecentErrorMessages() { + public List getMostRecentErrorMessages() { return _manager.getMostRecentErrorMessages(); } @@ -190,240 +186,33 @@ public class CommSystemFacadeImpl extends CommSystemFacade { /** @return non-null, possibly empty */ @Override - public Set createAddresses() { + public List createAddresses() { // No, don't do this, it makes it almost impossible to build inbound tunnels //if (_context.router().isHidden()) // return Collections.EMPTY_SET; - Map addresses = _manager.getAddresses(); - boolean newCreated = false; - - if (!addresses.containsKey(NTCPTransport.STYLE)) { - RouterAddress addr = createNTCPAddress(_context); - if (_log.shouldLog(Log.INFO)) - _log.info("NTCP address: " + addr); - if (addr != null) { - addresses.put(NTCPTransport.STYLE, addr); - newCreated = true; - } - } - + List addresses = new ArrayList(_manager.getAddresses()); if (_log.shouldLog(Log.INFO)) - _log.info("Creating addresses: " + addresses + " isNew? " + newCreated, new Exception("creator")); - return new HashSet(addresses.values()); + _log.info("Creating addresses: " + addresses, new Exception("creator")); + return addresses; } - public final static String PROP_I2NP_NTCP_HOSTNAME = "i2np.ntcp.hostname"; - public final static String PROP_I2NP_NTCP_PORT = "i2np.ntcp.port"; - public final static String PROP_I2NP_NTCP_AUTO_PORT = "i2np.ntcp.autoport"; - public final static String PROP_I2NP_NTCP_AUTO_IP = "i2np.ntcp.autoip"; - - /** - * This only creates an address if the hostname AND port are set in router.config, - * which should be rare. - * Otherwise, notifyReplaceAddress() below takes care of it. - * Note this is called both from above and from NTCPTransport.startListening() - * - * This should really be moved to ntcp/NTCPTransport.java, why is it here? - */ - public static RouterAddress createNTCPAddress(RouterContext ctx) { - if (!TransportManager.isNTCPEnabled(ctx)) return null; - String name = ctx.router().getConfigSetting(PROP_I2NP_NTCP_HOSTNAME); - String port = ctx.router().getConfigSetting(PROP_I2NP_NTCP_PORT); - /* - boolean isNew = false; - if (name == null) { - name = "localhost"; - isNew = true; - } - if (port == null) { - port = String.valueOf(ctx.random().nextInt(10240)+1024); - isNew = true; - } - */ - if ( (name == null) || (port == null) || (name.trim().length() <= 0) || ("null".equals(name)) ) - return null; - try { - int p = Integer.parseInt(port); - if ( (p <= 0) || (p > 64*1024) ) - return null; - } catch (NumberFormatException nfe) { - return null; - } - Properties props = new Properties(); - props.setProperty(NTCPAddress.PROP_HOST, name); - props.setProperty(NTCPAddress.PROP_PORT, port); - RouterAddress addr = new RouterAddress(); - addr.setCost(NTCPAddress.DEFAULT_COST); - //addr.setExpiration(null); - addr.setOptions(props); - addr.setTransportStyle(NTCPTransport.STYLE); - //if (isNew) { - // why save the same thing? - Map changes = new HashMap(); - changes.put(PROP_I2NP_NTCP_HOSTNAME, name); - changes.put(PROP_I2NP_NTCP_PORT, port); - ctx.router().saveConfig(changes, null); - //} - return addr; - } - /** * UDP changed addresses, tell NTCP and restart - * This should really be moved to ntcp/NTCPTransport.java, why is it here? + * + * All the work moved to NTCPTransport.externalAddressReceived() + * + * @param udpAddr may be null; or udpAddr's host/IP may be null */ @Override - public synchronized void notifyReplaceAddress(RouterAddress udpAddr) { - if (udpAddr == null) - return; - NTCPTransport t = (NTCPTransport) _manager.getTransport(NTCPTransport.STYLE); - if (t == null) - return; - RouterAddress oldAddr = t.getCurrentAddress(); - if (_log.shouldLog(Log.INFO)) - _log.info("Changing NTCP Address? was " + oldAddr); - RouterAddress newAddr = new RouterAddress(); - newAddr.setTransportStyle(NTCPTransport.STYLE); - Properties newProps = new Properties(); - if (oldAddr == null) { - newAddr.setCost(NTCPAddress.DEFAULT_COST); - } else { - newAddr.setCost(oldAddr.getCost()); - newProps.putAll(oldAddr.getOptionsMap()); - } - - boolean changed = false; - - // Auto Port Setting - // old behavior (<= 0.7.3): auto-port defaults to false, and true trumps explicit setting - // new behavior (>= 0.7.4): auto-port defaults to true, but explicit setting trumps auto - // TODO rewrite this to operate on ints instead of strings - String oport = newProps.getProperty(NTCPAddress.PROP_PORT); - String nport = null; - String cport = _context.getProperty(PROP_I2NP_NTCP_PORT); - if (cport != null && cport.length() > 0) { - nport = cport; - } else if (_context.getBooleanPropertyDefaultTrue(PROP_I2NP_NTCP_AUTO_PORT)) { - // 0.9.6 change - // This wasn't quite right, as udpAddr is the EXTERNAL port and we really - // want NTCP to bind to the INTERNAL port the first time, - // because if they are different, the NAT is changing them, and - // it probably isn't mapping UDP and TCP the same. + public void notifyReplaceAddress(RouterAddress udpAddr) { + byte[] ip = udpAddr != null ? udpAddr.getIP() : null; + int port = udpAddr != null ? udpAddr.getPort() : 0; + if (port < 0) { Transport udp = _manager.getTransport(UDPTransport.STYLE); - if (udp != null) { - int udpIntPort = udp.getRequestedPort(); - if (udpIntPort > 0) - // should always be true - nport = Integer.toString(udpIntPort); - } - if (nport == null) - // fallback - nport = udpAddr.getOption(UDPAddress.PROP_PORT); + if (udp != null) + port = udp.getRequestedPort(); } - if (_log.shouldLog(Log.INFO)) - _log.info("old: " + oport + " config: " + cport + " new: " + nport); - if (nport == null || nport.length() <= 0) - return; - // 0.9.6 change - // Don't have NTCP "chase" SSU's external port, - // as it may change, possibly frequently. - //if (oport == null || ! oport.equals(nport)) { - if (oport == null) { - newProps.setProperty(NTCPAddress.PROP_PORT, nport); - changed = true; - } - - // Auto IP Setting - // old behavior (<= 0.7.3): auto-ip defaults to false, and trumps configured hostname, - // and ignores reachability status - leading to - // "firewalled with inbound TCP enabled" warnings. - // new behavior (>= 0.7.4): auto-ip defaults to true, and explicit setting trumps auto, - // and only takes effect if reachability is OK. - // And new "always" setting ignores reachability status, like - // "true" was in 0.7.3 - String ohost = newProps.getProperty(NTCPAddress.PROP_HOST); - String enabled = _context.getProperty(PROP_I2NP_NTCP_AUTO_IP, "true").toLowerCase(Locale.US); - String name = _context.getProperty(PROP_I2NP_NTCP_HOSTNAME); - // hostname config trumps auto config - if (name != null && name.length() > 0) - enabled = "false"; - Transport udp = _manager.getTransport(UDPTransport.STYLE); - short status = STATUS_UNKNOWN; - if (udp != null) - status = udp.getReachabilityStatus(); - if (_log.shouldLog(Log.INFO)) - _log.info("old: " + ohost + " config: " + name + " auto: " + enabled + " status: " + status); - if (enabled.equals("always") || - (Boolean.parseBoolean(enabled) && status == STATUS_OK)) { - String nhost = udpAddr.getOption(UDPAddress.PROP_HOST); - if (_log.shouldLog(Log.INFO)) - _log.info("old: " + ohost + " config: " + name + " new: " + nhost); - if (nhost == null || nhost.length() <= 0) - return; - if (ohost == null || ! ohost.equalsIgnoreCase(nhost)) { - newProps.setProperty(NTCPAddress.PROP_HOST, nhost); - changed = true; - } - } else if (enabled.equals("false") && - name != null && name.length() > 0 && - !name.equals(ohost) && - nport != null) { - // Host name is configured, and we have a port (either auto or configured) - // but we probably only get here if the port is auto, - // otherwise createNTCPAddress() would have done it already - if (_log.shouldLog(Log.INFO)) - _log.info("old: " + ohost + " config: " + name + " new: " + name); - newProps.setProperty(NTCPAddress.PROP_HOST, name); - changed = true; - } else if (ohost == null || ohost.length() <= 0) { - return; - } else if (Boolean.parseBoolean(enabled) && status != STATUS_OK) { - // UDP transitioned to not-OK, turn off NTCP address - // This will commonly happen at startup if we were initially OK - // because UPnP was successful, but a subsequent SSU Peer Test determines - // we are still firewalled (SW firewall, bad UPnP indication, etc.) - if (_log.shouldLog(Log.INFO)) - _log.info("old: " + ohost + " config: " + name + " new: null"); - newAddr = null; - changed = true; - } - - if (!changed) { - if (oldAddr != null) { - int oldCost = oldAddr.getCost(); - int newCost = NTCPAddress.DEFAULT_COST; - if (TransportImpl.ADJUST_COST && !t.haveCapacity()) - newCost++; - if (newCost != oldCost) { - oldAddr.setCost(newCost); - if (_log.shouldLog(Log.WARN)) - _log.warn("Changing NTCP cost from " + oldCost + " to " + newCost); - } else { - _log.info("No change to NTCP Address"); - } - } else { - _log.info("No change to NTCP Address"); - } - return; - } - - // stopListening stops the pumper, readers, and writers, so required even if - // oldAddr == null since startListening starts them all again - // - // really need to fix this so that we can change or create an inbound address - // without tearing down everything - // Especially on disabling the address, we shouldn't tear everything down. - // - _log.warn("Halting NTCP to change address"); - t.stopListening(); - if (newAddr != null) - newAddr.setOptions(newProps); - // Wait for NTCP Pumper to stop so we don't end up with two... - while (t.isAlive()) { - try { Thread.sleep(5*1000); } catch (InterruptedException ie) {} - } - t.restartListening(newAddr); - _log.warn("Changed NTCP Address and started up, address is now " + newAddr); - return; + _manager.externalAddressReceived(Transport.AddressSource.SOURCE_SSU, ip, port); } /* @@ -451,10 +240,10 @@ public class CommSystemFacadeImpl extends CommSystemFacade { RouterInfo ri = _context.netDb().lookupRouterInfoLocally(iter.next()); if (ri == null) continue; - String host = getIPString(ri); - if (host == null) + byte[] ip = getIP(ri); + if (ip == null) continue; - _geoIP.add(host); + _geoIP.add(ip); } _context.simpleScheduler().addPeriodicEvent(new Lookup(), 5000, LOOKUP_TIME); } @@ -491,31 +280,35 @@ public class CommSystemFacadeImpl extends CommSystemFacade { /** * Uses the transport IP first because that lookup is fast, - * then the SSU IP from the netDb. + * then the IP from the netDb. * * @return two-letter lower-case country code or null */ @Override public String getCountry(Hash peer) { byte[] ip = TransportImpl.getIP(peer); + //if (ip != null && ip.length == 4) if (ip != null) return _geoIP.get(ip); RouterInfo ri = _context.netDb().lookupRouterInfoLocally(peer); if (ri == null) return null; - String s = getIPString(ri); - if (s != null) - return _geoIP.get(s); + ip = getIP(ri); + if (ip != null) + return _geoIP.get(ip); return null; } - private String getIPString(RouterInfo ri) { - // use SSU only, it is likely to be an IP not a hostname, - // we don't want to generate a lot of DNS queries at startup - RouterAddress ra = ri.getTargetAddress("SSU"); - if (ra == null) - return null; - return ra.getOption("host"); + private static byte[] getIP(RouterInfo ri) { + // Return first IP (v4 or v6) we find, any transport + // Assume IPv6 doesn't have geoIP for now + for (RouterAddress ra : ri.getAddresses()) { + byte[] rv = ra.getIP(); + //if (rv != null && rv.length == 4) + if (rv != null) + return rv; + } + return null; } /** full name for a country code, or the code if we don't know the name */ @@ -556,9 +349,14 @@ public class CommSystemFacadeImpl extends CommSystemFacade { return buf.toString(); } - /** @since 0.8.13 */ + /** + * Is everything disabled for testing? + * @since 0.8.13 + */ @Override - public boolean isDummy() { return false; } + public boolean isDummy() { + return _context.getBooleanProperty(PROP_DISABLED); + } /** * Translate diff --git a/router/java/src/net/i2p/router/transport/GeoIP.java b/router/java/src/net/i2p/router/transport/GeoIP.java index 5b6cc813f3..a410bbb42b 100644 --- a/router/java/src/net/i2p/router/transport/GeoIP.java +++ b/router/java/src/net/i2p/router/transport/GeoIP.java @@ -48,8 +48,12 @@ class GeoIP { private final Map _codeToName; /** code to itself to prevent String proliferation */ private final Map _codeCache; + + // In the following structures, an IPv4 IP is stored as a non-negative long, 0 to 2**32 - 1, + // and the first 8 bytes of an IPv6 IP are stored as a signed long. private final Map _IPToCountry; private final Set _pendingSearch; + private final Set _pendingIPv6Search; private final Set _notFound; private final AtomicBoolean _lock; private int _lookupRunCount; @@ -58,10 +62,11 @@ class GeoIP { public GeoIP(RouterContext context) { _context = context; _log = context.logManager().getLog(GeoIP.class); - _codeToName = new ConcurrentHashMap(256); - _codeCache = new ConcurrentHashMap(256); + _codeToName = new ConcurrentHashMap(512); + _codeCache = new ConcurrentHashMap(512); _IPToCountry = new ConcurrentHashMap(); _pendingSearch = new ConcurrentHashSet(); + _pendingIPv6Search = new ConcurrentHashSet(); _notFound = new ConcurrentHashSet(); _lock = new AtomicBoolean(); readCountryFile(); @@ -81,6 +86,7 @@ class GeoIP { _codeCache.clear(); _IPToCountry.clear(); _pendingSearch.clear(); + _pendingIPv6Search.clear(); _notFound.clear(); } @@ -107,6 +113,7 @@ class GeoIP { public void blockingLookup() { if (! _context.getBooleanPropertyDefaultTrue(PROP_GEOIP_ENABLED)) { _pendingSearch.clear(); + _pendingIPv6Search.clear(); return; } int pri = Thread.currentThread().getPriority(); @@ -132,18 +139,29 @@ class GeoIP { // clear the negative cache every few runs, to prevent it from getting too big if (((++_lookupRunCount) % CLEAR) == 0) _notFound.clear(); + // IPv4 Long[] search = _pendingSearch.toArray(new Long[_pendingSearch.size()]); - if (search.length <= 0) - return; _pendingSearch.clear(); - Arrays.sort(search); - String[] countries = readGeoIPFile(search); - - for (int i = 0; i < countries.length; i++) { - if (countries[i] != null) - _IPToCountry.put(search[i], countries[i]); - else - _notFound.add(search[i]); + if (search.length > 0) { + String[] countries = readGeoIPFile(search); + for (int i = 0; i < countries.length; i++) { + if (countries[i] != null) + _IPToCountry.put(search[i], countries[i]); + else + _notFound.add(search[i]); + } + } + // IPv6 + search = _pendingSearch.toArray(new Long[_pendingIPv6Search.size()]); + _pendingIPv6Search.clear(); + if (search.length > 0) { + String[] countries = GeoIPv6.readGeoIPFile(_context, search, _codeCache); + for (int i = 0; i < countries.length; i++) { + if (countries[i] != null) + _IPToCountry.put(search[i], countries[i]); + else + _notFound.add(search[i]); + } } } finally { _lock.set(false); @@ -169,16 +187,16 @@ class GeoIP { * */ private void readCountryFile() { - File GeoFile = new File(_context.getBaseDir(), GEOIP_DIR_DEFAULT); - GeoFile = new File(GeoFile, COUNTRY_FILE_DEFAULT); - if (!GeoFile.exists()) { + File geoFile = new File(_context.getBaseDir(), GEOIP_DIR_DEFAULT); + geoFile = new File(geoFile, COUNTRY_FILE_DEFAULT); + if (!geoFile.exists()) { if (_log.shouldLog(Log.WARN)) - _log.warn("Country file not found: " + GeoFile.getAbsolutePath()); + _log.warn("Country file not found: " + geoFile.getAbsolutePath()); return; } FileInputStream in = null; try { - in = new FileInputStream(GeoFile); + in = new FileInputStream(geoFile); BufferedReader br = new BufferedReader(new InputStreamReader(in, "UTF-8")); String line = null; while ( (line = br.readLine()) != null) { @@ -228,11 +246,11 @@ class GeoIP { * */ private String[] readGeoIPFile(Long[] search) { - File GeoFile = new File(_context.getBaseDir(), GEOIP_DIR_DEFAULT); - GeoFile = new File(GeoFile, GEOIP_FILE_DEFAULT); - if (!GeoFile.exists()) { + File geoFile = new File(_context.getBaseDir(), GEOIP_DIR_DEFAULT); + geoFile = new File(geoFile, GEOIP_FILE_DEFAULT); + if (!geoFile.exists()) { if (_log.shouldLog(Log.WARN)) - _log.warn("GeoIP file not found: " + GeoFile.getAbsolutePath()); + _log.warn("GeoIP file not found: " + geoFile.getAbsolutePath()); return new String[0]; } String[] rv = new String[search.length]; @@ -240,7 +258,7 @@ class GeoIP { long start = _context.clock().now(); FileInputStream in = null; try { - in = new FileInputStream(GeoFile); + in = new FileInputStream(geoFile); String buf = null; BufferedReader br = new BufferedReader(new InputStreamReader(in, "ISO-8859-1")); while ((buf = br.readLine()) != null && idx < search.length) { @@ -268,7 +286,7 @@ class GeoIP { } } catch (IOException ioe) { if (_log.shouldLog(Log.ERROR)) - _log.error("Error reading the GeoFile", ioe); + _log.error("Error reading the geoFile", ioe); } finally { if (in != null) try { in.close(); } catch (IOException ioe) {} } @@ -307,6 +325,7 @@ class GeoIP { /** * Add to the list needing lookup + * @param ip IPv4 or IPv6 */ public void add(String ip) { byte[] pib = Addresses.getIP(ip); @@ -314,20 +333,28 @@ class GeoIP { add(pib); } + /** + * Add to the list needing lookup + * @param ip IPv4 or IPv6 + */ public void add(byte ip[]) { - if (ip.length != 4) - return; add(toLong(ip)); } + /** see above for ip-to-long mapping */ private void add(long ip) { Long li = Long.valueOf(ip); - if (!(_IPToCountry.containsKey(li) || _notFound.contains(li))) - _pendingSearch.add(li); + if (!(_IPToCountry.containsKey(li) || _notFound.contains(li))) { + if (ip >= 0 && ip < (1L << 32)) + _pendingSearch.add(li); + else + _pendingIPv6Search.add(li); + } } /** * Get the country for an IP from the cache. + * @param ip IPv4 or IPv6 * @return lower-case code, generally two letters, or null. */ public String get(String ip) { @@ -338,23 +365,30 @@ class GeoIP { /** * Get the country for an IP from the cache. + * @param ip IPv4 or IPv6 * @return lower-case code, generally two letters, or null. */ public String get(byte ip[]) { - if (ip.length != 4) - return null; return get(toLong(ip)); } + /** see above for ip-to-long mapping */ private String get(long ip) { return _IPToCountry.get(Long.valueOf(ip)); } + /** see above for ip-to-long mapping */ private static long toLong(byte ip[]) { int rv = 0; - for (int i = 0; i < 4; i++) - rv |= (ip[i] & 0xff) << ((3-i)*8); - return rv & 0xffffffffl; + if (ip.length == 16) { + for (int i = 0; i < 8; i++) + rv |= (ip[i] & 0xffL) << ((7-i)*8); + return rv; + } else { + for (int i = 0; i < 4; i++) + rv |= (ip[i] & 0xff) << ((3-i)*8); + return rv & 0xffffffffl; + } } /** diff --git a/router/java/src/net/i2p/router/transport/GeoIPv6.java b/router/java/src/net/i2p/router/transport/GeoIPv6.java new file mode 100644 index 0000000000..36e443c083 --- /dev/null +++ b/router/java/src/net/i2p/router/transport/GeoIPv6.java @@ -0,0 +1,344 @@ +package net.i2p.router.transport; +/* + * free (adj.): unencumbered; not under the control of others + * Use at your own risk. + */ + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +import net.i2p.I2PAppContext; +import net.i2p.data.DataHelper; +import net.i2p.util.Log; + +/** + * Generate compressed geoipv6.dat.gz file, and + * lookup entries in it. + * + * @since IPv6 + */ +class GeoIPv6 { + + private static final String GEOIP_DIR_DEFAULT = "geoip"; + private static final String GEOIP_FILE_DEFAULT = "geoipv6.dat.gz"; + private static final String MAGIC = "I2PGeoIPv6\0\001\0\0\0\0"; + private static final String COMMENT = "I2P compressed geoipv6 file. See GeoIPv6.java for format."; + /** includes magic */ + private static final int HEADER_LEN = 256; + + /** + * Lookup search items in the geoip file. + * See below for format. + */ + public static String[] readGeoIPFile(I2PAppContext context, Long[] search, Map codeCache) { + Log log = context.logManager().getLog(GeoIPv6.class); + File geoFile = new File(context.getBaseDir(), GEOIP_DIR_DEFAULT); + geoFile = new File(geoFile, GEOIP_FILE_DEFAULT); + if (!geoFile.exists()) { + if (log.shouldLog(Log.WARN)) + log.warn("GeoIP file not found: " + geoFile.getAbsolutePath()); + return new String[0]; + } + return readGeoIPFile(geoFile, search, codeCache, log); + } + + /** + * Lookup search items in the geoip file. + * See below for format. + */ + private static String[] readGeoIPFile(File geoFile, Long[] search, Map codeCache, Log log) { + String[] rv = new String[search.length]; + int idx = 0; + long start = System.currentTimeMillis(); + InputStream in = null; + try { + in = new GZIPInputStream(new BufferedInputStream(new FileInputStream(geoFile))); + byte[] magic = new byte[MAGIC.length()]; + DataHelper.read(in, magic); + if (!DataHelper.eq(magic, DataHelper.getASCII(MAGIC))) + throw new IOException("Not a IPv6 geoip data file"); + // skip timestamp and comments + in.skip(HEADER_LEN - MAGIC.length()); + byte[] buf = new byte[18]; + while (DataHelper.read(in, buf) == 18 && idx < search.length) { + long ip1 = readLong(buf, 0); + long ip2 = readLong(buf, 8); + while (idx < search.length && search[idx].longValue() < ip1) { + idx++; + } + while (idx < search.length && search[idx].longValue() >= ip1 && search[idx].longValue() <= ip2) { + // written in lower case + String lc = new String(buf, 16, 2, "ISO-8859-1"); + // replace the new string with the identical one from the cache + String cached = codeCache.get(lc); + if (cached == null) + cached = lc; + rv[idx++] = cached; + } + } + } catch (IOException ioe) { + if (log.shouldLog(Log.ERROR)) + log.error("Error reading the geoFile", ioe); + } finally { + if (in != null) try { in.close(); } catch (IOException ioe) {} + } + + if (log.shouldLog(Log.INFO)) + log.info("GeoIPv6 processing finished, time: " + (System.currentTimeMillis() - start)); + return rv; + } + + + /** + * Read in and parse multiple IPv6 geoip CSV files, + * merge them, and write out a gzipped binary IPv6 geoip file. + * + * Acceptable input formats (IPv6 only): + *
+    *   #comment (# must be in column 1)
+    *   "text IP", "text IP", "bigint IP", "bigint IP", "country code", "country name"
+    *
+ * Quotes and spaces optional. Sorting not required. + * Country code case-insensitive. + * Fields 1, 2, and 5 are used; fields 3, 4, and 6 are ignored. + * This is identical to the format of the MaxMind GeoLite IPv6 file. + * + * Example: + *
+    *   "2001:200::", "2001:200:ffff:ffff:ffff:ffff:ffff:ffff", "42540528726795050063891204319802818560", "42540528806023212578155541913346768895", "JP", "Japan"
+    *
+ * + *
+    * Output format:
+    *   Bytes 0-9: Magic number "I2PGeoIPv6"
+    *   Bytes 10-11: version (0x0001)
+    *   Bytes 12-15 flags (0)
+    *   Bytes 16-23: Date (long)
+    *   Bytes 24-xx: Comment (UTF-8)
+    *   Bytes xx-255: null padding
+    *   Bytes 256-: 18 byte records:
+    *       8 byte from (/64)
+    *       8 byte to (/64)
+    *       2 byte country code LOWER case (ASCII)
+    *   Data must be sorted (SIGNED twos complement), no overlap
+    *
+ * + * SLOW. For preprocessing only! + * + * @return success + */ + private static boolean compressGeoIPv6CSVFiles(List inFiles, File outFile) { + boolean DEBUG = false; + List entries = new ArrayList(20000); + for (File geoFile : inFiles) { + int count = 0; + InputStream in = null; + try { + in = new BufferedInputStream(new FileInputStream(geoFile)); + if (geoFile.getName().endsWith(".gz")) + in = new GZIPInputStream(in); + String buf = null; + BufferedReader br = new BufferedReader(new InputStreamReader(in, "ISO-8859-1")); + while ((buf = br.readLine()) != null) { + try { + if (buf.charAt(0) == '#') { + continue; + } + String[] s = buf.split(","); + String ips1 = s[0].replace("\"", "").trim(); + String ips2 = s[1].replace("\"", "").trim(); + byte[] ip1 = InetAddress.getByName(ips1).getAddress(); + byte[] ip2 = InetAddress.getByName(ips2).getAddress(); + String country = s[4].replace("\"", "").trim().toLowerCase(Locale.US); + entries.add(new V6Entry(ip1, ip2, country)); + count++; + } catch (UnknownHostException uhe) { + uhe.printStackTrace(); + } catch (RuntimeException re) { + re.printStackTrace(); + } + } + System.err.println("Read " + count + " entries from " + geoFile); + } catch (IOException ioe) { + ioe.printStackTrace(); + //if (_log.shouldLog(Log.ERROR)) + // _log.error("Error reading the geoFile", ioe); + return false; + } finally { + if (in != null) try { in.close(); } catch (IOException ioe) {} + } + } + Collections.sort(entries); + // merge + V6Entry old = null; + for (int i = 0; i < entries.size(); i++) { + V6Entry e = entries.get(i); + if (DEBUG) + System.out.println("proc " + e.toString()); + if (old != null) { + if (e.from == old.from && e.to == old.to) { + // dup + if (DEBUG) + System.out.println("remove dup " + e); + entries.remove(i); + i--; + continue; + } + if (e.from <= old.to) { + // overlap + // truncate old + if (e.from < old.to) { + V6Entry rewrite = new V6Entry(old.from, e.from - 1, old.cc); + if (DEBUG) + System.out.println("rewrite old to " + rewrite); + entries.set(i - 1, rewrite); + } + if (e.to < old.to) { + // e inside old, add new after e + V6Entry insert = new V6Entry(e.to + 1, old.to, old.cc); + if (DEBUG) + System.out.println("insert " + insert); + int j = i + 1; + while (j < entries.size() && insert.compareTo(entries.get(j)) > 0) { + j++; + } + entries.add(j, insert); + } + } + } + old = e; + } + OutputStream out = null; + try { + out = new GZIPOutputStream(new BufferedOutputStream(new FileOutputStream(outFile))); + out.write(DataHelper.getASCII(MAGIC)); + writeLong(out, System.currentTimeMillis()); + byte[] comment = DataHelper.getUTF8(COMMENT); + out.write(comment); + out.write(new byte[256 - (16 + 8 + comment.length)]); + for (V6Entry e : entries) { + writeLong(out, e.from); + writeLong(out, e.to); + out.write(DataHelper.getASCII(e.cc)); + } + System.err.println("Wrote " + entries.size() + " entries to " + outFile); + } catch (IOException ioe) { + ioe.printStackTrace(); + //if (_log.shouldLog(Log.ERROR)) + // _log.error("Error reading the geoFile", ioe); + return false; + } finally { + if (out != null) try { out.close(); } catch (IOException ioe) {} + } + return true; + } + + /** + * Used to temporarily hold, sort, and merge entries before compressing + */ + private static class V6Entry implements Comparable { + public final long from, to; + public final String cc; + + public V6Entry(byte[] f, byte[] t, String c) { + if (f.length != 16 || t.length != 16 || c.length() != 2) + throw new IllegalArgumentException(); + from = toLong(f); + to = toLong(t); + cc = c; + if (to < from) + throw new IllegalArgumentException(toString()); + } + + public V6Entry(long f, long t, String c) { + from = f; + to = t; + cc = c; + if (t < f) + throw new IllegalArgumentException(toString()); + } + + /** twos complement */ + public int compareTo(V6Entry r) { + if (from < r.from) return -1; + if (r.from < from) return 1; + if (to < r.to) return -1; + if (r.to < to) return 1; + return 0; + } + + @Override + public String toString() { + return "0x" + Long.toHexString(from) + " -> 0x" + Long.toHexString(to) + " : " + cc; + } + } + + private static long toLong(byte ip[]) { + long rv = 0; + for (int i = 0; i < 8; i++) + rv |= (ip[i] & 0xffL) << ((7-i)*8); + return rv; + } + + /** like DataHelper.writeLong(rawStream, 8, value) but allows negative values */ + private static void writeLong(OutputStream rawStream, long value) throws IOException { + for (int i = 56; i >= 0; i -= 8) { + byte cur = (byte) (value >> i); + rawStream.write(cur); + } + } + + /** like DataHelper.readLong(src, offset, 8) but allows negative values */ + private static long readLong(byte[] src, int offset) throws IOException { + long rv = 0; + int limit = offset + 8; + for (int i = offset; i < limit; i++) { + rv <<= 8; + rv |= src[i] & 0xFF; + } + return rv; + } + + /** + * Merge and compress CSV files to I2P compressed format + * + * GeoIP infile1.csv[.gz] [infile2.csv[.gz]...] outfile.dat.gz + * + * Used to create the file for distribution, do not comment out + */ + public static void main(String args[]) { + if (args.length < 2) { + System.err.println("Usage: GeoIP infile1.csv [infile2.csv...] outfile.dat.gz"); + System.exit(1); + } + List infiles = new ArrayList(); + for (int i = 0; i < args.length - 1; i++) { + infiles.add(new File(args[i])); + } + File outfile = new File(args[args.length - 1]); + boolean success = compressGeoIPv6CSVFiles(infiles, outfile); + if (!success) { + System.err.println("Failed"); + System.exit(1); + } + // readback for testing + readGeoIPFile(outfile, new Long[] { Long.MAX_VALUE }, Collections.EMPTY_MAP, new Log(GeoIPv6.class)); + } +} diff --git a/router/java/src/net/i2p/router/transport/GetBidsJob.java b/router/java/src/net/i2p/router/transport/GetBidsJob.java index 85da18c13f..676f72e163 100644 --- a/router/java/src/net/i2p/router/transport/GetBidsJob.java +++ b/router/java/src/net/i2p/router/transport/GetBidsJob.java @@ -23,22 +23,25 @@ import net.i2p.util.Log; */ class GetBidsJob extends JobImpl { private final Log _log; - private final CommSystemFacadeImpl _facade; + private final TransportManager _tmgr; private final OutNetMessage _msg; - public GetBidsJob(RouterContext ctx, CommSystemFacadeImpl facade, OutNetMessage msg) { + /** + * @deprecated unused, see static getBids() + */ + public GetBidsJob(RouterContext ctx, TransportManager tmgr, OutNetMessage msg) { super(ctx); _log = ctx.logManager().getLog(GetBidsJob.class); - _facade = facade; + _tmgr = tmgr; _msg = msg; } public String getName() { return "Fetch bids for a message to be delivered"; } public void runJob() { - getBids(getContext(), _facade, _msg); + getBids(getContext(), _tmgr, _msg); } - static void getBids(RouterContext context, CommSystemFacadeImpl facade, OutNetMessage msg) { + static void getBids(RouterContext context, TransportManager tmgr, OutNetMessage msg) { Log log = context.logManager().getLog(GetBidsJob.class); Hash to = msg.getTarget().getIdentity().getHash(); msg.timestamp("bid"); @@ -61,14 +64,14 @@ class GetBidsJob extends JobImpl { return; } - TransportBid bid = facade.getNextBid(msg); + TransportBid bid = tmgr.getNextBid(msg); if (bid == null) { int failedCount = msg.getFailedTransports().size(); if (failedCount == 0) { context.statManager().addRateData("transport.bidFailNoTransports", msg.getLifetime()); // This used to be "no common transports" but it is almost always no transports at all context.banlist().banlistRouter(to, _x("No transports (hidden or starting up?)")); - } else if (failedCount >= facade.getTransportCount()) { + } else if (failedCount >= tmgr.getTransportCount()) { context.statManager().addRateData("transport.bidFailAllTransports", msg.getLifetime()); // fail after all transports were unsuccessful context.netDb().fail(to); @@ -82,7 +85,7 @@ class GetBidsJob extends JobImpl { } - private static void fail(RouterContext context, OutNetMessage msg) { + static void fail(RouterContext context, OutNetMessage msg) { if (msg.getOnFailedSendJob() != null) { context.jobQueue().addJob(msg.getOnFailedSendJob()); } diff --git a/router/java/src/net/i2p/router/transport/Transport.java b/router/java/src/net/i2p/router/transport/Transport.java index 4bece170db..954b3d1d7e 100644 --- a/router/java/src/net/i2p/router/transport/Transport.java +++ b/router/java/src/net/i2p/router/transport/Transport.java @@ -32,17 +32,90 @@ public interface Transport { * */ public void send(OutNetMessage msg); - public RouterAddress startListening(); + public void startListening(); public void stopListening(); - public RouterAddress getCurrentAddress(); - public RouterAddress updateAddress(); - public static final String SOURCE_UPNP = "upnp"; - public static final String SOURCE_INTERFACE = "local"; - public static final String SOURCE_CONFIG = "config"; // unused - public void externalAddressReceived(String source, byte[] ip, int port); - public void forwardPortStatus(int port, int externalPort, boolean success, String reason); + + /** + * What addresses are we currently listening to? + * Replaces getCurrentAddress() + * @return all addresses, non-null + * @since IPv6 + */ + public List getCurrentAddresses(); + + /** + * Do we have any current address? + * @since IPv6 + */ + public boolean hasCurrentAddress(); + + /** + * Ask the transport to update its addresses based on current information and return them + * @return all addresses, non-null + */ + public List updateAddress(); + + /** + * @since IPv6 + */ + public enum AddressSource { + SOURCE_UPNP("upnp"), + SOURCE_INTERFACE("local"), + /** unused */ + SOURCE_CONFIG("config"), + SOURCE_SSU("ssu"); + + private final String cfgstr; + + AddressSource(String cfgstr) { + this.cfgstr = cfgstr; + } + + public String toConfigString() { + return cfgstr; + } + } + + /** + * Notify a transport of an external address change. + * This may be from a local interface, UPnP, a config change, etc. + * This should not be called if the ip didn't change + * (from that source's point of view), or is a local address. + * May be called multiple times for IPv4 or IPv6. + * The transport should also do its own checking on whether to accept + * notifications from this source. + * + * This can be called before startListening() to set an initial address, + * or after the transport is running. + * + * @param source defined in Transport.java + * @param ip typ. IPv4 or IPv6 non-local; may be null to indicate IPv4 failure or port info only + * @param port 0 for unknown or unchanged + */ + public void externalAddressReceived(AddressSource source, byte[] ip, int port); + + /** + * Notify a transport of the results of trying to forward a port. + * + * @param ip may be null + * @param port the internal port + * @param externalPort the external port, which for now should always be the same as + * the internal port if the forwarding was successful. + */ + public void forwardPortStatus(byte[] ip, int port, int externalPort, boolean success, String reason); + + /** + * What INTERNAL port would the transport like to have forwarded by UPnP. + * This can't be passed via getCurrentAddress(), as we have to open the port + * before we can publish the address, and that's the external port anyway. + * + * @return port or -1 for none or 0 for any + */ public int getRequestedPort(); + + /** Who to notify on message availability */ public void setListener(TransportEventListener listener); + public String getStyle(); public int countPeers(); @@ -51,12 +124,17 @@ public interface Transport { public boolean haveCapacity(); public boolean haveCapacity(int pct); public Vector getClockSkews(); - public List getMostRecentErrorMessages(); + public List getMostRecentErrorMessages(); public void renderStatusHTML(Writer out, String urlBase, int sortFlags) throws IOException; public short getReachabilityStatus(); public void recheckReachability(); public boolean isBacklogged(Hash dest); + + /** + * Was the peer UNreachable (outbound only) the last time we tried it? + * This is NOT reset if the peer contacts us and it is never expired. + */ public boolean wasUnreachable(Hash dest); public boolean isUnreachable(Hash peer); diff --git a/router/java/src/net/i2p/router/transport/TransportImpl.java b/router/java/src/net/i2p/router/transport/TransportImpl.java index fc24856be9..60752f1d19 100644 --- a/router/java/src/net/i2p/router/transport/TransportImpl.java +++ b/router/java/src/net/i2p/router/transport/TransportImpl.java @@ -10,8 +10,11 @@ package net.i2p.router.transport; import java.io.IOException; import java.io.Writer; +import java.net.InetAddress; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; +import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.Iterator; @@ -23,6 +26,7 @@ import java.util.Vector; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CopyOnWriteArrayList; import net.i2p.data.DataHelper; import net.i2p.data.Hash; @@ -36,7 +40,6 @@ import net.i2p.router.MessageSelector; import net.i2p.router.OutNetMessage; import net.i2p.router.Router; import net.i2p.router.RouterContext; -import net.i2p.router.networkdb.kademlia.FloodfillNetworkDatabaseFacade; import net.i2p.util.ConcurrentHashSet; import net.i2p.util.LHMCache; import net.i2p.util.Log; @@ -51,13 +54,14 @@ import net.i2p.util.SystemVersion; public abstract class TransportImpl implements Transport { private final Log _log; private TransportEventListener _listener; - private RouterAddress _currentAddress; + protected final List _currentAddresses; // Only used by NTCP. SSU does not use. See send() below. private final BlockingQueue _sendPool; protected final RouterContext _context; /** map from routerIdentHash to timestamp (Long) that the peer was last unreachable */ private final Map _unreachableEntries; private final Set _wasUnreachableEntries; + private final Set _localAddresses; /** global router ident -> IP */ private static final Map _IPMap; @@ -88,12 +92,15 @@ public abstract class TransportImpl implements Transport { _context.statManager().createRequiredRateStat("transport.sendProcessingTime", "Time to process and send a message (ms)", "Transport", new long[] { 60*1000l, 10*60*1000l, 60*60*1000l, 24*60*60*1000l }); //_context.statManager().createRateStat("transport.sendProcessingTime." + getStyle(), "Time to process and send a message (ms)", "Transport", new long[] { 60*1000l }); _context.statManager().createRateStat("transport.expiredOnQueueLifetime", "How long a message that expires on our outbound queue is processed", "Transport", new long[] { 60*1000l, 10*60*1000l, 60*60*1000l, 24*60*60*1000l } ); + + _currentAddresses = new CopyOnWriteArrayList(); if (getStyle().equals("NTCP")) _sendPool = new ArrayBlockingQueue(8); else _sendPool = null; _unreachableEntries = new HashMap(16); _wasUnreachableEntries = new ConcurrentHashSet(16); + _localAddresses = new ConcurrentHashSet(4); _context.simpleScheduler().addPeriodicEvent(new CleanupUnreachable(), 2 * UNREACHABLE_PERIOD, UNREACHABLE_PERIOD / 2); } @@ -119,6 +126,9 @@ public abstract class TransportImpl implements Transport { /** Per-transport connection limit */ public int getMaxConnections() { + if (_context.commSystem().isDummy()) + // testing + return 0; String style = getStyle(); // object churn String maxProp; @@ -135,7 +145,7 @@ public abstract class TransportImpl implements Transport { if (bw > Router.CAPABILITY_BW12 && bw <= Router.CAPABILITY_BW256) def *= (1 + bw - Router.CAPABILITY_BW12); } - if (((FloodfillNetworkDatabaseFacade)_context.netDb()).floodfillEnabled()) { + if (_context.netDb().floodfillEnabled()) { // && !SystemVersion.isWindows()) { def *= 17; def /= 10; // 425 for Class O ff } @@ -167,7 +177,7 @@ public abstract class TransportImpl implements Transport { */ public Vector getClockSkews() { return new Vector(); } - public List getMostRecentErrorMessages() { return Collections.EMPTY_LIST; } + public List getMostRecentErrorMessages() { return Collections.EMPTY_LIST; } /** * Nonblocking call to pull the next outbound message @@ -463,63 +473,200 @@ public abstract class TransportImpl implements Transport { } /** Do we increase the advertised cost when approaching conn limits? */ - public static final boolean ADJUST_COST = true; + protected static final boolean ADJUST_COST = true; + protected static final int CONGESTION_COST_ADJUSTMENT = 2; - /** What addresses are we currently listening to? */ - public RouterAddress getCurrentAddress() { - return _currentAddress; + /** + * What addresses are we currently listening to? + * Replaces getCurrentAddress() + * @return all addresses, non-null + * @since IPv6 + */ + public List getCurrentAddresses() { + return _currentAddresses; + } + + /** + * What address are we currently listening to? + * Replaces getCurrentAddress() + * @param ipv6 true for IPv6 only; false for IPv4 only + * @return first matching address or null + * @since IPv6 + */ + public RouterAddress getCurrentAddress(boolean ipv6) { + for (RouterAddress ra : _currentAddresses) { + if (ipv6 == TransportUtil.isIPv6(ra)) + return ra; + } + return null; + } + + /** + * Do we have any current address? + * @since IPv6 + */ + public boolean hasCurrentAddress() { + return !_currentAddresses.isEmpty(); } /** * Ask the transport to update its address based on current information and return it * Transports should override. + * @return all addresses, non-null * @since 0.7.12 */ - public RouterAddress updateAddress() { - return _currentAddress; + public List updateAddress() { + return _currentAddresses; } /** - * Replace any existing addresses for the current transport with the given - * one. + * Replace any existing addresses for the current transport + * with the same IP length (4 or 16) with the given one. + * TODO: Allow multiple addresses of the same length. + * Calls listener.transportAddressChanged() + * + * @param address null to remove all */ protected void replaceAddress(RouterAddress address) { - // _log.error("Replacing address for " + getStyle() + " was " + _currentAddress + " now " + address); - _currentAddress = address; + if (_log.shouldLog(Log.WARN)) + _log.warn("Replacing address with " + address, new Exception()); + if (address == null) { + _currentAddresses.clear(); + } else { + boolean isIPv6 = TransportUtil.isIPv6(address); + for (RouterAddress ra : _currentAddresses) { + if (isIPv6 == TransportUtil.isIPv6(ra)) + _currentAddresses.remove(ra); + } + _currentAddresses.add(address); + } + if (_log.shouldLog(Log.WARN)) + _log.warn(getStyle() + " now has " + _currentAddresses.size() + " addresses"); if (_listener != null) _listener.transportAddressChanged(); } + /** + * Save a local address we were notified about before we started. + * + * @since IPv6 + */ + protected void saveLocalAddress(InetAddress address) { + _localAddresses.add(address); + } + + /** + * Return and then clear all saved local addresses. + * + * @since IPv6 + */ + protected Collection getSavedLocalAddresses() { + List rv = new ArrayList(_localAddresses); + _localAddresses.clear(); + return rv; + } + + /** + * Get all available address we can use, + * shuffled and then sorted by cost/preference. + * Lowest cost (most preferred) first. + * @return non-null, possibly empty + * @since IPv6 + */ + protected List getTargetAddresses(RouterInfo target) { + List rv = target.getTargetAddresses(getStyle()); + // Shuffle so everybody doesn't use the first one + if (rv.size() > 1) { + Collections.shuffle(rv, _context.random()); + TransportUtil.IPv6Config config = getIPv6Config(); + int adj; + switch (config) { + case IPV6_DISABLED: + adj = 10; break; + case IPV6_NOT_PREFERRED: + adj = 1; break; + default: + case IPV6_ENABLED: + adj = 0; break; + case IPV6_PREFERRED: + adj = -1; break; + case IPV6_ONLY: + adj = -10; break; + } + Collections.sort(rv, new AddrComparator(adj)); + } + return rv; + } + + /** + * Compare based on published cost, adjusting for our IPv6 preference. + * Lowest cost (most preferred) first. + * @since IPv6 + */ + private static class AddrComparator implements Comparator { + private final int adj; + + public AddrComparator(int ipv6Adjustment) { + adj = ipv6Adjustment; + } + + public int compare(RouterAddress l, RouterAddress r) { + int lc = l.getCost(); + int rc = r.getCost(); + byte[] lip = l.getIP(); + byte[] rip = r.getIP(); + if (lip == null) + lc += 20; + else if (lip.length == 16) + lc += adj; + if (rip == null) + rc += 20; + else if (rip.length == 16) + rc += adj; + if (lc > rc) + return 1; + if (lc < rc) + return -1; + return 0; + } + } + /** * Notify a transport of an external address change. * This may be from a local interface, UPnP, a config change, etc. * This should not be called if the ip didn't change - * (from that source's point of view), or is a local address, - * or if the ip is IPv6, but the transport should check anyway. + * (from that source's point of view), or is a local address. + * May be called multiple times for IPv4 or IPv6. * The transport should also do its own checking on whether to accept * notifications from this source. * * This can be called before startListening() to set an initial address, * or after the transport is running. * + * This implementation does nothing. Transports should override if they want notification. + * * @param source defined in Transport.java - * @param ip typ. IPv4 non-local + * @param ip typ. IPv4 or IPv6 non-local; may be null to indicate IPv4 failure or port info only * @param port 0 for unknown or unchanged */ - public void externalAddressReceived(String source, byte[] ip, int port) {} + public void externalAddressReceived(AddressSource source, byte[] ip, int port) {} /** * Notify a transport of the results of trying to forward a port. + * + * This implementation does nothing. Transports should override if they want notification. + * + * @param ip may be null * @param port the internal port * @param externalPort the external port, which for now should always be the same as * the internal port if the forwarding was successful. */ - public void forwardPortStatus(int port, int externalPort, boolean success, String reason) {} + public void forwardPortStatus(byte[] ip, int port, int externalPort, boolean success, String reason) {} /** - * What port would the transport like to have forwarded by UPnP. + * What INTERNAL port would the transport like to have forwarded by UPnP. * This can't be passed via getCurrentAddress(), as we have to open the port - * before we can publish the address. + * before we can publish the address, and that's the external port anyway. * * @return port or -1 for none or 0 for any */ @@ -531,13 +678,13 @@ public abstract class TransportImpl implements Transport { public void renderStatusHTML(Writer out) throws IOException {} public void renderStatusHTML(Writer out, String urlBase, int sortFlags) throws IOException { renderStatusHTML(out); } - public RouterContext getContext() { return _context; } public short getReachabilityStatus() { return CommSystemFacade.STATUS_UNKNOWN; } public void recheckReachability() {} public boolean isBacklogged(Hash dest) { return false; } public boolean isEstablished(Hash dest) { return false; } private static final long UNREACHABLE_PERIOD = 5*60*1000; + public boolean isUnreachable(Hash peer) { long now = _context.clock().now(); synchronized (_unreachableEntries) { @@ -551,6 +698,7 @@ public abstract class TransportImpl implements Transport { } } } + /** called when we can't reach a peer */ /** This isn't very useful since it is cleared when they contact us */ public void markUnreachable(Hash peer) { @@ -560,6 +708,7 @@ public abstract class TransportImpl implements Transport { } markWasUnreachable(peer, true); } + /** called when we establish a peer connection (outbound or inbound) */ public void markReachable(Hash peer, boolean isInbound) { // if *some* transport can reach them, then we shouldn't banlist 'em @@ -605,9 +754,15 @@ public abstract class TransportImpl implements Transport { else _wasUnreachableEntries.remove(peer); if (_log.shouldLog(Log.INFO)) - _log.info(this.getStyle() + " setting wasUnreachable to " + yes + " for " + peer); + _log.info(this.getStyle() + " setting wasUnreachable to " + yes + " for " + peer, + yes ? new Exception() : null); } + /** + * IP of the peer from the last connection (in or out, any transport). + * + * @param IPv4 or IPv6, non-null + */ public void setIP(Hash peer, byte[] ip) { byte[] old; synchronized (_IPMap) { @@ -617,6 +772,11 @@ public abstract class TransportImpl implements Transport { _context.commSystem().queueLookup(ip); } + /** + * IP of the peer from the last connection (in or out, any transport). + * + * @return IPv4 or IPv6 or null + */ public static byte[] getIP(Hash peer) { synchronized (_IPMap) { return _IPMap.get(peer); @@ -632,26 +792,20 @@ public abstract class TransportImpl implements Transport { } } - /** @param addr non-null */ - public static boolean isPubliclyRoutable(byte addr[]) { - if (addr.length == 4) { - int a0 = addr[0] & 0xFF; - if (a0 == 127) return false; - if (a0 == 10) return false; - int a1 = addr[1] & 0xFF; - if (a0 == 172 && a1 >= 16 && a1 <= 31) return false; - if (a0 == 192 && a1 == 168) return false; - if (a0 >= 224) return false; // no multicast - if (a0 == 0) return false; - if (a0 == 169 && a1 == 254) return false; - // 5/8 allocated to RIPE (30 November 2010) - //if ((addr[0]&0xFF) == 5) return false; // Hamachi - return true; // or at least possible to be true - } else if (addr.length == 16) { - return false; - } else { - // ipv? - return false; - } + /** + * @since IPv6 + */ + protected TransportUtil.IPv6Config getIPv6Config() { + return TransportUtil.getIPv6Config(_context, getStyle()); + } + + /** + * Allows IPv6 only if the transport is configured for it. + * Caller must check if we actually have a public IPv6 address. + * @param addr non-null + */ + protected boolean isPubliclyRoutable(byte addr[]) { + return TransportUtil.isPubliclyRoutable(addr, + getIPv6Config() != TransportUtil.IPv6Config.IPV6_DISABLED); } } diff --git a/router/java/src/net/i2p/router/transport/TransportManager.java b/router/java/src/net/i2p/router/transport/TransportManager.java index f6a7c71f80..974b75696a 100644 --- a/router/java/src/net/i2p/router/transport/TransportManager.java +++ b/router/java/src/net/i2p/router/transport/TransportManager.java @@ -14,6 +14,7 @@ import java.net.InetAddress; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -29,6 +30,7 @@ import net.i2p.data.i2np.I2NPMessage; import net.i2p.router.CommSystemFacade; import net.i2p.router.OutNetMessage; import net.i2p.router.RouterContext; +import static net.i2p.router.transport.Transport.AddressSource.*; import net.i2p.router.transport.crypto.DHSessionKeyBuilder; import net.i2p.router.transport.ntcp.NTCPTransport; import net.i2p.router.transport.udp.UDPTransport; @@ -85,13 +87,24 @@ public class TransportManager implements TransportEventListener { private void configTransports() { boolean enableUDP = _context.getBooleanPropertyDefaultTrue(PROP_ENABLE_UDP); + Transport udp = null; if (enableUDP) { - UDPTransport udp = new UDPTransport(_context, _dhThread); + udp = new UDPTransport(_context, _dhThread); addTransport(udp); initializeAddress(udp); } - if (isNTCPEnabled(_context)) - addTransport(new NTCPTransport(_context, _dhThread)); + if (isNTCPEnabled(_context)) { + Transport ntcp = new NTCPTransport(_context, _dhThread); + addTransport(ntcp); + initializeAddress(ntcp); + if (udp != null) { + // pass along the port SSU is probably going to use + // so that NTCP may bind early + int port = udp.getRequestedPort(); + if (port > 0) + ntcp.externalAddressReceived(SOURCE_CONFIG, null, port); + } + } if (_transports.isEmpty()) _log.log(Log.CRIT, "No transports are enabled"); } @@ -100,40 +113,44 @@ public class TransportManager implements TransportEventListener { return ctx.getBooleanPropertyDefaultTrue(PROP_ENABLE_NTCP); } + /** + * Notify transport of ALL routable interface addresses, including IPv6. + * It's the transport's job to ignore what it can't handle. + */ private void initializeAddress(Transport t) { - String ips = Addresses.getAnyAddress(); - if (ips == null) - return; - InetAddress ia; - try { - ia = InetAddress.getByName(ips); - } catch (UnknownHostException e) { - _log.error("UDP failed to bind to local address", e); - return; + Set ipset = Addresses.getAddresses(false, true); // non-local, include IPv6 + for (String ips : ipset) { + try { + InetAddress ia = InetAddress.getByName(ips); + byte[] ip = ia.getAddress(); + t.externalAddressReceived(SOURCE_INTERFACE, ip, 0); + } catch (UnknownHostException e) { + _log.error("UDP failed to bind to local address", e); + } } - byte[] ip = ia.getAddress(); - t.externalAddressReceived(Transport.SOURCE_INTERFACE, ip, 0); } /** - * callback from UPnP - * Only tell SSU, it will tell NTCP + * Initialize from interfaces, and callback from UPnP or SSU. + * Tell all transports... but don't loop * */ - public void externalAddressReceived(String source, byte[] ip, int port) { - Transport t = getTransport(UDPTransport.STYLE); - if (t != null) - t.externalAddressReceived(source, ip, port); + public void externalAddressReceived(Transport.AddressSource source, byte[] ip, int port) { + for (Transport t : _transports.values()) { + // don't loop + if (!(source == SOURCE_SSU && t.getStyle().equals(UDPTransport.STYLE))) + t.externalAddressReceived(source, ip, port); + } } /** * callback from UPnP * */ - public void forwardPortStatus(String style, int port, int externalPort, boolean success, String reason) { + public void forwardPortStatus(String style, byte[] ip, int port, int externalPort, boolean success, String reason) { Transport t = getTransport(style); if (t != null) - t.forwardPortStatus(port, externalPort, success, reason); + t.forwardPortStatus(ip, port, externalPort, success, reason); } public synchronized void startListening() { @@ -148,7 +165,17 @@ public class TransportManager implements TransportEventListener { _upnpManager.start(); configTransports(); _log.debug("Starting up the transport manager"); - for (Transport t : _transports.values()) { + // Let's do this in a predictable order to make testing easier + // Start NTCP first so it can get notified from SSU + List tps = new ArrayList(); + Transport tp = getTransport(NTCPTransport.STYLE); + if (tp != null) + tps.add(tp); + tp = getTransport(UDPTransport.STYLE); + if (tp != null) + tps.add(tp); + //for (Transport t : _transports.values()) { + for (Transport t : tps) { t.startListening(); if (_log.shouldLog(Log.DEBUG)) _log.debug("Transport " + t.getStyle() + " started"); @@ -248,7 +275,7 @@ public class TransportManager implements TransportEventListener { */ public boolean haveInboundCapacity(int pct) { for (Transport t : _transports.values()) { - if (t.getCurrentAddress() != null && t.haveCapacity(pct)) + if (t.hasCurrentAddress() && t.haveCapacity(pct)) return true; } return false; @@ -266,8 +293,8 @@ public class TransportManager implements TransportEventListener { if ((tempSkews == null) || (tempSkews.isEmpty())) continue; skews.addAll(tempSkews); } - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Transport manager returning " + skews.size() + " peer clock skews."); + //if (_log.shouldLog(Log.DEBUG)) + // _log.debug("Transport manager returning " + skews.size() + " peer clock skews."); return skews; } @@ -324,6 +351,8 @@ public class TransportManager implements TransportEventListener { * * For blocking purposes, etc. it's worth checking both * the netDb addresses and this address. + * + * @return IPv4 or IPv6 or null */ public byte[] getIP(Hash dest) { return TransportImpl.getIP(dest); @@ -332,35 +361,62 @@ public class TransportManager implements TransportEventListener { /** * This forces a rebuild */ - public Map getAddresses() { - Map rv = new HashMap(_transports.size()); + public List getAddresses() { + List rv = new ArrayList(4); // do this first since SSU may force a NTCP change for (Transport t : _transports.values()) t.updateAddress(); for (Transport t : _transports.values()) { - if (t.getCurrentAddress() != null) - rv.put(t.getStyle(), t.getCurrentAddress()); + rv.addAll(t.getCurrentAddresses()); } return rv; } /** - * The actual or requested INTERNAL ports, for each transport, - * which we will pass along to UPnP to be forwarded. + * @since IPv6 */ - private Map getPorts() { - Map rv = new HashMap(_transports.size()); + static class Port { + public final String style; + public final int port; + + public Port(String style, int port) { + this.style = style; + this.port = port; + } + + @Override + public int hashCode() { + return style.hashCode() ^ port; + } + + @Override + public boolean equals(Object o) { + if (o == null) + return false; + if (! (o instanceof Port)) + return false; + Port p = (Port) o; + return port == p.port && style.equals(p.style); + } + } + + /** + * Include the published port, or the requested port, for each transport + * which we will pass along to UPnP + */ + private Set getPorts() { + Set rv = new HashSet(4); for (Transport t : _transports.values()) { int port = t.getRequestedPort(); // Use UDP port for NTCP too - see comment in NTCPTransport.getRequestedPort() for why this is here if (t.getStyle().equals(NTCPTransport.STYLE) && port <= 0 && - _context.getBooleanProperty(CommSystemFacadeImpl.PROP_I2NP_NTCP_AUTO_PORT)) { + _context.getBooleanProperty(NTCPTransport.PROP_I2NP_NTCP_AUTO_PORT)) { Transport udp = getTransport(UDPTransport.STYLE); if (udp != null) port = t.getRequestedPort(); } if (port > 0) - rv.put(t.getStyle(), Integer.valueOf(port)); + rv.add(new Port(t.getStyle(), port)); } return rv; } @@ -461,11 +517,11 @@ public class TransportManager implements TransportEventListener { */ public void messageReceived(I2NPMessage message, RouterIdentity fromRouter, Hash fromRouterHash) { if (_log.shouldLog(Log.DEBUG)) - _log.debug("I2NPMessage received: " + message.getClass().getName(), new Exception("Where did I come from again?")); + _log.debug("I2NPMessage received: " + message.getClass().getSimpleName() /*, new Exception("Where did I come from again?") */ ); try { _context.inNetMessagePool().add(message, fromRouter, fromRouterHash); - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Added to in pool"); + //if (_log.shouldLog(Log.DEBUG)) + // _log.debug("Added to in pool"); } catch (IllegalArgumentException iae) { if (_log.shouldLog(Log.WARN)) _log.warn("Error receiving message", iae); @@ -477,8 +533,8 @@ public class TransportManager implements TransportEventListener { _upnpManager.update(getPorts()); } - public List getMostRecentErrorMessages() { - List rv = new ArrayList(16); + public List getMostRecentErrorMessages() { + List rv = new ArrayList(16); for (Transport t : _transports.values()) { rv.addAll(t.getMostRecentErrorMessages()); } @@ -502,11 +558,15 @@ public class TransportManager implements TransportEventListener { StringBuilder buf = new StringBuilder(4*1024); buf.append("

").append(_("Router Transport Addresses")).append("

\n");
         for (Transport t : _transports.values()) {
-            if (t.getCurrentAddress() != null)
-                buf.append(t.getCurrentAddress());
-            else
+            if (t.hasCurrentAddress()) {
+                for (RouterAddress ra : t.getCurrentAddresses()) {
+                    buf.append(ra.toString());
+                    buf.append("\n\n");
+                }
+            } else {
                 buf.append(_("{0} is used for outbound connections only", t.getStyle()));
-            buf.append("\n\n");
+                buf.append("\n\n");
+            }
         }
         buf.append("
\n"); out.write(buf.toString()); diff --git a/router/java/src/net/i2p/router/transport/TransportUtil.java b/router/java/src/net/i2p/router/transport/TransportUtil.java new file mode 100644 index 0000000000..b41a72e0e5 --- /dev/null +++ b/router/java/src/net/i2p/router/transport/TransportUtil.java @@ -0,0 +1,131 @@ +package net.i2p.router.transport; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind { either expressed or implied. + * It probably won't make your computer catch on fire { or eat + * your children { but it might. Use at your own risk. + * + */ + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.HashMap; +import java.util.Map; + +import net.i2p.data.RouterAddress; +import net.i2p.router.RouterContext; + +/** + * @since IPv6 + */ +public abstract class TransportUtil { + + public static final String NTCP_IPV6_CONFIG = "i2np.ntcp.ipv6"; + public static final String SSU_IPV6_CONFIG = "i2np.udp.ipv6"; + + public enum IPv6Config { + /** IPv6 disabled */ + IPV6_DISABLED("false"), + + /** lower priority than IPv4 */ + IPV6_NOT_PREFERRED("preferIPv4"), + + /** equal priority to IPv4 */ + IPV6_ENABLED("enable"), + + /** higher priority than IPv4 */ + IPV6_PREFERRED("preferIPv6"), + + /** IPv4 disabled */ + IPV6_ONLY("only"); + + private final String cfgstr; + + IPv6Config(String cfgstr) { + this.cfgstr = cfgstr; + } + + public String toConfigString() { + return cfgstr; + } + } + + private static final Map BY_NAME = new HashMap(); + private static final IPv6Config DEFAULT_IPV6_CONFIG = IPv6Config.IPV6_DISABLED; + + static { + for (IPv6Config cfg : IPv6Config.values()) { + BY_NAME.put(cfg.toConfigString(), cfg); + } + // alias + BY_NAME.put("true", IPv6Config.IPV6_ENABLED); + } + + public static IPv6Config getIPv6Config(RouterContext ctx, String transportStyle) { + String cfg; + if (transportStyle.equals("NTCP")) + cfg = ctx.getProperty(NTCP_IPV6_CONFIG); + else if (transportStyle.equals("SSU")) + cfg = ctx.getProperty(SSU_IPV6_CONFIG); + else + return DEFAULT_IPV6_CONFIG; + return getIPv6Config(cfg); + } + + public static IPv6Config getIPv6Config(String cfg) { + if (cfg == null) + return DEFAULT_IPV6_CONFIG; + IPv6Config c = BY_NAME.get(cfg); + if (c != null) + return c; + return DEFAULT_IPV6_CONFIG; + } + + /** + * Addresses without a host (i.e. w/introducers) + * are assumed to be IPv4 + */ + public static boolean isIPv6(RouterAddress addr) { + // do this the fast way, without calling getIP() to parse the host string + String host = addr.getOption(RouterAddress.PROP_HOST); + return host != null && host.contains(":"); + } + + /** + * @param addr non-null + * @since IPv6 moved from TransportImpl + */ + public static boolean isPubliclyRoutable(byte addr[], boolean allowIPv6) { + if (addr.length == 4) { + int a0 = addr[0] & 0xFF; + if (a0 == 127) return false; + if (a0 == 10) return false; + int a1 = addr[1] & 0xFF; + if (a0 == 172 && a1 >= 16 && a1 <= 31) return false; + if (a0 == 192 && a1 == 168) return false; + if (a0 >= 224) return false; // no multicast + if (a0 == 0) return false; + if (a0 == 169 && a1 == 254) return false; + // 5/8 allocated to RIPE (30 November 2010) + //if ((addr[0]&0xFF) == 5) return false; // Hamachi + return true; // or at least possible to be true + } else if (addr.length == 16) { + if (allowIPv6) { + // disallow 2002::/16 (6to4 RFC 3056) + if (addr[0] == 0x20 && addr[1] == 0x02) + return false; + try { + InetAddress ia = InetAddress.getByAddress(addr); + return + (!ia.isLinkLocalAddress()) && + (!ia.isMulticastAddress()) && + (!ia.isAnyLocalAddress()) && + (!ia.isLoopbackAddress()) && + (!ia.isSiteLocalAddress()); + } catch (UnknownHostException uhe) {} + } + } + return false; + } +} diff --git a/router/java/src/net/i2p/router/transport/UPnP.java b/router/java/src/net/i2p/router/transport/UPnP.java index 8d4dd1a6bc..40c5efbbd1 100644 --- a/router/java/src/net/i2p/router/transport/UPnP.java +++ b/router/java/src/net/i2p/router/transport/UPnP.java @@ -52,6 +52,7 @@ import org.freenetproject.ForwardPortStatus; * * @see "http://www.upnp.org/" * @see "http://en.wikipedia.org/wiki/Universal_Plug_and_Play" + * @since 0.7.4 */ /* @@ -147,7 +148,7 @@ class UPnP extends ControlPoint implements DeviceChangeListener, EventListener { InetAddress detectedIP = InetAddress.getByName(natAddress); short status = DetectedIP.NOT_SUPPORTED; - thinksWeAreDoubleNatted = !TransportImpl.isPubliclyRoutable(detectedIP.getAddress()); + thinksWeAreDoubleNatted = !TransportUtil.isPubliclyRoutable(detectedIP.getAddress(), false); // If we have forwarded a port AND we don't have a private address if (_log.shouldLog(Log.WARN)) _log.warn("NATAddress: \"" + natAddress + "\" detectedIP: " + detectedIP + " double? " + thinksWeAreDoubleNatted); @@ -843,7 +844,11 @@ class UPnP extends ControlPoint implements DeviceChangeListener, EventListener { fps = new ForwardPortStatus(ForwardPortStatus.PROBABLE_FAILURE, "UPnP port forwarding apparently failed", port.portNumber); } Map map = Collections.singletonMap(port, fps); - forwardCallback.portForwardStatus(map); + try { + forwardCallback.portForwardStatus(map); + } catch (Exception e) { + _log.error("UPnP RPT error", e); + } } } } diff --git a/router/java/src/net/i2p/router/transport/UPnPManager.java b/router/java/src/net/i2p/router/transport/UPnPManager.java index 63acc65c09..b3fb90cc2b 100644 --- a/router/java/src/net/i2p/router/transport/UPnPManager.java +++ b/router/java/src/net/i2p/router/transport/UPnPManager.java @@ -11,6 +11,7 @@ import java.util.Map; import java.util.Set; import net.i2p.router.RouterContext; +import static net.i2p.router.transport.Transport.AddressSource.SOURCE_UPNP; import net.i2p.util.Addresses; import net.i2p.util.Log; import net.i2p.util.Translate; @@ -25,6 +26,7 @@ import org.freenetproject.ForwardPortStatus; * Bridge from the I2P RouterAddress data structure to * the freenet data structures * + * @since 0.7.4 * @author zzz */ class UPnPManager { @@ -106,7 +108,7 @@ class UPnPManager { * which can have multiple UPnP threads running at once, but * that should be ok. */ - public void update(Map ports) { + public void update(Set ports) { if (_log.shouldLog(Log.DEBUG)) _log.debug("UPnP Update with " + ports.size() + " ports"); @@ -121,9 +123,9 @@ class UPnPManager { //} Set forwards = new HashSet(ports.size()); - for (Map.Entry entry : ports.entrySet()) { - String style = entry.getKey(); - int port = entry.getValue().intValue(); + for (TransportManager.Port entry : ports) { + String style = entry.style; + int port = entry.port; int protocol = -1; if ("SSU".equals(style)) protocol = ForwardPort.PROTOCOL_UDP_IPV4; @@ -151,17 +153,19 @@ class UPnPManager { if (_log.shouldLog(Log.DEBUG)) _log.debug("UPnP Callback:"); + byte[] ipaddr = null; DetectedIP[] ips = _upnp.getAddress(); if (ips != null) { for (DetectedIP ip : ips) { // store the first public one and tell the transport manager if it changed - if (TransportImpl.isPubliclyRoutable(ip.publicAddress.getAddress())) { + if (TransportUtil.isPubliclyRoutable(ip.publicAddress.getAddress(), false)) { if (_log.shouldLog(Log.DEBUG)) _log.debug("External address: " + ip.publicAddress + " type: " + ip.natType); if (!ip.publicAddress.equals(_detectedAddress)) { _detectedAddress = ip.publicAddress; - _manager.externalAddressReceived(Transport.SOURCE_UPNP, _detectedAddress.getAddress(), 0); + _manager.externalAddressReceived(SOURCE_UPNP, _detectedAddress.getAddress(), 0); } + ipaddr = ip.publicAddress.getAddress(); break; } } @@ -184,7 +188,7 @@ class UPnPManager { else continue; boolean success = fps.status >= ForwardPortStatus.MAYBE_SUCCESS; - _manager.forwardPortStatus(style, fp.portNumber, fps.externalPort, success, fps.reasonString); + _manager.forwardPortStatus(style, ipaddr, fp.portNumber, fps.externalPort, success, fps.reasonString); } } } 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 04da0506b4..9955268e98 100644 --- a/router/java/src/net/i2p/router/transport/ntcp/EstablishState.java +++ b/router/java/src/net/i2p/router/transport/ntcp/EstablishState.java @@ -261,8 +261,8 @@ class EstablishState { // ok, we are onto the encrypted area while (src.hasRemaining() && !_corrupt) { - if (_log.shouldLog(Log.DEBUG)) - _log.debug(prefix()+"Encrypted bytes available (" + src.hasRemaining() + ")"); + //if (_log.shouldLog(Log.DEBUG)) + // _log.debug(prefix()+"Encrypted bytes available (" + src.hasRemaining() + ")"); while (_curEncryptedOffset < _curEncrypted.length && src.hasRemaining()) { _curEncrypted[_curEncryptedOffset++] = src.get(); _received++; @@ -299,8 +299,8 @@ class EstablishState { } 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 (_log.shouldLog(Log.DEBUG)) + // _log.debug(prefix()+"subsequent block decrypted (" + _sz_aliceIdent_tsA_padding_aliceSig.size() + ")"); if (_sz_aliceIdent_tsA_padding_aliceSig.size() >= _sz_aliceIdent_tsA_padding_aliceSigSize) { verifyInbound(); 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 d3fa785651..fdc00de340 100644 --- a/router/java/src/net/i2p/router/transport/ntcp/EventPumper.java +++ b/router/java/src/net/i2p/router/transport/ntcp/EventPumper.java @@ -20,6 +20,7 @@ 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.router.CommSystemFacade; import net.i2p.router.RouterContext; @@ -779,9 +780,9 @@ class EventPumper implements Runnable { key.attach(con); con.setKey(key); try { - NTCPAddress naddr = con.getRemoteAddress(); - if (naddr.getPort() <= 0) - throw new IOException("Invalid NTCP address: " + naddr); + RouterAddress naddr = con.getRemoteAddress(); + if (naddr.getPort() <= 0) + throw new IOException("Invalid NTCP address: " + naddr); InetSocketAddress saddr = new InetSocketAddress(naddr.getHost(), naddr.getPort()); boolean connected = con.getChannel().connect(saddr); if (connected) { diff --git a/router/java/src/net/i2p/router/transport/ntcp/NTCPAddress.java b/router/java/src/net/i2p/router/transport/ntcp/NTCPAddress.java deleted file mode 100644 index b37d23cc11..0000000000 --- a/router/java/src/net/i2p/router/transport/ntcp/NTCPAddress.java +++ /dev/null @@ -1,134 +0,0 @@ -package net.i2p.router.transport.ntcp; -/* - * 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.util.Properties; - -import net.i2p.I2PAppContext; -import net.i2p.data.DataHelper; -import net.i2p.data.RouterAddress; -import net.i2p.router.transport.TransportImpl; -import net.i2p.util.Addresses; -import net.i2p.util.Log; - -/** - * Wrap up an address - */ -public class NTCPAddress { - private final int _port; - private final String _host; - //private InetAddress _addr; - /** Port number used in RouterAddress definitions */ - public final static String PROP_PORT = RouterAddress.PROP_PORT; - /** Host name used in RouterAddress definitions */ - public final static String PROP_HOST = RouterAddress.PROP_HOST; - public static final int DEFAULT_COST = 10; - - public NTCPAddress(String host, int port) { - if (host != null) - _host = host.trim(); - else - _host = null; - _port = port; - } - - /* - public NTCPAddress() { - _host = null; - _port = -1; - // _addr = null; - } - - public NTCPAddress(InetAddress addr, int port) { - if (addr != null) - _host = addr.getHostAddress(); - _addr = addr; - _port = port; - } - */ - - public NTCPAddress(RouterAddress addr) { - if (addr == null) { - _host = null; - _port = -1; - return; - } - _host = addr.getOption(PROP_HOST); - _port = addr.getPort(); - } - - public RouterAddress toRouterAddress() { - if ( (_host == null) || (_port <= 0) ) - return null; - - RouterAddress addr = new RouterAddress(); - - addr.setCost(DEFAULT_COST); - //addr.setExpiration(null); - - Properties props = new Properties(); - props.setProperty(PROP_HOST, _host); - props.setProperty(PROP_PORT, ""+_port); - - addr.setOptions(props); - addr.setTransportStyle(NTCPTransport.STYLE); - return addr; - } - - public String getHost() { return _host; } - //public void setHost(String host) { _host = host; } - //public InetAddress getAddress() { return _addr; } - //public void setAddress(InetAddress addr) { _addr = addr; } - public int getPort() { return _port; } - //public void setPort(int port) { _port = port; } - - public boolean isPubliclyRoutable() { - return isPubliclyRoutable(_host); - } - - public static boolean isPubliclyRoutable(String host) { - if (host == null) return false; - byte quad[] = Addresses.getIP(host); - return TransportImpl.isPubliclyRoutable(quad); - } - - @Override - public String toString() { return _host + ":" + _port; } - - @Override - public int hashCode() { - int rv = _port; - //if (_addr != null) - // rv += _addr.getHostAddress().hashCode(); - //else - if (_host != null) rv ^= _host.hashCode(); - return rv; - } - - @Override - public boolean equals(Object val) { - if ( (val != null) && (val instanceof NTCPAddress) ) { - NTCPAddress addr = (NTCPAddress)val; - String hostname = null; - if (addr.getHost() != null) - hostname = addr.getHost().trim(); - String ourHost = getHost(); - if (ourHost != null) - ourHost = ourHost.trim(); - return DataHelper.eq(hostname, ourHost) && getPort() == addr.getPort(); - } - return false; - } - - public boolean equals(RouterAddress addr) { - if (addr == null) return false; - return ( (_host.equals(addr.getOption(PROP_HOST))) && - (Integer.toString(_port).equals(addr.getOption(PROP_PORT))) ); - } -} 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 96951cc5f3..e18933cc99 100644 --- a/router/java/src/net/i2p/router/transport/ntcp/NTCPConnection.java +++ b/router/java/src/net/i2p/router/transport/ntcp/NTCPConnection.java @@ -1,6 +1,7 @@ package net.i2p.router.transport.ntcp; import java.io.IOException; +import java.net.Inet6Address; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.SocketChannel; @@ -16,6 +17,7 @@ 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.SessionKey; @@ -86,7 +88,7 @@ class NTCPConnection { private final NTCPTransport _transport; private final boolean _isInbound; private volatile boolean _closed; - private NTCPAddress _remAddr; + private final RouterAddress _remAddr; private RouterIdentity _remotePeer; private long _clockSkew; // in seconds /** @@ -160,6 +162,7 @@ class NTCPConnection { _log = ctx.logManager().getLog(getClass()); _created = System.currentTimeMillis(); _transport = transport; + _remAddr = null; _chan = chan; _readBufs = new ConcurrentLinkedQueue(); _writeBufs = new ConcurrentLinkedQueue(); @@ -182,7 +185,7 @@ class NTCPConnection { * Create an outbound unconnected NTCP connection * */ - public NTCPConnection(RouterContext ctx, NTCPTransport transport, RouterIdentity remotePeer, NTCPAddress remAddr) { + public NTCPConnection(RouterContext ctx, NTCPTransport transport, RouterIdentity remotePeer, RouterAddress remAddr) { _context = ctx; _log = ctx.logManager().getLog(getClass()); _created = System.currentTimeMillis(); @@ -211,15 +214,42 @@ class NTCPConnection { _prevReadBlock = new byte[BLOCK_SIZE]; _transport.establishing(this); } - + + /** + * Valid for inbound; valid for outbound shortly after creation + */ public SocketChannel getChannel() { return _chan; } + + /** + * Valid for inbound; valid for outbound shortly after creation + */ public SelectionKey getKey() { return _conKey; } public void setChannel(SocketChannel chan) { _chan = chan; } public void setKey(SelectionKey key) { _conKey = key; } public boolean isInbound() { return _isInbound; } public boolean isEstablished() { return _established; } + + /** + * @since IPv6 + */ + public boolean isIPv6() { + return _chan != null && + _chan.socket().getInetAddress() instanceof Inet6Address; + } + + /** + * Only valid during establishment; null later + */ public EstablishState getEstablishState() { return _establishState; } - public NTCPAddress getRemoteAddress() { return _remAddr; } + + /** + * Only valid for outbound; null for inbound + */ + public RouterAddress getRemoteAddress() { return _remAddr; } + + /** + * Valid for outbound; valid for inbound after handshake + */ public RouterIdentity getRemotePeer() { return _remotePeer; } public void setRemotePeer(RouterIdentity ident) { _remotePeer = ident; } 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 343ee5f0b0..2d808c5bbc 100644 --- a/router/java/src/net/i2p/router/transport/ntcp/NTCPTransport.java +++ b/router/java/src/net/i2p/router/transport/ntcp/NTCPTransport.java @@ -3,14 +3,17 @@ package net.i2p.router.transport.ntcp; import java.io.IOException; import java.net.InetSocketAddress; import java.net.InetAddress; +import java.net.Inet6Address; import java.net.UnknownHostException; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.text.DecimalFormat; import java.text.NumberFormat; +import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Locale; @@ -30,12 +33,17 @@ import net.i2p.router.OutNetMessage; import net.i2p.router.RouterContext; import net.i2p.router.transport.CommSystemFacadeImpl; import net.i2p.router.transport.Transport; +import static net.i2p.router.transport.Transport.AddressSource.*; import net.i2p.router.transport.TransportBid; import net.i2p.router.transport.TransportImpl; +import net.i2p.router.transport.TransportUtil; +import static net.i2p.router.transport.TransportUtil.IPv6Config.*; import net.i2p.router.transport.crypto.DHSessionKeyBuilder; +import net.i2p.router.transport.udp.UDPTransport; import net.i2p.util.Addresses; import net.i2p.util.ConcurrentHashSet; import net.i2p.util.Log; +import net.i2p.util.OrderedProperties; import net.i2p.util.Translate; /** @@ -52,16 +60,25 @@ public class NTCPTransport extends TransportImpl { private final SharedBid _transientFail; private final Object _conLock; private final Map _conByIdent; - private NTCPAddress _myAddress; private final EventPumper _pumper; private final Reader _reader; private net.i2p.router.transport.ntcp.Writer _writer; + private int _ssuPort; + /** synch on this */ + private final Set _endpoints; + /** * list of NTCPConnection of connections not yet established that we * want to remove on establishment or close on timeout */ private final Set _establishing; + public final static String PROP_I2NP_NTCP_HOSTNAME = "i2np.ntcp.hostname"; + public final static String PROP_I2NP_NTCP_PORT = "i2np.ntcp.port"; + public final static String PROP_I2NP_NTCP_AUTO_PORT = "i2np.ntcp.autoport"; + public final static String PROP_I2NP_NTCP_AUTO_IP = "i2np.ntcp.autoip"; + public static final int DEFAULT_COST = 10; + /** this is rarely if ever used, default is to bind to wildcard address */ public static final String PROP_BIND_INTERFACE = "i2np.ntcp.bindInterface"; @@ -147,6 +164,7 @@ public class NTCPTransport extends TransportImpl { _context.statManager().createRateStat("ntcp.wantsQueuedWrite", "", "ntcp", RATES); //_context.statManager().createRateStat("ntcp.write", "", "ntcp", RATES); _context.statManager().createRateStat("ntcp.writeError", "", "ntcp", RATES); + _endpoints = new HashSet(4); _establishing = new ConcurrentHashSet(16); _conLock = new Object(); _conByIdent = new ConcurrentHashMap(64); @@ -195,10 +213,9 @@ public class NTCPTransport extends TransportImpl { isNew = true; RouterAddress addr = getTargetAddress(target); if (addr != null) { - NTCPAddress naddr = new NTCPAddress(addr); - con = new NTCPConnection(_context, this, ident, naddr); + con = new NTCPConnection(_context, this, ident, addr); if (_log.shouldLog(Log.DEBUG)) - _log.debug("Send on a new con: " + con + " at " + addr + " for " + ih.toBase64()); + _log.debug("Send on a new con: " + con + " at " + addr + " for " + ih); _conByIdent.put(ih, con); } else { _log.error("we bid on a peer who doesn't have an ntcp address? " + target); @@ -318,12 +335,12 @@ public class NTCPTransport extends TransportImpl { if (_log.shouldLog(Log.DEBUG)) _log.debug("slow bid when trying to send to " + peer); if (haveCapacity()) { - if (addr.getCost() > NTCPAddress.DEFAULT_COST) + if (addr.getCost() > DEFAULT_COST) return _slowCostBid; else return _slowBid; } else { - if (addr.getCost() > NTCPAddress.DEFAULT_COST) + if (addr.getCost() > DEFAULT_COST) return _nearCapacityCostBid; else return _nearCapacityBid; @@ -336,7 +353,7 @@ public class NTCPTransport extends TransportImpl { * @since 0.9.6 */ private RouterAddress getTargetAddress(RouterInfo target) { - List addrs = target.getTargetAddresses(STYLE); + List addrs = getTargetAddresses(target); for (int i = 0; i < addrs.size(); i++) { RouterAddress addr = addrs.get(i); byte[] ip = addr.getIP(); @@ -458,38 +475,62 @@ public class NTCPTransport extends TransportImpl { * verify stopped with isAlive() * Unfortunately TransportManager doesn't do that, so we * check here to prevent two pumpers. - * @return appears to be ignored by caller */ - public synchronized RouterAddress startListening() { + public synchronized void startListening() { // try once again to prevent two pumpers which is fatal if (_pumper.isAlive()) - return _myAddress != null ? _myAddress.toRouterAddress() : null; + return; if (_log.shouldLog(Log.WARN)) _log.warn("Starting ntcp transport listening"); startIt(); - configureLocalAddress(); - return bindAddress(); + RouterAddress addr = configureLocalAddress(); + int port; + if (addr != null) + // probably not set + port = addr.getPort(); + else + // received by externalAddressReceived() from TransportManager + port = _ssuPort; + RouterAddress myAddress = bindAddress(port); + if (myAddress != null) { + // fixed interface, or bound to the specified host + replaceAddress(myAddress); + } else if (addr != null) { + // specified host, bound to wildcard + replaceAddress(addr); + } else if (port > 0) { + // all detected interfaces + for (InetAddress ia : getSavedLocalAddresses()) { + OrderedProperties props = new OrderedProperties(); + props.setProperty(RouterAddress.PROP_HOST, ia.getHostAddress()); + props.setProperty(RouterAddress.PROP_PORT, Integer.toString(port)); + int cost = getDefaultCost(ia instanceof Inet6Address); + myAddress = new RouterAddress(STYLE, props, cost); + replaceAddress(myAddress); + } + } + // TransportManager.startListening() calls router.rebuildRouterInfo() } /** - * Only called by CSFI. - * Caller should stop the transport first, then - * verify stopped with isAlive() - * @return appears to be ignored by caller + * Only called by externalAddressReceived(). + * + * Doesn't actually restart unless addr is non-null and + * the port is different from the current listen port. + * + * If we had interface addresses before, we lost them. + * + * @param addr may be null */ - public synchronized RouterAddress restartListening(RouterAddress addr) { - // try once again to prevent two pumpers which is fatal - // we could just return null since the return value is ignored - if (_pumper.isAlive()) - return _myAddress != null ? _myAddress.toRouterAddress() : null; - if (_log.shouldLog(Log.WARN)) _log.warn("Restarting ntcp transport listening"); - - startIt(); - if (addr == null) - _myAddress = null; - else - _myAddress = new NTCPAddress(addr); - return bindAddress(); + private synchronized void restartListening(RouterAddress addr) { + if (addr != null) { + RouterAddress myAddress = bindAddress(addr.getPort()); + if (myAddress != null) + replaceAddress(myAddress); + else + replaceAddress(addr); + // UDPTransport.rebuildExternalAddress() calls router.rebuildRouterInfo() + } } /** @@ -520,9 +561,19 @@ public class NTCPTransport extends TransportImpl { return _pumper.isAlive(); } - /** call from synchronized method */ - private RouterAddress bindAddress() { - if (_myAddress != null) { + /** + * Only does something if myPort > 0 and myPort != current bound port + * (or there's no current port, or the configured interface or hostname changed). + * If we are changing the bound port, this restarts everything, which takes a long time. + * + * call from synchronized method + * + * @param myPort does nothing if <= 0 + * @return new address ONLY if bound to specific address, otherwise null + */ + private RouterAddress bindAddress(int port) { + RouterAddress myAddress = null; + if (port > 0) { InetAddress bindToAddr = null; String bindTo = _context.getProperty(PROP_BIND_INTERFACE); @@ -530,70 +581,122 @@ public class NTCPTransport extends TransportImpl { // If we are configured with a fixed IP address, // AND it's one of our local interfaces, // bind only to that. - boolean isFixed = _context.getProperty(CommSystemFacadeImpl.PROP_I2NP_NTCP_AUTO_IP, "true") - .toLowerCase(Locale.US).equals("false"); - String fixedHost = _context.getProperty(CommSystemFacadeImpl.PROP_I2NP_NTCP_HOSTNAME); - if (isFixed && fixedHost != null) { - try { - String testAddr = InetAddress.getByName(fixedHost).getHostAddress(); - if (Addresses.getAddresses().contains(testAddr)) - bindTo = testAddr; - } catch (UnknownHostException uhe) {} - } + bindTo = getFixedHost(); } if (bindTo != null) { try { bindToAddr = InetAddress.getByName(bindTo); } catch (UnknownHostException uhe) { - _log.log(Log.CRIT, "Invalid NTCP bind interface specified [" + bindTo + "]", uhe); + _log.error("Invalid NTCP bind interface specified [" + bindTo + "]", uhe); // this can be implemented later, just updates some stats // see udp/UDPTransport.java //setReachabilityStatus(CommSystemFacade.STATUS_HOSED); - return null; + //return null; + // fall thru } } try { - ServerSocketChannel chan = ServerSocketChannel.open(); - chan.configureBlocking(false); - - int port = _myAddress.getPort(); - if (port > 0 && port < 1024) - _log.logAlways(Log.WARN, "Specified NTCP port is " + port + ", ports lower than 1024 not recommended"); - InetSocketAddress addr = null; - if(bindToAddr==null) { + InetSocketAddress addr; + if (bindToAddr == null) { addr = new InetSocketAddress(port); } else { addr = new InetSocketAddress(bindToAddr, port); if (_log.shouldLog(Log.WARN)) _log.warn("Binding only to " + bindToAddr); + OrderedProperties props = new OrderedProperties(); + props.setProperty(RouterAddress.PROP_HOST, bindTo); + props.setProperty(RouterAddress.PROP_PORT, Integer.toString(port)); + int cost = getDefaultCost(false); + myAddress = new RouterAddress(STYLE, props, cost); } + if (!_endpoints.isEmpty()) { + // If we are already bound to the new address, OR + // if the host is specified and we are bound to the wildcard on the same port, + // do nothing. Changing config from wildcard to a specified host will + // require a restart. + if (_endpoints.contains(addr) || + (bindToAddr != null && _endpoints.contains(new InetSocketAddress(port)))) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Already listening on " + addr); + return null; + } + // FIXME support multiple binds + // FIXME just close and unregister + stopWaitAndRestart(); + } + if (port < 1024) + _log.logAlways(Log.WARN, "Specified NTCP port is " + port + ", ports lower than 1024 not recommended"); + ServerSocketChannel chan = ServerSocketChannel.open(); + chan.configureBlocking(false); chan.socket().bind(addr); + _endpoints.add(addr); if (_log.shouldLog(Log.INFO)) _log.info("Listening on " + addr); _pumper.register(chan); } catch (IOException ioe) { _log.error("Error listening", ioe); + myAddress = null; } } else { if (_log.shouldLog(Log.INFO)) _log.info("Outbound NTCP connections only - no listener configured"); } - - if (_myAddress != null) { - RouterAddress rv = _myAddress.toRouterAddress(); - if (rv != null) - replaceAddress(rv); - return rv; - } else { - return null; - } + return myAddress; } + /** + * @return configured host or null. Must be one of our local interfaces. + * @since IPv6 moved from bindAddress() + */ + private String getFixedHost() { + boolean isFixed = _context.getProperty(PROP_I2NP_NTCP_AUTO_IP, "true") + .toLowerCase(Locale.US).equals("false"); + String fixedHost = _context.getProperty(PROP_I2NP_NTCP_HOSTNAME); + if (isFixed && fixedHost != null) { + try { + String testAddr = InetAddress.getByName(fixedHost).getHostAddress(); + // FIXME range of IPv6 addresses + if (Addresses.getAddresses().contains(testAddr)) + return testAddr; + } catch (UnknownHostException uhe) {} + } + return null; + } + + /** + * Caller must sync + * @since IPv6 moved from externalAddressReceived() + */ + private void stopWaitAndRestart() { + if (_log.shouldLog(Log.WARN)) + _log.warn("Halting NTCP to change address"); + stopListening(); + // Wait for NTCP Pumper to stop so we don't end up with two... + while (isAlive()) { + try { Thread.sleep(5*1000); } catch (InterruptedException ie) {} + } + if (_log.shouldLog(Log.WARN)) + _log.warn("Restarting NTCP transport listening"); + startIt(); + } + + /** + * Hook for NTCPConnection + */ Reader getReader() { return _reader; } + + /** + * Hook for NTCPConnection + */ net.i2p.router.transport.ntcp.Writer getWriter() { return _writer; } + public String getStyle() { return STYLE; } + + /** + * Hook for NTCPConnection + */ EventPumper getPumper() { return _pumper; } /** @@ -638,33 +741,255 @@ public class NTCPTransport extends TransportImpl { //private boolean bindAllInterfaces() { return true; } - /** caller must synch on this */ - private void configureLocalAddress() { - RouterContext ctx = getContext(); - if (ctx == null) { - System.err.println("NIO transport has no context?"); - } else { + /** + * Generally returns null + * caller must synch on this + */ + private RouterAddress configureLocalAddress() { // this generally returns null -- see javadoc - RouterAddress ra = CommSystemFacadeImpl.createNTCPAddress(ctx); - if (ra != null) { - NTCPAddress addr = new NTCPAddress(ra); + RouterAddress addr = createNTCPAddress(); + if (addr != null) { if (addr.getPort() <= 0) { - _myAddress = null; + addr = null; if (_log.shouldLog(Log.ERROR)) _log.error("NTCP address is outbound only, since the NTCP configuration is invalid"); } else { - _myAddress = addr; - replaceAddress(ra); if (_log.shouldLog(Log.INFO)) - _log.info("NTCP address configured: " + _myAddress); + _log.info("NTCP address configured: " + addr); } } else { if (_log.shouldLog(Log.INFO)) _log.info("NTCP address is outbound only"); } - } + return addr; } + /** + * This only creates an address if the hostname AND port are set in router.config, + * which should be rare. + * Otherwise, notifyReplaceAddress() below takes care of it. + * Note this is called both from above and from NTCPTransport.startListening() + * + * @since IPv6 moved from CSFI + */ + private RouterAddress createNTCPAddress() { + // Fixme doesn't check PROP_BIND_INTERFACE + String name = _context.getProperty(PROP_I2NP_NTCP_HOSTNAME); + if ( (name == null) || (name.trim().length() <= 0) || ("null".equals(name)) ) + return null; + int p = _context.getProperty(PROP_I2NP_NTCP_PORT, -1); + if (p <= 0 || p >= 64*1024) + return null; + OrderedProperties props = new OrderedProperties(); + props.setProperty(RouterAddress.PROP_HOST, name); + props.setProperty(RouterAddress.PROP_PORT, Integer.toString(p)); + int cost = getDefaultCost(false); + RouterAddress addr = new RouterAddress(STYLE, props, cost); + return addr; + } + + private int getDefaultCost(boolean isIPv6) { + int rv = DEFAULT_COST; + if (isIPv6) { + TransportUtil.IPv6Config config = getIPv6Config(); + if (config == IPV6_PREFERRED) + rv--; + else if (config == IPV6_NOT_PREFERRED) + rv++; + } + return rv; + } + + /** + * UDP changed addresses, tell NTCP and (possibly) restart + * + * @since IPv6 moved from CSFI.notifyReplaceAddress() + */ + @Override + public void externalAddressReceived(AddressSource source, byte[] ip, int port) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Received address: " + Addresses.toString(ip, port) + " from: " + source); + if (ip != null && !isPubliclyRoutable(ip)) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Invalid address: " + Addresses.toString(ip, port) + " from: " + source); + return; + } + if (!isAlive()) { + if (source == SOURCE_INTERFACE || source == SOURCE_UPNP) { + try { + InetAddress ia = InetAddress.getByAddress(ip); + saveLocalAddress(ia); + } catch (UnknownHostException uhe) {} + } else if (source == SOURCE_CONFIG) { + // save for startListening() + _ssuPort = port; + } + return; + } + // ignore UPnP for now, get everything from SSU + if (source != SOURCE_SSU) + return; + externalAddressReceived(ip, port); + } + + /** + * UDP changed addresses, tell NTCP and restart. + * Port may be set to indicate requested port even if ip is null. + * + * @param ip previously validated + * @since IPv6 moved from CSFI.notifyReplaceAddress() + */ + private synchronized void externalAddressReceived(byte[] ip, int port) { + // FIXME just take first IPv4 address for now + // FIXME if SSU set to hostname, NTCP will be set to IP + RouterAddress oldAddr = getCurrentAddress(false); + if (_log.shouldLog(Log.INFO)) + _log.info("Changing NTCP Address? was " + oldAddr); + + OrderedProperties newProps = new OrderedProperties(); + int cost; + if (oldAddr == null) { + cost = getDefaultCost(ip != null && ip.length == 16); + } else { + cost = oldAddr.getCost(); + newProps.putAll(oldAddr.getOptionsMap()); + } + RouterAddress newAddr = new RouterAddress(STYLE, newProps, cost); + + boolean changed = false; + + // Auto Port Setting + // old behavior (<= 0.7.3): auto-port defaults to false, and true trumps explicit setting + // new behavior (>= 0.7.4): auto-port defaults to true, but explicit setting trumps auto + // TODO rewrite this to operate on ints instead of strings + String oport = newProps.getProperty(RouterAddress.PROP_PORT); + String nport = null; + String cport = _context.getProperty(PROP_I2NP_NTCP_PORT); + if (cport != null && cport.length() > 0) { + nport = cport; + } else if (_context.getBooleanPropertyDefaultTrue(PROP_I2NP_NTCP_AUTO_PORT)) { + // 0.9.6 change + // This wasn't quite right, as udpAddr is the EXTERNAL port and we really + // want NTCP to bind to the INTERNAL port the first time, + // because if they are different, the NAT is changing them, and + // it probably isn't mapping UDP and TCP the same. + if (port > 0) + // should always be true + nport = Integer.toString(port); + } + if (_log.shouldLog(Log.INFO)) + _log.info("old: " + oport + " config: " + cport + " new: " + nport); + if (nport == null || nport.length() <= 0) + return; + // 0.9.6 change + // Don't have NTCP "chase" SSU's external port, + // as it may change, possibly frequently. + //if (oport == null || ! oport.equals(nport)) { + if (oport == null) { + newProps.setProperty(RouterAddress.PROP_PORT, nport); + changed = true; + } + + // Auto IP Setting + // old behavior (<= 0.7.3): auto-ip defaults to false, and trumps configured hostname, + // and ignores reachability status - leading to + // "firewalled with inbound TCP enabled" warnings. + // new behavior (>= 0.7.4): auto-ip defaults to true, and explicit setting trumps auto, + // and only takes effect if reachability is OK. + // And new "always" setting ignores reachability status, like + // "true" was in 0.7.3 + String ohost = newProps.getProperty(RouterAddress.PROP_HOST); + String enabled = _context.getProperty(PROP_I2NP_NTCP_AUTO_IP, "true").toLowerCase(Locale.US); + String name = _context.getProperty(PROP_I2NP_NTCP_HOSTNAME); + // hostname config trumps auto config + if (name != null && name.length() > 0) + enabled = "false"; + + // assume SSU is happy if the address is non-null + // TODO is this sufficient? + boolean ssuOK = ip != null; + if (_log.shouldLog(Log.INFO)) + _log.info("old: " + ohost + " config: " + name + " auto: " + enabled + " ssuOK? " + ssuOK); + if (enabled.equals("always") || + (Boolean.parseBoolean(enabled) && ssuOK)) { + // ip non-null + String nhost = Addresses.toString(ip); + if (_log.shouldLog(Log.INFO)) + _log.info("old: " + ohost + " config: " + name + " new: " + nhost); + if (nhost == null || nhost.length() <= 0) + return; + if (ohost == null || ! ohost.equalsIgnoreCase(nhost)) { + newProps.setProperty(RouterAddress.PROP_HOST, nhost); + changed = true; + } + } else if (enabled.equals("false") && + name != null && name.length() > 0 && + !name.equals(ohost) && + nport != null) { + // Host name is configured, and we have a port (either auto or configured) + // but we probably only get here if the port is auto, + // otherwise createNTCPAddress() would have done it already + if (_log.shouldLog(Log.INFO)) + _log.info("old: " + ohost + " config: " + name + " new: " + name); + newProps.setProperty(RouterAddress.PROP_HOST, name); + changed = true; + } else if (ohost == null || ohost.length() <= 0) { + return; + } else if (Boolean.parseBoolean(enabled) && !ssuOK) { + // UDP transitioned to not-OK, turn off NTCP address + // This will commonly happen at startup if we were initially OK + // because UPnP was successful, but a subsequent SSU Peer Test determines + // we are still firewalled (SW firewall, bad UPnP indication, etc.) + if (_log.shouldLog(Log.INFO)) + _log.info("old: " + ohost + " config: " + name + " new: null"); + newAddr = null; + changed = true; + } + + if (!changed) { + if (oldAddr != null) { + // change cost only? + int oldCost = oldAddr.getCost(); + int newCost = getDefaultCost(ohost != null && ohost.contains(":")); + if (ADJUST_COST && !haveCapacity()) + newCost += CONGESTION_COST_ADJUSTMENT; + if (newCost != oldCost) { + newAddr.setCost(newCost); + if (_log.shouldLog(Log.WARN)) + _log.warn("Changing NTCP cost from " + oldCost + " to " + newCost); + // fall thru and republish + } else { + _log.info("No change to NTCP Address"); + return; + } + } else { + _log.info("No change to NTCP Address"); + return; + } + } + + // stopListening stops the pumper, readers, and writers, so required even if + // oldAddr == null since startListening starts them all again + // + // really need to fix this so that we can change or create an inbound address + // without tearing down everything + // Especially on disabling the address, we shouldn't tear everything down. + // + //if (_log.shouldLog(Log.WARN)) + // _log.warn("Halting NTCP to change address"); + //stopListening(); + // Wait for NTCP Pumper to stop so we don't end up with two... + //while (isAlive()) { + // try { Thread.sleep(5*1000); } catch (InterruptedException ie) {} + //} + restartListening(newAddr); + if (_log.shouldLog(Log.WARN)) + _log.warn("Updating NTCP Address with " + newAddr); + return; + } + + + /** * If we didn't used to be forwarded, and we have an address, * and we are configured to use UPnP, update our RouterAddress @@ -676,21 +1001,21 @@ public class NTCPTransport extends TransportImpl { * NTCP address when it transitions to OK. */ @Override - public void forwardPortStatus(int port, int externalPort, boolean success, String reason) { + public void forwardPortStatus(byte[] ip, int port, int externalPort, boolean success, String reason) { if (_log.shouldLog(Log.WARN)) { if (success) - _log.warn("UPnP has opened the NTCP port: " + port); + _log.warn("UPnP has opened the NTCP port: " + port + " via " + Addresses.toString(ip, externalPort)); else _log.warn("UPnP has failed to open the NTCP port: " + port + " reason: " + reason); } } /** - * @return current port, else NTCP configured port, else -1 (but not UDP port if auto) + * @return current IPv4 port, else NTCP configured port, else -1 (but not UDP port if auto) */ @Override public int getRequestedPort() { - NTCPAddress addr = _myAddress; + RouterAddress addr = getCurrentAddress(false); if (addr != null) { int port = addr.getPort(); if (port > 0) @@ -700,7 +1025,7 @@ public class NTCPTransport extends TransportImpl { // from here, so we do it in TransportManager. // if (Boolean.valueOf(_context.getProperty(CommSystemFacadeImpl.PROP_I2NP_NTCP_AUTO_PORT)).booleanValue()) // return foo; - return _context.getProperty(CommSystemFacadeImpl.PROP_I2NP_NTCP_PORT, -1); + return _context.getProperty(PROP_I2NP_NTCP_PORT, -1); } /** @@ -714,7 +1039,8 @@ public class NTCPTransport extends TransportImpl { */ @Override public short getReachabilityStatus() { - if (isAlive() && _myAddress != null) { + // If we have an IPv4 address + if (isAlive() && getCurrentAddress(false) != null) { for (NTCPConnection con : _conByIdent.values()) { if (con.isInbound()) return CommSystemFacade.STATUS_OK; @@ -733,18 +1059,17 @@ public class NTCPTransport extends TransportImpl { _writer.stopWriting(); _reader.stopReading(); _finisher.stop(); - Map cons = null; + List cons; synchronized (_conLock) { - cons = new HashMap(_conByIdent); + cons = new ArrayList(_conByIdent.values()); _conByIdent.clear(); } - for (Iterator iter = cons.values().iterator(); iter.hasNext(); ) { - NTCPConnection con = (NTCPConnection)iter.next(); + for (NTCPConnection con : cons) { con.close(); } NTCPConnection.releaseResources(); - // will this work? replaceAddress(null); + _endpoints.clear(); } public static final String STYLE = "NTCP"; @@ -753,7 +1078,7 @@ public class NTCPTransport extends TransportImpl { @Override public void renderStatusHTML(java.io.Writer out, String urlBase, int sortFlags) throws IOException { - TreeSet peers = new TreeSet(getComparator(sortFlags)); + TreeSet peers = new TreeSet(getComparator(sortFlags)); peers.addAll(_conByIdent.values()); long offsetTotal = 0; @@ -771,6 +1096,7 @@ public class NTCPTransport extends TransportImpl { "\n" + "" + "" + + "" + "" + "" + "" + @@ -783,8 +1109,7 @@ public class NTCPTransport extends TransportImpl { " \n"); out.write(buf.toString()); buf.setLength(0); - for (Iterator iter = peers.iterator(); iter.hasNext(); ) { - NTCPConnection con = (NTCPConnection)iter.next(); + for (NTCPConnection con : peers) { buf.append("\n"); - buf.append(""); long idleIn = Math.max(now-peer.getLastReceiveTime(), 0); long idleOut = Math.max(now-peer.getLastSendTime(), 0); @@ -2413,23 +2711,24 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority numPeers++; } + if (numPeers > 0) { // buf.append("\n"); - buf.append("" + + buf.append("" + "" + "\n" + "\n" + "\n" + ""); } + } // numPeers > 0 buf.append("
").append(_("Peer")).append("").append(_("Dir")).append("").append(_("IPv6")).append("").append(_("Idle")).append("").append(_("In/Out")).append("").append(_("Up")).append("
"); buf.append(_context.commSystem().renderPeerHTML(con.getRemotePeer().calculateHash())); //byte[] ip = getIP(con.getRemotePeer().calculateHash()); @@ -795,6 +1120,11 @@ public class NTCPTransport extends TransportImpl { buf.append("\"Inbound\""); else buf.append("\"Outbound\""); + buf.append(""); + if (con.isIPv6()) + buf.append("✓"); + else + buf.append(" "); buf.append(""); buf.append(DataHelper.formatDuration2(con.getTimeSinceReceive())); buf.append(THINSP).append(DataHelper.formatDuration2(con.getTimeSinceSend())); @@ -843,7 +1173,8 @@ public class NTCPTransport extends TransportImpl { if (!peers.isEmpty()) { // buf.append("

").append(peers.size()).append(' ').append(_("peers")).append("  "); + buf.append("
").append(_("SUMMARY")) + .append(""); buf.append("").append(formatRate(bpsRecv/1024)).append(THINSP).append(formatRate(bpsSend/1024)).append(""); buf.append("").append(DataHelper.formatDuration2(totalUptime/peers.size())); buf.append("").append(DataHelper.formatDuration2(offsetTotal*1000/peers.size())); diff --git a/router/java/src/net/i2p/router/transport/udp/ACKSender.java b/router/java/src/net/i2p/router/transport/udp/ACKSender.java index f1aedda5ae..8fef9c4342 100644 --- a/router/java/src/net/i2p/router/transport/udp/ACKSender.java +++ b/router/java/src/net/i2p/router/transport/udp/ACKSender.java @@ -62,7 +62,7 @@ class ACKSender implements Runnable { public synchronized void shutdown() { _alive = false; - PeerState poison = new PeerState(_context, _transport, null, 0, null, false); + PeerState poison = new PeerState(_context, _transport, new byte[4], 0, null, false); poison.setTheyRelayToUsAs(POISON_PS); _peersToACK.offer(poison); for (int i = 1; i <= 5 && !_peersToACK.isEmpty(); i++) { 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 4987314a36..95250aa959 100644 --- a/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java +++ b/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java @@ -249,7 +249,7 @@ class EstablishmentManager { maybeTo = new RemoteHostId(remAddr.getAddress(), port); if ((!_transport.isValid(maybeTo.getIP())) || - Arrays.equals(maybeTo.getIP(), _transport.getExternalIP())) { + (Arrays.equals(maybeTo.getIP(), _transport.getExternalIP()) && !_transport.allowLocal())) { _transport.failed(msg, "Remote peer's IP isn't valid"); _transport.markUnreachable(toHash); //_context.banlist().banlistRouter(msg.getTarget().getIdentity().calculateHash(), "Invalid SSU address", UDPTransport.STYLE); @@ -447,7 +447,9 @@ class EstablishmentManager { } if (!_transport.allowConnection()) return; // drop the packet - state = new InboundEstablishState(_context, from.getIP(), from.getPort(), _transport.getExternalPort(), + byte[] fromIP = from.getIP(); + state = new InboundEstablishState(_context, fromIP, from.getPort(), + _transport.getExternalPort(fromIP.length == 16), _transport.getDHBuilder()); state.receiveSessionRequest(reader.getSessionRequestReader()); InboundEstablishState oldState = _inboundStates.putIfAbsent(from, state); @@ -459,8 +461,10 @@ class EstablishmentManager { if (isNew) { // Don't offer to relay to privileged ports. + // Only offer for an IPv4 session. // TODO if already we have their RI, only offer if they need it (no 'C' cap) - if (_transport.canIntroduce() && state.getSentPort() >= 1024) { + if (_transport.canIntroduce() && state.getSentPort() >= 1024 && + state.getSentIP().length == 4) { // ensure > 0 long tag = 1 + _context.random().nextLong(MAX_TAG_VALUE); state.setSentRelayTag(tag); @@ -673,7 +677,8 @@ class EstablishmentManager { String smtu = addr.getOption(UDPAddress.PROP_MTU); if (smtu != null) { try { - int mtu = MTU.rectify(Integer.parseInt(smtu)); + boolean isIPv6 = state.getSentIP().length == 16; + int mtu = MTU.rectify(isIPv6, Integer.parseInt(smtu)); peer.setHisMTU(mtu); } catch (NumberFormatException nfe) {} } @@ -833,7 +838,9 @@ class EstablishmentManager { _inboundStates.remove(state.getRemoteHostId()); return; } - _transport.send(_builder.buildSessionCreatedPacket(state, _transport.getExternalPort(), _transport.getIntroKey())); + _transport.send(_builder.buildSessionCreatedPacket(state, + _transport.getExternalPort(state.getSentIP().length == 16), + _transport.getIntroKey())); state.createdPacketSent(); } @@ -884,6 +891,9 @@ class EstablishmentManager { state.introSent(); } + /** + * We are Alice, we sent a RelayRequest to Bob and got a response back. + */ void receiveRelayResponse(RemoteHostId bob, UDPPacketReader reader) { long nonce = reader.getRelayResponseReader().readNonce(); OutboundEstablishState state = _liveIntroductions.remove(Long.valueOf(nonce)); @@ -893,6 +903,7 @@ class EstablishmentManager { return; // already established } + // Note that we ignore the Alice (us) IP/Port in the RelayResponse int sz = reader.getRelayResponseReader().readCharlieIPSize(); byte ip[] = new byte[sz]; reader.getRelayResponseReader().readCharlieIP(ip, 0); @@ -940,15 +951,17 @@ class EstablishmentManager { } /** - * Are IP and port valid? + * Are IP and port valid? This is only for checking the relay response. + * Reject all IPv6, for now, even if we are configured for it. * Refuse anybody in the same /16 * @since 0.9.3 */ private boolean isValid(byte[] ip, int port) { - return port >= 1024 && + return port >= UDPTransport.MIN_PEER_PORT && port <= 65535 && + ip != null && ip.length == 4 && _transport.isValid(ip) && - (!DataHelper.eq(ip, 0, _transport.getExternalIP(), 0, 2)) && + (!_transport.isTooClose(ip)) && (!_context.blocklist().isBlocklisted(ip)); } diff --git a/router/java/src/net/i2p/router/transport/udp/IPThrottler.java b/router/java/src/net/i2p/router/transport/udp/IPThrottler.java index 5f94620621..1308c2e707 100644 --- a/router/java/src/net/i2p/router/transport/udp/IPThrottler.java +++ b/router/java/src/net/i2p/router/transport/udp/IPThrottler.java @@ -3,6 +3,7 @@ package net.i2p.router.transport.udp; import net.i2p.util.ObjectCounter; import net.i2p.util.SimpleScheduler; import net.i2p.util.SimpleTimer; +import net.i2p.util.SipHash; /** * Count IPs @@ -21,12 +22,16 @@ class IPThrottler { /** * Increments before checking - * @return true if ip.length != 4 */ public boolean shouldThrottle(byte[] ip) { - if (ip.length != 4) - return true; - return _counter.increment(toInt(ip)) > _max; + // for IPv4 we simply use the IP; + // for IPv6 we use a secure hash as an attacker could select the lower bytes + Integer key; + if (ip.length == 4) + key = toInt(ip); + else + key = Integer.valueOf(SipHash.hashCode(ip)); + return _counter.increment(key) > _max; } private static Integer toInt(byte ip[]) { diff --git a/router/java/src/net/i2p/router/transport/udp/InboundMessageFragments.java b/router/java/src/net/i2p/router/transport/udp/InboundMessageFragments.java index 2d551fda98..33b766d6c0 100644 --- a/router/java/src/net/i2p/router/transport/udp/InboundMessageFragments.java +++ b/router/java/src/net/i2p/router/transport/udp/InboundMessageFragments.java @@ -66,6 +66,7 @@ class InboundMessageFragments /*implements UDPTransport.PartialACKSource */{ _ackSender.shutdown(); _messageReceiver.shutdown(); } + public boolean isAlive() { return _alive; } /** 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 0d49d2b5b1..03b70e2a7d 100644 --- a/router/java/src/net/i2p/router/transport/udp/IntroductionManager.java +++ b/router/java/src/net/i2p/router/transport/udp/IntroductionManager.java @@ -23,6 +23,52 @@ import net.i2p.util.Log; /** * Keep track of inbound and outbound introductions. + * + * IPv6 info: Alice-Bob communication may be via IPv4 or IPv6. + * Bob-Charlie communication must be via established IPv4 session as that's the only way + * that Bob knows Charlie's IPv4 address to give it to Alice. + * Alice-Charlie communication is via IPv4. + * If Alice-Bob is over IPv6, Alice must include her IPv4 address in + * the RelayRequest message. + * + * From udp.html on the website: + +

Indirect session establishment by means of a third party introduction +is necessary for efficient NAT traversal. Charlie, a router behind a +NAT or firewall which does not allow unsolicited inbound UDP packets, +first contacts a few peers, choosing some to serve as introducers. Each +of these peers (Bob, Bill, Betty, etc) provide Charlie with an introduction +tag - a 4 byte random number - which he then makes available to the public +as methods of contacting him. Alice, a router who has Charlie's published +contact methods, first sends a RelayRequest packet to one or more of the +introducers, asking each to introduce her to Charlie (offering the +introduction tag to identify Charlie). Bob then forwards a RelayIntro +packet to Charlie including Alice's public IP and port number, then sends +Alice back a RelayResponse packet containing Charlie's public IP and port +number. When Charlie receives the RelayIntro packet, he sends off a small +random packet to Alice's IP and port (poking a hole in his NAT/firewall), +and when Alice receives Bob's RelayResponse packet, she begins a new +full direction session establishment with the specified IP and port.

+

+Alice first connects to introducer Bob, who relays the request to Charlie. +

+
+        Alice                         Bob                  Charlie
+    RelayRequest ---------------------->
+         <-------------- RelayResponse    RelayIntro ----------->
+         <-------------------------------------------- HolePunch (data ignored)
+    SessionRequest -------------------------------------------->
+         <-------------------------------------------- SessionCreated
+    SessionConfirmed ------------------------------------------>
+         <-------------------------------------------- DeliveryStatusMessage
+         <-------------------------------------------- DatabaseStoreMessage
+    DatabaseStoreMessage -------------------------------------->
+    Data <--------------------------------------------------> Data
+
+ +

+After the hole punch, the session is established between Alice and Charlie as in a direct establishment. +

*/ class IntroductionManager { private final RouterContext _context; @@ -77,6 +123,9 @@ class IntroductionManager { // let's not use an introducer on a privileged port, sounds like trouble if (peer.getRemotePort() < 1024) return; + // Only allow relay as Bob or Charlie if the Bob-Charlie session is IPv4 + if (peer.getRemoteIP().length != 4) + return; if (_log.shouldLog(Log.DEBUG)) _log.debug("Adding peer " + peer.getRemoteHostId() + ", weRelayToThemAs " + peer.getWeRelayToThemAs() + ", theyRelayToUsAs " + peer.getTheyRelayToUsAs()); @@ -136,6 +185,8 @@ class IntroductionManager { _log.info("Picked peer has no local routerInfo: " + cur); continue; } + // FIXME we can include all his addresses including IPv6 even if we don't support IPv6 (isValid() is false) + // but requires RelayRequest support, see below RouterAddress ra = _transport.getTargetAddress(ri); if (ra == null) { if (_log.shouldLog(Log.INFO)) @@ -159,6 +210,8 @@ class IntroductionManager { _log.info("Peer is idle too long: " + cur); continue; } + // FIXME we can include all his addresses including IPv6 even if we don't support IPv6 (isValid() is false) + // but requires RelayRequest support, see below byte[] ip = cur.getRemoteIP(); int port = cur.getRemotePort(); if (!isValid(ip, port)) @@ -331,6 +384,9 @@ class IntroductionManager { // ip/port inside message should be 0:0, as it's unimplemented on send - // see PacketBuilder.buildRelayRequest() + // and we don't read it here. + // FIXME implement for getting Alice's IPv4 in RelayRequest sent over IPv6? + // or is that just too easy to spoof? if (!isValid(alice.getIP(), alice.getPort()) || ipSize != 0 || port != 0) { if (_log.shouldLog(Log.WARN)) { byte ip[] = new byte[ipSize]; @@ -368,14 +424,16 @@ class IntroductionManager { /** * Are IP and port valid? + * Reject all IPv6, for now, even if we are configured for it. * Refuse anybody in the same /16 * @since 0.9.3 */ private boolean isValid(byte[] ip, int port) { - return port >= 1024 && + return port >= UDPTransport.MIN_PEER_PORT && port <= 65535 && + ip != null && ip.length == 4 && _transport.isValid(ip) && - (!DataHelper.eq(ip, 0, _transport.getExternalIP(), 0, 2)) && + (!_transport.isTooClose(ip)) && (!_context.blocklist().isBlocklisted(ip)); } } diff --git a/router/java/src/net/i2p/router/transport/udp/MTU.java b/router/java/src/net/i2p/router/transport/udp/MTU.java index 9258043f6b..641c121635 100644 --- a/router/java/src/net/i2p/router/transport/udp/MTU.java +++ b/router/java/src/net/i2p/router/transport/udp/MTU.java @@ -1,6 +1,7 @@ package net.i2p.router.transport.udp; import java.net.InetAddress; +import java.net.Inet6Address; import java.net.NetworkInterface; import java.net.SocketException; import java.util.Enumeration; @@ -41,7 +42,8 @@ abstract class MTU { try { // testing //return ifc.getMTU(); - return rectify(ifc.getMTU()); + boolean isIPv6 = addr instanceof Inet6Address; + return rectify(isIPv6, ifc.getMTU()); } catch (SocketException se) { // ignore } catch (Throwable t) { @@ -58,11 +60,16 @@ abstract class MTU { /** * @return min of PeerState.MIN_MTU, max of PeerState.LARGE_MTU, - * rectified so rv % 16 == 12 + * rectified so rv % 16 == 12 (IPv4) + * or rv % 16 == 0 (IPv6) */ - public static int rectify(int mtu) { + public static int rectify(boolean isIPv6, int mtu) { int rv = mtu; int mod = rv % 16; + if (isIPv6) { + rv -= mod; + return Math.max(PeerState.MIN_IPV6_MTU, Math.min(PeerState.MAX_IPV6_MTU, rv)); + } if (mod > 12) rv -= mod - 12; else if (mod < 12) @@ -72,11 +79,29 @@ abstract class MTU { /**** public static void main(String args[]) { + System.out.println("Cmd line interfaces:"); + for (int i = 0; i < args.length; i++) { + try { + InetAddress test = InetAddress.getByName(args[i]); + System.out.println("MTU of " + args[i] + " is " + getMTU(test)); + } catch (Exception e) { + e.printStackTrace(); + } + } + System.out.println("All interfaces:"); try { - InetAddress test = InetAddress.getByName(args[0]); - System.out.println("MTU is " + getMTU(test)); - } catch (Exception e) { - e.printStackTrace(); + Enumeration ifcs = NetworkInterface.getNetworkInterfaces(); + if (ifcs != null) { + while (ifcs.hasMoreElements()) { + NetworkInterface ifc = ifcs.nextElement(); + for(Enumeration addrs = ifc.getInetAddresses(); addrs.hasMoreElements();) { + InetAddress addr = addrs.nextElement(); + System.out.println("MTU of " + addr.getHostAddress() + " is " + getMTU(addr)); + } + } + } + } catch (SocketException se) { + System.out.println("no interfaces"); } } ****/ 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 d4c8be8336..96e9a66865 100644 --- a/router/java/src/net/i2p/router/transport/udp/OutboundMessageFragments.java +++ b/router/java/src/net/i2p/router/transport/udp/OutboundMessageFragments.java @@ -309,8 +309,8 @@ class OutboundMessageFragments { // use max of 1 second so finishMessages() and/or PeerState.finishMessages() // gets called regularly int toWait = Math.min(Math.max(nextSendDelay, 10), MAX_WAIT); - if (_log.shouldLog(Log.DEBUG)) - _log.debug("wait for " + toWait); + //if (_log.shouldLog(Log.DEBUG)) + // _log.debug("wait for " + toWait); // wait.. or somethin' synchronized (_activePeers) { try { 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 965db51ed6..ed62440a42 100644 --- a/router/java/src/net/i2p/router/transport/udp/PacketBuilder.java +++ b/router/java/src/net/i2p/router/transport/udp/PacketBuilder.java @@ -137,6 +137,10 @@ class PacketBuilder { /** 74 */ public static final int MIN_DATA_PACKET_OVERHEAD = IP_HEADER_SIZE + UDP_HEADER_SIZE + DATA_HEADER_SIZE; + public static final int IPV6_HEADER_SIZE = 40; + /** 94 */ + public static final int MIN_IPV6_DATA_PACKET_OVERHEAD = IPV6_HEADER_SIZE + UDP_HEADER_SIZE + DATA_HEADER_SIZE; + /** one byte field */ public static final int ABSOLUTE_MAX_ACKS = 255; @@ -152,6 +156,9 @@ class PacketBuilder { */ private static final int MAX_RESEND_ACKS_SMALL = 4; + /** + * @param transport may be null for unit testing only + */ public PacketBuilder(I2PAppContext ctx, UDPTransport transport) { _context = ctx; _transport = transport; @@ -237,7 +244,15 @@ class PacketBuilder { } int currentMTU = peer.getMTU(); - int availableForAcks = currentMTU - MIN_DATA_PACKET_OVERHEAD - dataSize; + 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... @@ -398,16 +413,16 @@ class PacketBuilder { setTo(packet, peer.getRemoteIPAddress(), peer.getRemotePort()); if (_log.shouldLog(Log.INFO)) { - msg.append(" pkt size ").append(off + (IP_HEADER_SIZE + UDP_HEADER_SIZE)); + msg.append(" pkt size ").append(off + (ipHeaderSize + UDP_HEADER_SIZE)); _log.info(msg.toString()); } // the packet could have been built before the current mtu got lowered, so // compare to LARGE_MTU - if (off + (IP_HEADER_SIZE + UDP_HEADER_SIZE) > PeerState.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 + (IP_HEADER_SIZE + UDP_HEADER_SIZE)) + + " pkt size " + (off + (ipHeaderSize + UDP_HEADER_SIZE)) + " MTU " + currentMTU + ' ' + availableForAcks + " for all acks " + availableForExplicitAcks + " for full acks " + @@ -559,7 +574,7 @@ class PacketBuilder { state.prepareSessionCreated(); byte sentIP[] = state.getSentIP(); - if ( (sentIP == null) || (sentIP.length <= 0) || ( (_transport != null) && (!_transport.isValid(sentIP)) ) ) { + if ( (sentIP == null) || (sentIP.length <= 0) || (!_transport.isValid(sentIP))) { if (_log.shouldLog(Log.ERROR)) _log.error("How did our sent IP become invalid? " + state); state.fail(); @@ -639,7 +654,7 @@ class PacketBuilder { int off = HEADER_SIZE; byte toIP[] = state.getSentIP(); - if ( (_transport !=null) && (!_transport.isValid(toIP)) ) { + if (!_transport.isValid(toIP)) { packet.release(); return null; } @@ -1046,6 +1061,7 @@ class PacketBuilder { // specify these if we know what our external receive ip/port is and if its different // from what bob is going to think + // FIXME IPv4 addr must be specified when sent over IPv6 private byte[] getOurExplicitIP() { return null; } private int getOurExplicitPort() { return 0; } @@ -1065,8 +1081,10 @@ class PacketBuilder { // let's not use an introducer on a privileged port, sounds like trouble if (ikey == null || iport < 1024 || iport > 65535 || iaddr == null || tag <= 0 || + // must be IPv4 for now as we don't send Alice IP/port, see below + iaddr.getAddress().length != 4 || (!_transport.isValid(iaddr.getAddress())) || - Arrays.equals(iaddr.getAddress(), _transport.getExternalIP())) { + (Arrays.equals(iaddr.getAddress(), _transport.getExternalIP()) && !_transport.allowLocal())) { if (_log.shouldLog(_log.WARN)) _log.warn("Cannot build a relay request to " + state.getRemoteIdentity().calculateHash() + ", as their UDP address is invalid: addr=" + addr + " index=" + i); @@ -1078,11 +1096,17 @@ class PacketBuilder { return rv; } + /** + * TODO Alice IP/port in packet will always be null/0, must be fixed to + * send a RelayRequest over IPv6 + * + */ public UDPPacket buildRelayRequest(InetAddress introHost, int introPort, byte introKey[], long introTag, SessionKey ourIntroKey, long introNonce, boolean encrypt) { UDPPacket packet = buildPacketHeader(PEER_RELAY_REQUEST_FLAG_BYTE); byte data[] = packet.getPacket().getData(); int off = HEADER_SIZE; + // FIXME must specify these if request is going over IPv6 byte ourIP[] = getOurExplicitIP(); int ourPort = getOurExplicitPort(); @@ -1206,6 +1230,7 @@ class PacketBuilder { DataHelper.toLong(data, off, 2, charlie.getRemotePort()); off += 2; + // Alice IP/Port currently ignored on receive - see UDPPacketReader byte aliceIP[] = alice.getIP(); DataHelper.toLong(data, off, 1, aliceIP.length); off++; @@ -1230,7 +1255,7 @@ class PacketBuilder { } /** - * Sends an empty unauthenticated packet for hole punching. + * Creates an empty unauthenticated packet for hole punching. * Parameters must be validated previously. */ public UDPPacket buildHolePunch(InetAddress to, int port) { @@ -1247,6 +1272,23 @@ class PacketBuilder { return packet; } + /** + * TESTING ONLY. + * Creates an arbitrary packet for unit testing. + * Null transport in constructor OK. + * + * @param type 0-15 + * @since IPv6 + */ + public UDPPacket buildPacket(byte[] data, InetAddress to, int port) { + UDPPacket packet = UDPPacket.acquire(_context, false); + byte d[] = packet.getPacket().getData(); + System.arraycopy(data, 0, d, 0, data.length); + packet.getPacket().setLength(data.length); + setTo(packet, to, port); + return packet; + } + /** * Create a new packet and add the flag byte and the time stamp. * Caller should add data starting at HEADER_SIZE. diff --git a/router/java/src/net/i2p/router/transport/udp/PacketHandler.java b/router/java/src/net/i2p/router/transport/udp/PacketHandler.java index 96ceec9c0d..e901ac8595 100644 --- a/router/java/src/net/i2p/router/transport/udp/PacketHandler.java +++ b/router/java/src/net/i2p/router/transport/udp/PacketHandler.java @@ -3,9 +3,11 @@ package net.i2p.router.transport.udp; import java.util.Date; import java.util.List; import java.util.Map; +import java.util.concurrent.BlockingQueue; import net.i2p.router.Router; import net.i2p.router.RouterContext; +import net.i2p.router.util.CoDelBlockingQueue; import net.i2p.data.DataHelper; import net.i2p.util.I2PThread; import net.i2p.util.LHMCache; @@ -26,7 +28,6 @@ class PacketHandler { private final RouterContext _context; private final Log _log; private final UDPTransport _transport; - private final UDPEndpoint _endpoint; private final EstablishmentManager _establisher; private final InboundMessageFragments _inbound; private final PeerTestManager _testManager; @@ -34,19 +35,22 @@ class PacketHandler { private volatile boolean _keepReading; private final Handler[] _handlers; private final Map _failCache; + private final BlockingQueue _inboundQueue; private static final Object DUMMY = new Object(); + private static final int TYPE_POISON = -99999; + private static final int MIN_QUEUE_SIZE = 16; + private static final int MAX_QUEUE_SIZE = 192; private static final int MIN_NUM_HANDLERS = 1; // unless < 32MB private static final int MAX_NUM_HANDLERS = 1; /** let packets be up to 30s slow */ private static final long GRACE_PERIOD = Router.CLOCK_FUDGE_FACTOR + 30*1000; - PacketHandler(RouterContext ctx, UDPTransport transport, UDPEndpoint endpoint, EstablishmentManager establisher, + PacketHandler(RouterContext ctx, UDPTransport transport, EstablishmentManager establisher, InboundMessageFragments inbound, PeerTestManager testManager, IntroductionManager introManager) { _context = ctx; _log = ctx.logManager().getLog(PacketHandler.class); _transport = transport; - _endpoint = endpoint; _establisher = establisher; _inbound = inbound; _testManager = testManager; @@ -56,6 +60,8 @@ class PacketHandler { long maxMemory = Runtime.getRuntime().maxMemory(); if (maxMemory == Long.MAX_VALUE) maxMemory = 96*1024*1024l; + int qsize = (int) Math.max(MIN_QUEUE_SIZE, Math.min(MAX_QUEUE_SIZE, maxMemory / (2*1024*1024))); + _inboundQueue = new CoDelBlockingQueue(ctx, "UDP-Receiver", qsize); int num_handlers; if (maxMemory < 32*1024*1024) num_handlers = 1; @@ -107,6 +113,7 @@ class PacketHandler { public synchronized void shutdown() { _keepReading = false; + stopQueue(); } String getHandlerStatus() { @@ -119,9 +126,55 @@ class PacketHandler { return rv.toString(); } - /** @since 0.8.8 */ - int getHandlerCount() { - return _handlers.length; + /** + * Blocking call to retrieve the next inbound packet, or null if we have + * shut down. + * + * @since IPv6 moved from UDPReceiver + */ + public void queueReceived(UDPPacket packet) throws InterruptedException { + _inboundQueue.put(packet); + } + + + /** + * Blocking for a while + * + * @since IPv6 moved from UDPReceiver + */ + private void stopQueue() { + _inboundQueue.clear(); + for (int i = 0; i < _handlers.length; i++) { + UDPPacket poison = UDPPacket.acquire(_context, false); + poison.setMessageType(TYPE_POISON); + _inboundQueue.offer(poison); + } + for (int i = 1; i <= 5 && !_inboundQueue.isEmpty(); i++) { + try { + Thread.sleep(i * 50); + } catch (InterruptedException ie) {} + } + _inboundQueue.clear(); + } + + /** + * Blocking call to retrieve the next inbound packet, or null if we have + * shut down. + * + * @since IPv6 moved from UDPReceiver + */ + public UDPPacket receiveNext() { + UDPPacket rv = null; + //int remaining = 0; + while (_keepReading && rv == null) { + try { + rv = _inboundQueue.take(); + } catch (InterruptedException ie) {} + if (rv != null && rv.getMessageType() == TYPE_POISON) + return null; + } + //_context.statManager().addRateData("udp.receiveRemaining", remaining, 0); + return rv; } /** the packet is from a peer we are establishing an outbound con to, but failed validation, so fallback */ @@ -144,7 +197,7 @@ class PacketHandler { _state = 1; while (_keepReading) { _state = 2; - UDPPacket packet = _endpoint.receive(); + UDPPacket packet = receiveNext(); _state = 3; if (packet == null) break; // keepReading is probably false, or bind failed... 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 cf7ce21928..db7fb4ea34 100644 --- a/router/java/src/net/i2p/router/transport/udp/PacketPusher.java +++ b/router/java/src/net/i2p/router/transport/udp/PacketPusher.java @@ -1,26 +1,29 @@ package net.i2p.router.transport.udp; +import java.util.List; + import net.i2p.router.RouterContext; import net.i2p.util.I2PThread; import net.i2p.util.Log; /** * Blocking thread to grab new packets off the outbound fragment - * pool and toss 'em onto the outbound packet queue + * pool and toss 'em onto the outbound packet queues. * + * Here we select which UDPEndpoint/UDPSender to send it out. */ class PacketPusher implements Runnable { // private RouterContext _context; private final Log _log; private final OutboundMessageFragments _fragments; - private final UDPSender _sender; + private final List _endpoints; private volatile boolean _alive; - public PacketPusher(RouterContext ctx, OutboundMessageFragments fragments, UDPSender sender) { + public PacketPusher(RouterContext ctx, OutboundMessageFragments fragments, List endpoints) { // _context = ctx; _log = ctx.logManager().getLog(PacketPusher.class); _fragments = fragments; - _sender = sender; + _endpoints = endpoints; } public synchronized void startup() { @@ -38,8 +41,7 @@ class PacketPusher implements Runnable { if (packets != null) { for (int i = 0; i < packets.length; i++) { if (packets[i] != null) // null for ACKed fragments - // BLOCKING if queue is full - _sender.add(packets[i]); + send(packets[i]); } } } catch (Exception e) { @@ -47,4 +49,38 @@ class PacketPusher implements Runnable { } } } + + /** + * This sends it directly out, bypassing OutboundMessageFragments + * and the PacketPusher. The only queueing is for the bandwidth limiter. + * BLOCKING if OB queue is full. + * + * @param packet non-null + * @since IPv6 + */ + public void send(UDPPacket packet) { + boolean isIPv4 = packet.getPacket().getAddress().getAddress().length == 4; + for (int j = 0; j < _endpoints.size(); j++) { + // Find the best endpoint (socket) to send this out. + // TODO if we have multiple IPv4, or multiple IPv6 endpoints, + // we have to track which one we're using in the PeerState and + // somehow set that in the UDPPacket so we're consistent + UDPEndpoint ep; + try { + ep = _endpoints.get(j); + } catch (IndexOutOfBoundsException ioobe) { + // whups, list changed + break; + } + if ((isIPv4 && ep.isIPv4()) || + ((!isIPv4) && ep.isIPv6())) { + // BLOCKING if queue is full + ep.getSender().add(packet); + return; + } + } + // not handled + _log.error("No endpoint to send " + packet); + packet.release(); + } } 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 31b2dd7bda..65e236019e 100644 --- a/router/java/src/net/i2p/router/transport/udp/PeerState.java +++ b/router/java/src/net/i2p/router/transport/udp/PeerState.java @@ -267,6 +267,12 @@ class PeerState { * and so PacketBuilder.buildPacket() works correctly. */ public static final int MIN_MTU = 620; + + /** + * IPv6/UDP header is 48 bytes, so we want MTU % 16 == 0. + */ + public static final int MIN_IPV6_MTU = 1280; + public static final int MAX_IPV6_MTU = 1472; // TODO 1488 private static final int DEFAULT_MTU = MIN_MTU; /* @@ -315,9 +321,15 @@ class PeerState { _receivePeriodBegin = now; _lastCongestionOccurred = -1; _remotePort = remotePort; - _mtu = DEFAULT_MTU; - _mtuReceive = DEFAULT_MTU; - _largeMTU = transport.getMTU(); + if (remoteIP.length == 4) { + _mtu = DEFAULT_MTU; + _mtuReceive = DEFAULT_MTU; + _largeMTU = transport.getMTU(false); + } else { + _mtu = MIN_IPV6_MTU; + _mtuReceive = MIN_IPV6_MTU; + _largeMTU = transport.getMTU(true); + } //_mtuLastChecked = -1; _lastACKSend = -1; _rto = INIT_RTO; @@ -686,6 +698,12 @@ class PeerState { public int getConcurrentSendWindow() { return _concurrentMessagesAllowed; } public int getConsecutiveSendRejections() { return _consecutiveRejections; } public boolean isInbound() { return _isInbound; } + + /** @since IPv6 */ + public boolean isIPv6() { + return _remoteIP.length == 16; + } + public long getIntroducerTime() { return _lastIntroducerTime; } public void setIntroducerTime() { _lastIntroducerTime = _context.clock().now(); } @@ -1145,12 +1163,12 @@ class PeerState { _context.statManager().addRateData("udp.mtuIncrease", _mtuIncreases); } } else if (!wantLarge && _mtu == _largeMTU) { - _mtu = MIN_MTU; + _mtu = _remoteIP.length == 4 ? MIN_MTU : MIN_IPV6_MTU; _mtuDecreases++; _context.statManager().addRateData("udp.mtuDecrease", _mtuDecreases); } } else { - _mtu = DEFAULT_MTU; + _mtu = _remoteIP.length == 4 ? DEFAULT_MTU : MIN_IPV6_MTU; } } @@ -1158,7 +1176,8 @@ class PeerState { * @since 0.9.2 */ public synchronized void setHisMTU(int mtu) { - if (mtu <= MIN_MTU || mtu >= _largeMTU) + if (mtu <= MIN_MTU || mtu >= _largeMTU || + (_remoteIP.length == 16 && mtu <= MIN_IPV6_MTU)) return; _largeMTU = mtu; if (mtu < _mtu) @@ -1225,17 +1244,27 @@ class PeerState { /** 60 */ private static final int OVERHEAD_SIZE = PacketBuilder.IP_HEADER_SIZE + PacketBuilder.UDP_HEADER_SIZE + UDPPacket.MAC_SIZE + UDPPacket.IV_SIZE; + /** 80 */ + private static final int IPV6_OVERHEAD_SIZE = PacketBuilder.IPV6_HEADER_SIZE + PacketBuilder.UDP_HEADER_SIZE + + UDPPacket.MAC_SIZE + UDPPacket.IV_SIZE; /** * @param size not including IP header, UDP header, MAC or IV */ public void packetReceived(int size) { _packetsReceived++; - size += OVERHEAD_SIZE; - if (size <= MIN_MTU) { + int minMTU; + if (_remoteIP.length == 4) { + size += OVERHEAD_SIZE; + minMTU = MIN_MTU; + } else { + size += IPV6_OVERHEAD_SIZE; + minMTU = MIN_IPV6_MTU; + } + if (size <= minMTU) { _consecutiveSmall++; if (_consecutiveSmall >= MTU_RCV_DISPLAY_THRESHOLD) - _mtuReceive = MIN_MTU; + _mtuReceive = minMTU; } else { _consecutiveSmall = 0; if (size > _mtuReceive) @@ -1289,7 +1318,7 @@ class PeerState { private int countMaxACKData() { return Math.min(PacketBuilder.ABSOLUTE_MAX_ACKS * 4, _mtu - - PacketBuilder.IP_HEADER_SIZE + - (_remoteIP.length == 4 ? PacketBuilder.IP_HEADER_SIZE : PacketBuilder.IPV6_HEADER_SIZE) - PacketBuilder.UDP_HEADER_SIZE - UDPPacket.IV_SIZE - UDPPacket.MAC_SIZE @@ -1630,11 +1659,14 @@ class PeerState { /** * how much payload data can we shove in there? - * @return MTU - 87, i.e. 533 or 1397 + * @return MTU - 87, i.e. 533 or 1397 (IPv4), MTU - 107 (IPv6) */ - private static final int fragmentSize(int mtu) { - // 46 + 20 + 8 + 13 = 74 + 13 = 87 - return mtu - (PacketBuilder.MIN_DATA_PACKET_OVERHEAD + MIN_ACK_SIZE); + private int fragmentSize() { + // 46 + 20 + 8 + 13 = 74 + 13 = 87 (IPv4) + // 46 + 40 + 8 + 13 = 74 + 13 = 107 (IPv6) + return _mtu - + (_remoteIP.length == 4 ? PacketBuilder.MIN_DATA_PACKET_OVERHEAD : PacketBuilder.MIN_IPV6_DATA_PACKET_OVERHEAD) - + MIN_ACK_SIZE; } private enum ShouldSend { YES, NO, NO_BW }; @@ -1647,7 +1679,7 @@ class PeerState { long now = _context.clock().now(); if (state.getNextSendTime() <= now) { if (!state.isFragmented()) { - state.fragment(fragmentSize(_mtu)); + state.fragment(fragmentSize()); if (state.getMessage() != null) state.getMessage().timestamp("fragment into " + state.getFragmentCount()); 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 2adecf5217..ffc512654b 100644 --- a/router/java/src/net/i2p/router/transport/udp/PeerTestManager.java +++ b/router/java/src/net/i2p/router/transport/udp/PeerTestManager.java @@ -24,6 +24,13 @@ import net.i2p.util.SimpleTimer; * Entry points are runTest() to start a new test as Alice, * and receiveTest() for all received test packets. * + * IPv6 info: All Alice-Bob and Alice-Charlie communication is via IPv4. + * The Bob-Charlie communication may be via IPv6, however Charlie must + * be IPv4-capable. + * The IP address (of Alice) in the message must be IPv4 if present, + * as we only support testing of IPv4. + * Testing of IPv6 could be added in the future. + * * From udp.html on the website:

The automation of collaborative reachability testing for peers is @@ -166,6 +173,8 @@ class PeerTestManager { /** * The next few methods are for when we are Alice + * + * @param bobIP IPv4 only */ public synchronized void runTest(InetAddress bobIP, int bobPort, SessionKey bobCipherKey, SessionKey bobMACKey) { if (_currentTest != null) { @@ -173,7 +182,7 @@ class PeerTestManager { _log.warn("We are already running a test: " + _currentTest + ", aborting test with bob = " + bobIP); return; } - if (DataHelper.eq(bobIP.getAddress(), 0, _transport.getExternalIP(), 0, 2)) { + if (_transport.isTooClose(bobIP.getAddress())) { if (_log.shouldLog(Log.WARN)) _log.warn("Not running test with Bob too close to us " + bobIP); return; @@ -304,7 +313,7 @@ class PeerTestManager { // The reply is from Bob int ipSize = testInfo.readIPSize(); - if (ipSize != 4 && ipSize != 16) { + if (ipSize != 4) { // There appears to be a bug where Bob is sending us a zero-length IP. // We could proceed without setting the IP, but then when Charlie // sends us his message, we will think we are behind a symmetric NAT @@ -366,6 +375,8 @@ class PeerTestManager { throw new UnknownHostException("port 0"); test.setAlicePortFromCharlie(testPort); byte ip[] = new byte[testInfo.readIPSize()]; + if (ip.length != 4) + throw new UnknownHostException("not IPv4"); testInfo.readIP(ip, 0); InetAddress addr = InetAddress.getByAddress(ip); test.setAliceIPFromCharlie(addr); @@ -485,7 +496,7 @@ class PeerTestManager { int fromPort = from.getPort(); if (fromPort < 1024 || fromPort > 65535 || (!_transport.isValid(fromIP)) || - DataHelper.eq(fromIP, 0, _transport.getExternalIP(), 0, 2) || + _transport.isTooClose(fromIP) || _context.blocklist().isBlocklisted(fromIP)) { // spoof check, and don't respond to privileged ports if (_log.shouldLog(Log.WARN)) @@ -505,6 +516,7 @@ class PeerTestManager { if ((testPort > 0 && (testPort < 1024 || testPort > 65535)) || (testIP != null && ((!_transport.isValid(testIP)) || + testIP.length != 4 || _context.blocklist().isBlocklisted(testIP)))) { // spoof check, and don't respond to privileged ports if (_log.shouldLog(Log.WARN)) @@ -544,7 +556,7 @@ class PeerTestManager { Long lNonce = Long.valueOf(nonce); PeerTestState state = _activeTests.get(lNonce); - if (testIP != null && DataHelper.eq(testIP, 0, _transport.getExternalIP(), 0, 2)) { + if (testIP != null && _transport.isTooClose(testIP)) { // spoof check - have to do this after receiveTestReply(), since // the field should be us there. // Let's also eliminate anybody in the same /16 @@ -641,6 +653,8 @@ class PeerTestManager { byte aliceIPData[] = new byte[sz]; try { testInfo.readIP(aliceIPData, 0); + if (sz != 4) + throw new UnknownHostException("not IPv4"); int alicePort = testInfo.readPort(); if (alicePort == 0) throw new UnknownHostException("port 0"); @@ -706,7 +720,12 @@ class PeerTestManager { PeerState charlie; RouterInfo charlieInfo = null; if (state == null) { // pick a new charlie - charlie = _transport.pickTestPeer(from); + if (from.getIP().length != 4) { + if (_log.shouldLog(Log.WARN)) + _log.warn("PeerTest over IPv6 from Alice as Bob? " + from); + return; + } + charlie = _transport.pickTestPeer(CHARLIE, from); } else { charlie = _transport.getPeerState(new RemoteHostId(state.getCharlieIP().getAddress(), state.getCharliePort())); } 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 5381bfaea4..55fa9788d1 100644 --- a/router/java/src/net/i2p/router/transport/udp/UDPAddress.java +++ b/router/java/src/net/i2p/router/transport/udp/UDPAddress.java @@ -2,16 +2,17 @@ package net.i2p.router.transport.udp; import java.net.InetAddress; import java.net.UnknownHostException; +import java.util.Map; import net.i2p.data.Base64; import net.i2p.data.RouterAddress; import net.i2p.data.SessionKey; +import net.i2p.util.LHMCache; /** * basic helper to parse out peer info from a udp address - * FIXME public for ConfigNetHelper */ -public class UDPAddress { +class UDPAddress { private final String _host; private InetAddress _hostAddress; private final int _port; @@ -37,6 +38,23 @@ public class UDPAddress { public static final String PROP_INTRO_KEY_PREFIX = "ikey"; public static final String PROP_INTRO_TAG_PREFIX = "itag"; static final int MAX_INTRODUCERS = 3; + private static final String[] PROP_INTRO_HOST; + private static final String[] PROP_INTRO_PORT; + private static final String[] PROP_INTRO_IKEY; + private static final String[] PROP_INTRO_TAG; + static { + // object churn + PROP_INTRO_HOST = new String[MAX_INTRODUCERS]; + PROP_INTRO_PORT = new String[MAX_INTRODUCERS]; + PROP_INTRO_IKEY = new String[MAX_INTRODUCERS]; + PROP_INTRO_TAG = new String[MAX_INTRODUCERS]; + for (int i = 0; i < MAX_INTRODUCERS; i++) { + PROP_INTRO_HOST[i] = PROP_INTRO_HOST_PREFIX + i; + PROP_INTRO_PORT[i] = PROP_INTRO_PORT_PREFIX + i; + PROP_INTRO_IKEY[i] = PROP_INTRO_KEY_PREFIX + i; + PROP_INTRO_TAG[i] = PROP_INTRO_TAG_PREFIX + i; + } + } public UDPAddress(RouterAddress addr) { // TODO make everything final @@ -49,33 +67,38 @@ public class UDPAddress { _port = addr.getPort(); try { String mtu = addr.getOption(PROP_MTU); - if (mtu != null) - _mtu = MTU.rectify(Integer.parseInt(mtu)); + if (mtu != null) { + boolean isIPv6 = _host != null && _host.contains(":"); + _mtu = MTU.rectify(isIPv6, Integer.parseInt(mtu)); + } } catch (NumberFormatException nfe) {} String key = addr.getOption(PROP_INTRO_KEY); - if (key != null) - _introKey = Base64.decode(key.trim()); + if (key != null) { + byte[] ik = Base64.decode(key.trim()); + if (ik != null && ik.length == SessionKey.KEYSIZE_BYTES) + _introKey = ik; + } - for (int i = MAX_INTRODUCERS; i >= 0; i--) { - String host = addr.getOption(PROP_INTRO_HOST_PREFIX + i); + for (int i = MAX_INTRODUCERS - 1; i >= 0; i--) { + String host = addr.getOption(PROP_INTRO_HOST[i]); if (host == null) continue; - String port = addr.getOption(PROP_INTRO_PORT_PREFIX + i); + String port = addr.getOption(PROP_INTRO_PORT[i]); if (port == null) continue; - String k = addr.getOption(PROP_INTRO_KEY_PREFIX + i); + String k = addr.getOption(PROP_INTRO_IKEY[i]); if (k == null) continue; byte ikey[] = Base64.decode(k); if ( (ikey == null) || (ikey.length != SessionKey.KEYSIZE_BYTES) ) continue; - String t = addr.getOption(PROP_INTRO_TAG_PREFIX + i); + String t = addr.getOption(PROP_INTRO_TAG[i]); if (t == null) continue; - int p = -1; + int p; try { p = Integer.parseInt(port); - if (p <= 0 || p > 65535) continue; + if (p < UDPTransport.MIN_PEER_PORT || p > 65535) continue; } catch (NumberFormatException nfe) { continue; } - long tag = -1; + long tag; try { tag = Long.parseLong(t); if (tag <= 0) continue; @@ -131,14 +154,10 @@ public class UDPAddress { } public String getHost() { return _host; } + InetAddress getHostAddress() { - if (_hostAddress == null) { - try { - _hostAddress = InetAddress.getByName(_host); - } catch (UnknownHostException uhe) { - _hostAddress = null; - } - } + if (_hostAddress == null) + _hostAddress = getByName(_host); return _hostAddress; } @@ -150,18 +169,17 @@ public class UDPAddress { byte[] getIntroKey() { return _introKey; } int getIntroducerCount() { return (_introAddresses == null ? 0 : _introAddresses.length); } + InetAddress getIntroducerHost(int i) { - if (_introAddresses[i] == null) { - try { - _introAddresses[i] = InetAddress.getByName(_introHosts[i]); - } catch (UnknownHostException uhe) { - _introAddresses[i] = null; - } - } + if (_introAddresses[i] == null) + _introAddresses[i] = getByName(_introHosts[i]); return _introAddresses[i]; } + int getIntroducerPort(int i) { return _introPorts[i]; } + byte[] getIntroducerKey(int i) { return _introKeys[i]; } + long getIntroducerTag(int i) { return _introTags[i]; } /** @@ -192,4 +210,71 @@ public class UDPAddress { } return rv.toString(); } + + //////////////// + // cache copied from Addresses.java but caching InetAddress instead of byte[] + + + /** + * Textual IP to InetAddress, because InetAddress.getByName() is slow. + * + * @since IPv6 + */ + private static final Map _inetAddressCache; + + static { + long maxMemory = Runtime.getRuntime().maxMemory(); + if (maxMemory == Long.MAX_VALUE) + maxMemory = 96*1024*1024l; + long min = 128; + long max = 2048; + // 512 nominal for 128 MB + int size = (int) Math.max(min, Math.min(max, 1 + (maxMemory / (256*1024)))); + _inetAddressCache = new LHMCache(size); + } + + /** + * Caching version of InetAddress.getByName(host), which is slow. + * Caches numeric host names only. + * Will resolve but not cache DNS host names. + * + * Unlike InetAddress.getByName(), we do NOT allow numeric IPs + * of the form d.d.d, d.d, or d, as these are almost certainly mistakes. + * + * @param host DNS or IPv4 or IPv6 host name; if null returns null + * @return InetAddress or null + * @since IPv6 + */ + private static InetAddress getByName(String host) { + if (host == null) + return null; + InetAddress rv; + synchronized (_inetAddressCache) { + rv = _inetAddressCache.get(host); + } + if (rv == null) { + try { + boolean isIPv4 = host.replaceAll("[0-9\\.]", "").length() == 0; + if (isIPv4 && host.replaceAll("[0-9]", "").length() != 3) + return null; + rv = InetAddress.getByName(host); + if (isIPv4 || + host.replaceAll("[0-9a-fA-F:]", "").length() == 0) { + synchronized (_inetAddressCache) { + _inetAddressCache.put(host, rv); + } + } + } catch (UnknownHostException uhe) {} + } + return rv; + } + + /** + * @since IPv6 + */ + static void clearCache() { + synchronized(_inetAddressCache) { + _inetAddressCache.clear(); + } + } } diff --git a/router/java/src/net/i2p/router/transport/udp/UDPEndpoint.java b/router/java/src/net/i2p/router/transport/udp/UDPEndpoint.java index 179649de52..b750df660a 100644 --- a/router/java/src/net/i2p/router/transport/udp/UDPEndpoint.java +++ b/router/java/src/net/i2p/router/transport/udp/UDPEndpoint.java @@ -1,15 +1,19 @@ package net.i2p.router.transport.udp; +import java.io.IOException; import java.net.DatagramSocket; import java.net.InetAddress; +import java.net.Inet4Address; +import java.net.Inet6Address; import java.net.SocketException; +import java.util.concurrent.atomic.AtomicInteger; import net.i2p.router.RouterContext; import net.i2p.util.Log; /** - * Coordinate the low level datagram socket, managing the UDPSender and - * UDPReceiver + * Coordinate the low-level datagram socket, creating and managing the UDPSender and + * UDPReceiver. */ class UDPEndpoint { private final RouterContext _context; @@ -20,8 +24,11 @@ class UDPEndpoint { private UDPReceiver _receiver; private DatagramSocket _socket; private final InetAddress _bindAddress; + private final boolean _isIPv4, _isIPv6; + private static final AtomicInteger _counter = new AtomicInteger(); /** + * @param transport may be null for unit testing ONLY * @param listenPort -1 or the requested port, may not be honored * @param bindAddress null ok */ @@ -31,22 +38,27 @@ class UDPEndpoint { _transport = transport; _bindAddress = bindAddress; _listenPort = listenPort; + _isIPv4 = bindAddress == null || bindAddress instanceof Inet4Address; + _isIPv6 = bindAddress == null || bindAddress instanceof Inet6Address; } /** caller should call getListenPort() after this to get the actual bound port and determine success */ - public synchronized void startup() { + public synchronized void startup() throws SocketException { if (_log.shouldLog(Log.DEBUG)) _log.debug("Starting up the UDP endpoint"); shutdown(); _socket = getSocket(); if (_socket == null) { _log.log(Log.CRIT, "UDP Unable to open a port"); - return; + throw new SocketException("SSU Unable to bind to a port on " + _bindAddress); } - _sender = new UDPSender(_context, _socket, "UDPSender"); - _receiver = new UDPReceiver(_context, _transport, _socket, "UDPReceiver"); + int count = _counter.incrementAndGet(); + _sender = new UDPSender(_context, _socket, "UDPSender " + count); _sender.startup(); - _receiver.startup(); + if (_transport != null) { + _receiver = new UDPReceiver(_context, _transport, _socket, "UDPReceiver " + count); + _receiver.startup(); + } } public synchronized void shutdown() { @@ -103,9 +115,7 @@ class UDPEndpoint { if (port <= 0) { // try random ports rather than just do new DatagramSocket() // so we stay out of the way of other I2P stuff - int minPort = _context.getProperty(PROP_MIN_PORT, MIN_RANDOM_PORT); - int maxPort = _context.getProperty(PROP_MAX_PORT, MAX_RANDOM_PORT); - port = minPort + _context.random().nextInt(maxPort - minPort); + port = selectRandomPort(_context); } try { if (_bindAddress == null) @@ -131,6 +141,17 @@ class UDPEndpoint { return socket; } + /** + * Pick a random port between the configured boundaries + * @since IPv6 + */ + public static int selectRandomPort(RouterContext ctx) { + int minPort = Math.min(65535, Math.max(1, ctx.getProperty(PROP_MIN_PORT, MIN_RANDOM_PORT))); + int maxPort = Math.min(65535, Math.max(minPort, ctx.getProperty(PROP_MAX_PORT, MAX_RANDOM_PORT))); + return minPort + ctx.random().nextInt(1 + maxPort - minPort); + } + + /** call after startup() to get actual port or -1 on startup failure */ public int getListenPort() { return _listenPort; } public UDPSender getSender() { return _sender; } @@ -143,15 +164,24 @@ class UDPEndpoint { public void send(UDPPacket packet) { _sender.add(packet); } - + /** * Blocking call to receive the next inbound UDP packet from any peer. - * @return null if we have shut down + * + * UNIT TESTING ONLY. Direct from the socket. + * In normal operation, UDPReceiver thread injects to PacketHandler queue. + * + * @return null if we have shut down, or on failure */ public UDPPacket receive() { - if (_receiver == null) + UDPPacket packet = UDPPacket.acquire(_context, true); + try { + _socket.receive(packet.getPacket()); + return packet; + } catch (IOException ioe) { + packet.release(); return null; - return _receiver.receiveNext(); + } } /** @@ -162,4 +192,20 @@ class UDPEndpoint { if (_sender != null) _sender.clear(); } + + /** + * @return true for wildcard too + * @since IPv6 + */ + public boolean isIPv4() { + return _isIPv4; + } + + /** + * @return true for wildcard too + * @since IPv6 + */ + public boolean isIPv6() { + return _isIPv6; + } } 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 958a6d4c9e..275152f358 100644 --- a/router/java/src/net/i2p/router/transport/udp/UDPPacket.java +++ b/router/java/src/net/i2p/router/transport/udp/UDPPacket.java @@ -10,6 +10,7 @@ import net.i2p.I2PAppContext; import net.i2p.data.DataHelper; import net.i2p.data.SessionKey; import net.i2p.router.util.CDQEntry; +import net.i2p.util.Addresses; import net.i2p.util.Log; /** @@ -292,8 +293,7 @@ class UDPPacket implements CDQEntry { StringBuilder buf = new StringBuilder(256); buf.append(_packet.getLength()); buf.append(" byte pkt with "); - buf.append(_packet.getAddress().getHostAddress()).append(":"); - buf.append(_packet.getPort()); + buf.append(Addresses.toString(_packet.getAddress().getAddress(), _packet.getPort())); //buf.append(" id=").append(System.identityHashCode(this)); if (_messageType >= 0) buf.append(" msgType=").append(_messageType); 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 e8c138a1c5..6a82fcebd5 100644 --- a/router/java/src/net/i2p/router/transport/udp/UDPPacketReader.java +++ b/router/java/src/net/i2p/router/transport/udp/UDPPacketReader.java @@ -730,6 +730,7 @@ class UDPPacketReader { return (int)DataHelper.fromLong(_message, offset, 2); } + /** @deprecated unused */ public int readAliceIPSize() { int offset = readBodyOffset(); offset += DataHelper.fromLong(_message, offset, 1); @@ -737,6 +738,7 @@ class UDPPacketReader { offset += 2; return (int)DataHelper.fromLong(_message, offset, 1); } + /** @deprecated unused */ public void readAliceIP(byte target[], int targetOffset) { int offset = readBodyOffset(); offset += DataHelper.fromLong(_message, offset, 1); @@ -746,6 +748,7 @@ class UDPPacketReader { offset++; System.arraycopy(_message, offset, target, targetOffset, sz); } + /** @deprecated unused */ public int readAlicePort() { int offset = readBodyOffset(); offset += DataHelper.fromLong(_message, offset, 1); diff --git a/router/java/src/net/i2p/router/transport/udp/UDPReceiver.java b/router/java/src/net/i2p/router/transport/udp/UDPReceiver.java index b7b06bb8d0..d8976d483e 100644 --- a/router/java/src/net/i2p/router/transport/udp/UDPReceiver.java +++ b/router/java/src/net/i2p/router/transport/udp/UDPReceiver.java @@ -3,11 +3,9 @@ package net.i2p.router.transport.udp; import java.io.IOException; import java.net.DatagramSocket; import java.util.Arrays; -import java.util.concurrent.BlockingQueue; import net.i2p.router.RouterContext; import net.i2p.router.transport.FIFOBandwidthLimiter; -import net.i2p.router.util.CoDelBlockingQueue; import net.i2p.util.I2PThread; import net.i2p.util.Log; import net.i2p.util.SimpleTimer; @@ -20,37 +18,31 @@ import net.i2p.util.SystemVersion; * waiting around too long, they are dropped. Packets should be pulled off * from the queue ASAP by a {@link PacketHandler} * + * There is a UDPReceiver for each UDPEndpoint. + * It contains a thread but no queue. Received packets are queued + * in the common PacketHandler queue. */ class UDPReceiver { private final RouterContext _context; private final Log _log; private final DatagramSocket _socket; private String _name; - private final BlockingQueue _inboundQueue; private volatile boolean _keepRunning; private final Runner _runner; private final UDPTransport _transport; - private static int __id; - private final int _id; + private final PacketHandler _handler; private static final boolean _isAndroid = SystemVersion.isAndroid(); - private static final int TYPE_POISON = -99999; - private static final int MIN_QUEUE_SIZE = 16; - private static final int MAX_QUEUE_SIZE = 192; - public UDPReceiver(RouterContext ctx, UDPTransport transport, DatagramSocket socket, String name) { _context = ctx; _log = ctx.logManager().getLog(UDPReceiver.class); - _id = ++__id; _name = name; - long maxMemory = Runtime.getRuntime().maxMemory(); - if (maxMemory == Long.MAX_VALUE) - maxMemory = 96*1024*1024l; - int qsize = (int) Math.max(MIN_QUEUE_SIZE, Math.min(MAX_QUEUE_SIZE, maxMemory / (2*1024*1024))); - _inboundQueue = new CoDelBlockingQueue(ctx, "UDP-Receiver", qsize); _socket = socket; _transport = transport; + _handler = transport.getPacketHandler(); + if (_handler == null) + throw new IllegalStateException(); _runner = new Runner(); //_context.statManager().createRateStat("udp.receivePacketSize", "How large packets received are", "udp", UDPTransport.RATES); //_context.statManager().createRateStat("udp.receiveRemaining", "How many packets are left sitting on the receiver's queue", "udp", UDPTransport.RATES); @@ -62,24 +54,12 @@ class UDPReceiver { public synchronized void startup() { //adjustDropProbability(); _keepRunning = true; - I2PThread t = new I2PThread(_runner, _name + '.' + _id, true); + I2PThread t = new I2PThread(_runner, _name, true); t.start(); } public synchronized void shutdown() { _keepRunning = false; - _inboundQueue.clear(); - for (int i = 0; i < _transport.getPacketHandlerCount(); i++) { - UDPPacket poison = UDPPacket.acquire(_context, false); - poison.setMessageType(TYPE_POISON); - _inboundQueue.offer(poison); - } - for (int i = 1; i <= 5 && !_inboundQueue.isEmpty(); i++) { - try { - Thread.sleep(i * 50); - } catch (InterruptedException ie) {} - } - _inboundQueue.clear(); } /********* @@ -171,7 +151,7 @@ class UDPReceiver { } // drop anything apparently from our IP (any port) - if (Arrays.equals(from.getIP(), _transport.getExternalIP())) { + if (Arrays.equals(from.getIP(), _transport.getExternalIP()) && !_transport.allowLocal()) { if (_log.shouldLog(Log.WARN)) _log.warn("Dropping (spoofed?) packet from ourselves"); packet.release(); @@ -194,7 +174,7 @@ class UDPReceiver { if (!rejected) { ****/ try { - _inboundQueue.put(packet); + _handler.queueReceived(packet); } catch (InterruptedException ie) { packet.release(); _keepRunning = false; @@ -229,24 +209,6 @@ class UDPReceiver { } ****/ - /** - * Blocking call to retrieve the next inbound packet, or null if we have - * shut down. - * - */ - public UDPPacket receiveNext() { - UDPPacket rv = null; - //int remaining = 0; - while (_keepRunning && rv == null) { - try { - rv = _inboundQueue.take(); - } catch (InterruptedException ie) {} - if (rv != null && rv.getMessageType() == TYPE_POISON) - return null; - } - //_context.statManager().addRateData("udp.receiveRemaining", remaining, 0); - return rv; - } private class Runner implements Runnable { //private volatile boolean _socketChanged; @@ -288,7 +250,10 @@ class UDPReceiver { // DatagramSocket javadocs: If the message is longer than the packet's length, the message is truncated. throw new IOException("packet too large! truncated and dropped from: " + packet.getRemoteHost()); } - if (size > 0) { + if (_context.commSystem().isDummy()) { + // testing + packet.release(); + } else if (size > 0) { //FIFOBandwidthLimiter.Request req = _context.bandwidthLimiter().requestInbound(size, "UDP receiver"); //_context.bandwidthLimiter().requestInbound(req, size, "UDP receiver"); FIFOBandwidthLimiter.Request req = diff --git a/router/java/src/net/i2p/router/transport/udp/UDPSender.java b/router/java/src/net/i2p/router/transport/udp/UDPSender.java index 885cc7299d..aa1affbb57 100644 --- a/router/java/src/net/i2p/router/transport/udp/UDPSender.java +++ b/router/java/src/net/i2p/router/transport/udp/UDPSender.java @@ -14,6 +14,9 @@ import net.i2p.util.Log; /** * Lowest level packet sender, pushes anything on its queue ASAP. * + * There is a UDPSender for each UDPEndpoint. + * It contains a thread and a queue. Packet to be sent are queued + * by the PacketPusher. */ class UDPSender { private final RouterContext _context; @@ -23,13 +26,16 @@ class UDPSender { private final BlockingQueue _outboundQueue; private volatile boolean _keepRunning; private final Runner _runner; + private final boolean _dummy; + private static final int TYPE_POISON = 99999; - + private static final int MIN_QUEUE_SIZE = 64; private static final int MAX_QUEUE_SIZE = 384; public UDPSender(RouterContext ctx, DatagramSocket socket, String name) { _context = ctx; + _dummy = false; // ctx.commSystem().isDummy(); _log = ctx.logManager().getLog(UDPSender.class); long maxMemory = Runtime.getRuntime().maxMemory(); if (maxMemory == Long.MAX_VALUE) @@ -176,6 +182,12 @@ class UDPSender { _log.error("Dropping large UDP packet " + psz + " bytes: " + packet); return; } + if (_dummy) { + // testing + // back to the cache + packet.release(); + return; + } try { _outboundQueue.put(packet); } catch (InterruptedException ie) { 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 bae48409b7..d5748dc206 100644 --- a/router/java/src/net/i2p/router/transport/udp/UDPTransport.java +++ b/router/java/src/net/i2p/router/transport/udp/UDPTransport.java @@ -3,6 +3,7 @@ package net.i2p.router.transport.udp; import java.io.IOException; import java.io.Writer; import java.net.InetAddress; +import java.net.SocketException; import java.net.UnknownHostException; import java.text.DecimalFormat; import java.util.ArrayList; @@ -18,6 +19,7 @@ import java.util.Set; import java.util.TreeSet; import java.util.Vector; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; import net.i2p.data.DatabaseEntry; import net.i2p.data.DataHelper; @@ -32,15 +34,19 @@ import net.i2p.router.CommSystemFacade; import net.i2p.router.OutNetMessage; import net.i2p.router.Router; import net.i2p.router.RouterContext; -import net.i2p.router.networkdb.kademlia.FloodfillNetworkDatabaseFacade; import net.i2p.router.transport.Transport; +import static net.i2p.router.transport.Transport.AddressSource.*; import net.i2p.router.transport.TransportBid; import net.i2p.router.transport.TransportImpl; +import net.i2p.router.transport.TransportUtil; +import static net.i2p.router.transport.TransportUtil.IPv6Config.*; +import static net.i2p.router.transport.udp.PeerTestState.Role.*; import net.i2p.router.transport.crypto.DHSessionKeyBuilder; import net.i2p.router.util.RandomIterator; import net.i2p.util.Addresses; import net.i2p.util.ConcurrentHashSet; import net.i2p.util.Log; +import net.i2p.util.OrderedProperties; import net.i2p.util.SimpleScheduler; import net.i2p.util.SimpleTimer; import net.i2p.util.SimpleTimer2; @@ -51,7 +57,7 @@ import net.i2p.util.Translate; */ public class UDPTransport extends TransportImpl implements TimedWeightedPriorityMessageQueue.FailedListener { private final Log _log; - private UDPEndpoint _endpoint; + private final List _endpoints; private final Object _addDropLock = new Object(); /** Peer (Hash) to PeerState */ private final Map _peersByIdent; @@ -77,19 +83,17 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority private long _lastInboundReceivedOn; private final DHSessionKeyBuilder.Factory _dhFactory; private int _mtu; + private int _mtu_ipv6; + + /** + * Do we have a public IPv6 address? + * TODO periodically update via CSFI.NetMonitor? + */ + private boolean _haveIPv6Address; /** do we need to rebuild our external router address asap? */ private boolean _needsRebuild; - /** summary info to distribute */ - private RouterAddress _externalAddress; - /** - * Port number on which we can be reached, or -1 for error, or 0 for unset - * Do NOT use this for current internal port - use _endpoint.getListenPort() - */ - private int _externalListenPort; - /** IP address of externally reachable host, or null */ - private InetAddress _externalListenHost; /** introduction key */ private SessionKey _introKey; @@ -152,7 +156,9 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority /** allowed sources of address updates */ public static final String PROP_SOURCES = "i2np.udp.addressSources"; - public static final String DEFAULT_SOURCES = "local,upnp,ssu"; + public static final String DEFAULT_SOURCES = SOURCE_INTERFACE.toConfigString() + ',' + + SOURCE_UPNP.toConfigString() + ',' + + SOURCE_SSU.toConfigString(); /** remember IP changes */ public static final String PROP_IP= "i2np.lastIP"; public static final String PROP_IP_CHANGE = "i2np.lastIPChange"; @@ -209,6 +215,7 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority _peersByIdent = new ConcurrentHashMap(128); _peersByRemoteHost = new ConcurrentHashMap(128); _dropList = new ConcurrentHashSet(2); + _endpoints = new CopyOnWriteArrayList(); // See comments in DummyThrottle.java if (USE_PRIORITY) { @@ -239,6 +246,8 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority _introducersSelectedOn = -1; _lastInboundReceivedOn = -1; _mtu = PeerState.LARGE_MTU; + _mtu_ipv6 = PeerState.MIN_IPV6_MTU; + setupPort(); _needsRebuild = true; _context.statManager().createRateStat("udp.alreadyConnected", "What is the lifetime of a reestablished session", "udp", RATES); @@ -260,14 +269,36 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority _context.simpleScheduler().addPeriodicEvent(new PingIntroducers(), MIN_EXPIRE_TIMEOUT * 3 / 4); } - public synchronized void startup() { + /** + * Pick a port if not previously configured, so that TransportManager may + * call getRequestedPort() before we've started to get a best-guess of what our + * port is going to be, and pass that to NTCP + * + * @since IPv6 + */ + private void setupPort() { + int port = getRequestedPort(); + if (port < 0) { + port = UDPEndpoint.selectRandomPort(_context); + Map changes = new HashMap(); + changes.put(PROP_INTERNAL_PORT, Integer.toString(port)); + changes.put(PROP_EXTERNAL_PORT, Integer.toString(port)); + _context.router().saveConfig(changes, null); + _log.logAlways(Log.INFO, "UDP selected random port " + port); + } + } + + private synchronized void startup() { _fragments.shutdown(); if (_pusher != null) _pusher.shutdown(); if (_handler != null) _handler.shutdown(); - if (_endpoint != null) - _endpoint.shutdown(); + for (UDPEndpoint endpoint : _endpoints) { + endpoint.shutdown(); + // should we remove? + _endpoints.remove(endpoint); + } if (_establisher != null) _establisher.shutdown(); if (_refiller != null) @@ -278,11 +309,10 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority _introManager.reset(); UDPPacket.clearCache(); + if (_log.shouldLog(Log.WARN)) _log.warn("Starting SSU transport listening"); _introKey = new SessionKey(new byte[SessionKey.KEYSIZE_BYTES]); System.arraycopy(_context.routerHash().getData(), 0, _introKey.getData(), 0, SessionKey.KEYSIZE_BYTES); - rebuildExternalAddress(); - // bind host String bindTo = _context.getProperty(PROP_BIND_INTERFACE); @@ -305,9 +335,10 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority try { bindToAddr = InetAddress.getByName(bindTo); } catch (UnknownHostException uhe) { - _log.log(Log.CRIT, "Invalid SSU bind interface specified [" + bindTo + "]", uhe); - setReachabilityStatus(CommSystemFacade.STATUS_HOSED); - return; + _log.error("Invalid SSU bind interface specified [" + bindTo + "]", uhe); + //setReachabilityStatus(CommSystemFacade.STATUS_HOSED); + //return; + // fall thru... } } @@ -317,7 +348,7 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority // we will check below after starting up the endpoint. int port; int oldIPort = _context.getProperty(PROP_INTERNAL_PORT, -1); - int oldBindPort = _endpoint != null ? _endpoint.getListenPort() : -1; + int oldBindPort = getListenPort(false); int oldEPort = _context.getProperty(PROP_EXTERNAL_PORT, -1); if (oldIPort > 0) port = oldIPort; @@ -329,14 +360,24 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority _log.warn("Binding only to " + bindToAddr); if (_log.shouldLog(Log.INFO)) _log.info("Binding to the port: " + port); - if (_endpoint == null) { - _endpoint = new UDPEndpoint(_context, this, port, bindToAddr); + if (_endpoints.isEmpty()) { + // will always be empty since we are removing them above + UDPEndpoint endpoint = new UDPEndpoint(_context, this, port, bindToAddr); + _endpoints.add(endpoint); + // TODO add additional endpoints for additional addresses/ports } else { - // todo, set bind address too - _endpoint.setListenPort(port); + // unused for now + for (UDPEndpoint endpoint : _endpoints) { + if (endpoint.isIPv4()) { + // hack, first IPv4 endpoint, FIXME + // todo, set bind address too + endpoint.setListenPort(port); + break; + } + } } setMTU(bindToAddr); - + if (_establisher == null) _establisher = new EstablishmentManager(_context, this); @@ -344,7 +385,7 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority _testManager = new PeerTestManager(_context, this); if (_handler == null) - _handler = new PacketHandler(_context, this, _endpoint, _establisher, _inboundFragments, _testManager, _introManager); + _handler = new PacketHandler(_context, this, _establisher, _inboundFragments, _testManager, _introManager); // See comments in DummyThrottle.java if (USE_PRIORITY && _refiller == null) @@ -355,15 +396,25 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority // Startup the endpoint with the requested port, check the actual port, and // take action if it failed or was different than requested or it needs to be saved - _endpoint.startup(); - int newPort = _endpoint.getListenPort(); - _externalListenPort = newPort; - if (newPort <= 0) { + int newPort = -1; + for (UDPEndpoint endpoint : _endpoints) { + try { + endpoint.startup(); + // hack, first IPv4 endpoint, FIXME + if (newPort < 0 && endpoint.isIPv4()) { + newPort = endpoint.getListenPort(); + } + } catch (SocketException se) { + _endpoints.remove(endpoint); + } + } + if (_endpoints.isEmpty()) { _log.log(Log.CRIT, "Unable to open UDP port"); setReachabilityStatus(CommSystemFacade.STATUS_HOSED); return; } - if (newPort != port || newPort != oldIPort || newPort != oldEPort) { + if (newPort > 0 && + (newPort != port || newPort != oldIPort || newPort != oldEPort)) { // attempt to use it as our external port - this will be overridden by // externalAddressReceived(...) Map changes = new HashMap(); @@ -376,7 +427,7 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority _handler.startup(); _fragments.startup(); _inboundFragments.startup(); - _pusher = new PacketPusher(_context, _fragments, _endpoint.getSender()); + _pusher = new PacketPusher(_context, _fragments, _endpoints); _pusher.startup(); if (USE_PRIORITY) _refiller.startup(); @@ -385,12 +436,25 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority _expireEvent.setIsAlive(true); _testEvent.setIsAlive(true); // this queues it for 3-6 minutes in the future... _testEvent.reschedule(10*1000); // lets requeue it for Real Soon + + // set up external addresses + // REA param is false; + // TransportManager.startListening() calls router.rebuildRouterInfo() + if (newPort > 0 && bindToAddr == null) { + for (InetAddress ia : getSavedLocalAddresses()) { + rebuildExternalAddress(ia.getHostAddress(), newPort, false); + } + } + rebuildExternalAddress(false); } public synchronized void shutdown() { destroyAll(); - if (_endpoint != null) - _endpoint.shutdown(); + for (UDPEndpoint endpoint : _endpoints) { + endpoint.shutdown(); + // should we remove? + _endpoints.remove(endpoint); + } //if (_flooder != null) // _flooder.shutdown(); if (_refiller != null) @@ -410,6 +474,12 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority _dropList.clear(); _introManager.reset(); UDPPacket.clearCache(); + UDPAddress.clearCache(); + } + + /** @since IPv6 */ + private boolean isAlive() { + return _inboundFragments.isAlive(); } /** @@ -418,23 +488,76 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority */ SessionKey getIntroKey() { return _introKey; } - /** @deprecated unused */ - public int getLocalPort() { - return _endpoint != null ? _endpoint.getListenPort() : -1; + int getExternalPort(boolean ipv6) { + RouterAddress addr = getCurrentAddress(ipv6); + if (addr != null) { + int rv = addr.getPort(); + if (rv > 0) + return rv; + } + return getRequestedPort(ipv6); } - public InetAddress getLocalAddress() { return _externalListenHost; } - public int getExternalPort() { return _externalListenPort; } - /** + * IPv4 only * @return IP or null * @since 0.9.2 */ byte[] getExternalIP() { - InetAddress ia = _externalListenHost; - if (ia == null) - return null; - return ia.getAddress(); + RouterAddress addr = getCurrentAddress(false); + if (addr != null) + return addr.getIP(); + return null; + } + + /** + * Is this IP too close to ours to trust it for + * things like relaying? + * @param ip IPv4 or IPv6 + * @since IPv6 + */ + boolean isTooClose(byte[] ip) { + if (allowLocal()) + return false; + for (RouterAddress addr : getCurrentAddresses()) { + byte[] myip = addr.getIP(); + if (myip == null || ip.length != myip.length) + continue; + if (ip.length == 4) { + if (DataHelper.eq(ip, 0, myip, 0, 2)) + return true; + } else if (ip.length == 16) { + if (DataHelper.eq(ip, 0, myip, 0, 8)) + return true; + } + } + return false; + } + + /** + * The current port of the first matching endpoint. + * To be enhanced to handle multiple endpoints of the same type. + * @return port or -1 + * @since IPv6 + */ + private int getListenPort(boolean ipv6) { + for (UDPEndpoint endpoint : _endpoints) { + if (((!ipv6) && endpoint.isIPv4()) || + (ipv6 && endpoint.isIPv6())) + return endpoint.getListenPort(); + } + return -1; + } + + /** + * The current or configured internal IPv4 port. + * UDPEndpoint should always be instantiated (and a random port picked if not configured) + * before this is called, so the returned value should be > 0 + * unless the endpoint failed to bind. + */ + @Override + public int getRequestedPort() { + return getRequestedPort(false); } /** @@ -443,15 +566,12 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority * before this is called, so the returned value should be > 0 * unless the endpoint failed to bind. */ - @Override - public int getRequestedPort() { - if (_endpoint != null) { - int rv = _endpoint.getListenPort(); - if (rv > 0) - return rv; - } + private int getRequestedPort(boolean ipv6) { + int rv = getListenPort(ipv6); + if (rv > 0) + return rv; // fallbacks - int rv = _context.getProperty(PROP_INTERNAL_PORT, -1); + rv = _context.getProperty(PROP_INTERNAL_PORT, -1); if (rv > 0) return rv; return _context.getProperty(PROP_EXTERNAL_PORT, -1); @@ -460,20 +580,30 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority /** * Set the MTU for the socket interface at addr. * @param addr null ok + * @return the mtu * @since 0.9.2 */ - private void setMTU(InetAddress addr) { + private int setMTU(InetAddress addr) { String p = _context.getProperty(PROP_DEFAULT_MTU); if (p != null) { try { - _mtu = MTU.rectify(Integer.parseInt(p)); - return; + int pmtu = Integer.parseInt(p); + _mtu = MTU.rectify(false, pmtu); + _mtu_ipv6 = MTU.rectify(true, pmtu); + return _mtu; } catch (NumberFormatException nfe) {} } int mtu = MTU.getMTU(addr); - if (mtu <= 0) - mtu = PeerState.LARGE_MTU; - _mtu = mtu; + if (addr != null && addr.getAddress().length == 16) { + if (mtu <= 0) + mtu = PeerState.MIN_IPV6_MTU; + _mtu_ipv6 = mtu; + } else { + if (mtu <= 0) + mtu = PeerState.LARGE_MTU; + _mtu = mtu; + } + return mtu; } /** @@ -482,8 +612,9 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority * @return limited to range PeerState.MIN_MTU to PeerState.LARGE_MTU. * @since 0.9.2 */ - int getMTU() { - return _mtu; + int getMTU(boolean ipv6) { + // TODO multiple interfaces of each type + return ipv6 ? _mtu_ipv6 : _mtu; } /** @@ -497,26 +628,65 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority _lastInboundReceivedOn = System.currentTimeMillis(); } + // temp prevent multiples + private boolean gotIPv4Addr = false; + private boolean gotIPv6Addr = false; + /** * From config, UPnP, local i/f, ... + * Not for info received from peers - see externalAddressReceived(Hash, ip, port) * - * @param source used for logging only - * @param ip publicly routable IPv4 only + * @param source as defined in Transport.SOURCE_xxx + * @param ip publicly routable IPv4 or IPv6, null ok * @param port 0 if unknown */ @Override - public void externalAddressReceived(String source, byte[] ip, int port) { + public void externalAddressReceived(Transport.AddressSource source, byte[] ip, int port) { if (_log.shouldLog(Log.WARN)) _log.warn("Received address: " + Addresses.toString(ip, port) + " from: " + source); + if (ip == null) + return; + if (source == SOURCE_INTERFACE && ip.length == 16) { + // must be set before isValid() call + _haveIPv6Address = true; + } if (explicitAddressSpecified()) return; String sources = _context.getProperty(PROP_SOURCES, DEFAULT_SOURCES); - if (!sources.contains(source)) + if (!sources.contains(source.toConfigString())) return; + if (!isValid(ip)) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Invalid address: " + Addresses.toString(ip, port) + " from: " + source); + return; + } + if (!isAlive()) { + if (source == SOURCE_INTERFACE || source == SOURCE_UPNP) { + try { + InetAddress ia = InetAddress.getByAddress(ip); + saveLocalAddress(ia); + } catch (UnknownHostException uhe) {} + } + return; + } + if (source == SOURCE_INTERFACE) { + // temp prevent multiples + if (ip.length == 4) { + if (gotIPv4Addr) + return; + else + gotIPv4Addr = true; + } else if (ip.length == 16) { + if (gotIPv6Addr) + return; + else + gotIPv6Addr = true; + } + } boolean changed = changeAddress(ip, port); // Assume if we have an interface with a public IP that we aren't firewalled. // If this is wrong, the peer test will figure it out and change the status. - if (changed && source.equals(Transport.SOURCE_INTERFACE)) + if (changed && ip.length == 4 && source == SOURCE_INTERFACE) setReachabilityStatus(CommSystemFacade.STATUS_OK); } @@ -527,14 +697,14 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority * Don't do anything if UPnP claims failure. */ @Override - public void forwardPortStatus(int port, int externalPort, boolean success, String reason) { + public void forwardPortStatus(byte[] ip, int port, int externalPort, boolean success, String reason) { if (_log.shouldLog(Log.WARN)) { if (success) - _log.warn("UPnP has opened the SSU port: " + port + " via external port: " + externalPort); + _log.warn("UPnP has opened the SSU port: " + port + " via " + Addresses.toString(ip, externalPort)); else _log.warn("UPnP has failed to open the SSU port: " + port + " reason: " + reason); } - if (success && _externalListenHost != null) + if (success && ip != null && getExternalIP() != null) setReachabilityStatus(CommSystemFacade.STATUS_OK); } @@ -555,9 +725,10 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority * @param ourPort >= 1024 */ void externalAddressReceived(Hash from, byte ourIP[], int ourPort) { + if (ourIP.length != 4) + return; boolean isValid = isValid(ourIP) && - ((ourPort >= MIN_EXTERNAL_PORT && ourPort <= MAX_EXTERNAL_PORT) || - ourPort == _externalListenPort || _externalListenPort <= 0); + (ourPort >= MIN_EXTERNAL_PORT && ourPort <= MAX_EXTERNAL_PORT); boolean explicitSpecified = explicitAddressSpecified(); boolean inboundRecent = _lastInboundReceivedOn + ALLOW_IP_CHANGE_INTERVAL > System.currentTimeMillis(); if (_log.shouldLog(Log.INFO)) @@ -579,33 +750,49 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority markUnreachable(from); //_context.banlist().banlistRouter(from, "They said we had an invalid IP", STYLE); return; - } else if (inboundRecent && _externalListenPort > 0 && _externalListenHost != null) { + } + RouterAddress addr = getCurrentAddress(false); + if (inboundRecent && addr != null && addr.getPort() > 0 && addr.getHost() != null) { // use OS clock since its an ordering thing, not a time thing // Note that this fails us if we switch from one IP to a second, then back to the first, // as some routers still have the first IP and will successfully connect, // leaving us thinking the second IP is still good. if (_log.shouldLog(Log.INFO)) _log.info("Ignoring IP address suggestion, since we have received an inbound con recently"); - } else if (from.equals(_lastFrom) || !eq(_lastOurIP, _lastOurPort, ourIP, ourPort)) { - _lastFrom = from; - _lastOurIP = ourIP; - _lastOurPort = ourPort; - if (_log.shouldLog(Log.INFO)) - _log.info("The router " + from + " told us we have a new IP - " - + Addresses.toString(ourIP, ourPort) + ". Wait until somebody else tells us the same thing."); } else { - if (_log.shouldLog(Log.INFO)) - _log.info(from + " and " + _lastFrom + " agree we have a new IP - " - + Addresses.toString(ourIP, ourPort) + ". Changing address."); - _lastFrom = from; - _lastOurIP = ourIP; - _lastOurPort = ourPort; - changeAddress(ourIP, ourPort); + // New IP + boolean changeIt = false; + synchronized(this) { + if (from.equals(_lastFrom) || !eq(_lastOurIP, _lastOurPort, ourIP, ourPort)) { + _lastFrom = from; + _lastOurIP = ourIP; + _lastOurPort = ourPort; + if (_log.shouldLog(Log.INFO)) + _log.info("The router " + from + " told us we have a new IP - " + + Addresses.toString(ourIP, ourPort) + ". Wait until somebody else tells us the same thing."); + } else { + _lastFrom = from; + _lastOurIP = ourIP; + _lastOurPort = ourPort; + changeIt = true; + } + } + if (changeIt) { + if (_log.shouldLog(Log.INFO)) + _log.info(from + " and " + _lastFrom + " agree we have a new IP - " + + Addresses.toString(ourIP, ourPort) + ". Changing address."); + changeAddress(ourIP, ourPort); + } } - } /** + * Possibly change our external address to the IP/port. + * IP/port are already validated, but not yet compared to current IP/port. + * We compare here. + * + * @param ourIP MUST have been previously validated with isValid() + * IPv4 or IPv6 OK * @param ourPort >= 1024 or 0 for no change */ private boolean changeAddress(byte ourIP[], int ourPort) { @@ -614,39 +801,33 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority boolean updated = false; boolean fireTest = false; + boolean isIPv6 = ourIP.length == 16; + RouterAddress current = getCurrentAddress(isIPv6); + byte[] externalListenHost = current != null ? current.getIP() : null; + int externalListenPort = current != null ? current.getPort() : getRequestedPort(isIPv6); + if (_log.shouldLog(Log.INFO)) _log.info("Change address? status = " + _reachabilityStatus + " diff = " + (_context.clock().now() - _reachabilityStatusLastUpdated) + - " old = " + _externalListenHost + ':' + _externalListenPort + + " old = " + Addresses.toString(externalListenHost, externalListenPort) + " new = " + Addresses.toString(ourIP, ourPort)); + if ((fixedPort && externalListenPort > 0) || ourPort <= 0) + ourPort = externalListenPort; + synchronized (this) { - if ( (_externalListenHost == null) || - (!eq(_externalListenHost.getAddress(), _externalListenPort, ourIP, ourPort)) ) { + if (ourPort > 0 && + !eq(externalListenHost, externalListenPort, ourIP, ourPort)) { // This prevents us from changing our IP when we are not firewalled //if ( (_reachabilityStatus != CommSystemFacade.STATUS_OK) || // (_externalListenHost == null) || (_externalListenPort <= 0) || // (_context.clock().now() - _reachabilityStatusLastUpdated > 2*TEST_FREQUENCY) ) { // they told us something different and our tests are either old or failing - try { - _externalListenHost = InetAddress.getByAddress(ourIP); - // fixed port defaults to true so we never do this - if (ourPort >= MIN_EXTERNAL_PORT && ourPort <= MAX_EXTERNAL_PORT && !fixedPort) - _externalListenPort = ourPort; if (_log.shouldLog(Log.WARN)) _log.warn("Trying to change our external address to " + - Addresses.toString(ourIP, _externalListenPort)); - if (_externalListenPort > 0) { - rebuildExternalAddress(); - replaceAddress(_externalAddress); - updated = true; - } - } catch (UnknownHostException uhe) { - _externalListenHost = null; - if (_log.shouldLog(Log.WARN)) - _log.warn("Error trying to change our external address to " + - Addresses.toString(ourIP, ourPort), uhe); - } + Addresses.toString(ourIP, ourPort)); + RouterAddress newAddr = rebuildExternalAddress(ourIP, ourPort, true); + updated = newAddr != null; //} else { // // they told us something different, but our tests are recent and positive, // // so lets test again @@ -666,13 +847,16 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority } else if (updated) { _context.statManager().addRateData("udp.addressUpdated", 1); Map changes = new HashMap(); - if (!fixedPort) + if (ourIP.length == 4 && !fixedPort) changes.put(PROP_EXTERNAL_PORT, Integer.toString(ourPort)); // queue a country code lookup of the new IP - _context.commSystem().queueLookup(ourIP); + if (ourIP.length == 4) + _context.commSystem().queueLookup(ourIP); // store these for laptop-mode (change ident on restart... or every time... when IP changes) + // IPV4 ONLY String oldIP = _context.getProperty(PROP_IP); - if (!_externalListenHost.getHostAddress().equals(oldIP)) { + String newIP = Addresses.toString(ourIP); + if (ourIP.length == 4 && !newIP.equals(oldIP)) { long lastChanged = 0; long now = _context.clock().now(); String lcs = _context.getProperty(PROP_IP_CHANGE); @@ -682,7 +866,7 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority } catch (NumberFormatException nfe) {} } - changes.put(PROP_IP, _externalListenHost.getHostAddress()); + changes.put(PROP_IP, newIP); changes.put(PROP_IP_CHANGE, Long.toString(now)); _context.router().saveConfig(changes, null); @@ -702,7 +886,7 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority _context.router().shutdown(Router.EXIT_HARD_RESTART); // doesn't return } - } else if (!fixedPort) { + } else if (ourIP.length == 4 && !fixedPort) { // save PROP_EXTERNAL_PORT _context.router().saveConfig(changes, null); } @@ -713,15 +897,33 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority return updated; } + /** + * @param laddr and raddr may be null + */ private static final boolean eq(byte laddr[], int lport, byte raddr[], int rport) { return (rport == lport) && DataHelper.eq(laddr, raddr); } - /** @param addr may be null */ + /** + * An IPv6 address is only valid if we are configured to support IPv6 + * AND we have a public IPv6 address. + * + * @param addr may be null, returns false + */ public final boolean isValid(byte addr[]) { if (addr == null) return false; - if (isPubliclyRoutable(addr)) + if (isPubliclyRoutable(addr) && + (addr.length != 16 || _haveIPv6Address)) return true; + return allowLocal(); + } + + /** + * Are we allowed to connect to local addresses? + * + * @since IPv6 + */ + boolean allowLocal() { return _context.getBooleanProperty("i2np.udp.allowLocal"); } @@ -751,12 +953,13 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority /** * Get the states for all peers at the given remote host, ignoring port. * Used for a last-chance search for a peer that changed port, by PacketHandler. + * Always returns empty list for IPv6 hostInfo. * @since 0.9.3 */ List getPeerStatesByIP(RemoteHostId hostInfo) { List rv = new ArrayList(4); byte[] ip = hostInfo.getIP(); - if (ip != null) { + if (ip != null && ip.length == 4) { for (PeerState ps : _peersByIdent.values()) { if (DataHelper.eq(ip, ps.getRemoteIP())) rv.add(ps); @@ -777,7 +980,7 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority * Remove and add to peersByRemoteHost map * @since 0.9.3 */ - public void changePeerPort(PeerState peer, int newPort) { + void changePeerPort(PeerState peer, int newPort) { int oldPort; synchronized (_addDropLock) { oldPort = peer.getRemotePort(); @@ -799,6 +1002,7 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority EstablishmentManager getEstablisher() { return _establisher; } + /** * Intercept RouterInfo entries received directly from a peer to inject them into * the PeersByCapacity listing. @@ -1128,11 +1332,14 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority if ( (altByHost != null) && (peer != altByHost) ) locked_dropPeer(altByHost, shouldBanlist, "recurse"); } + /** + * Does the IPv4 external address need to be rebuilt? + */ private boolean needsRebuild() { if (_needsRebuild) return true; // simple enough if (_context.router().isHidden()) return false; + RouterAddress addr = getCurrentAddress(false); if (introducersRequired()) { - RouterAddress addr = _externalAddress; UDPAddress ua = new UDPAddress(addr); int valid = 0; for (int i = 0; i < ua.getIntroducerCount(); i++) { @@ -1158,26 +1365,23 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority return true; } } else { - boolean rv = (_externalListenHost == null) || (_externalListenPort <= 0); + byte[] externalListenHost = addr != null ? addr.getIP() : null; + int externalListenPort = addr != null ? addr.getPort() : -1; + boolean rv = (externalListenHost == null) || (externalListenPort <= 0); if (!rv) { - RouterAddress addr = _externalAddress; - UDPAddress ua = new UDPAddress(addr); - if (ua.getIntroducerCount() > 0) + // shortcut to determine if introducers are present + if (addr.getOption("ihost0") != null) rv = true; // status == ok and we don't actually need introducers, so rebuild } - if (_log.shouldLog(Log.INFO)) { - if (rv) { - _log.info("Need to initialize our direct SSU info (" + _externalListenHost + ":" + _externalListenPort + ")"); - } else { - RouterAddress addr = _externalAddress; - UDPAddress ua = new UDPAddress(addr); - if ( (ua.getPort() <= 0) || (ua.getHost() == null) ) { - _log.info("Our direct SSU info is initialized, but not used in our address yet"); - rv = true; - } else { - //_log.info("Our direct SSU info is initialized"); - } - } + if (rv) { + if (_log.shouldLog(Log.INFO)) + _log.info("Need to initialize our direct SSU info (" + Addresses.toString(externalListenHost, externalListenPort) + ')'); + } else if (addr.getPort() <= 0 || addr.getHost() == null) { + if (_log.shouldLog(Log.INFO)) + _log.info("Our direct SSU info is initialized, but not used in our address yet"); + rv = true; + } else { + //_log.info("Our direct SSU info is initialized"); } return rv; } @@ -1216,7 +1420,7 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority void send(UDPPacket packet) { if (_log.shouldLog(Log.DEBUG)) _log.debug("Sending packet " + packet); - _endpoint.send(packet); + _pusher.send(packet); } /** @@ -1242,7 +1446,9 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority * @since 0.8.9 */ private void destroyAll() { - _endpoint.clearOutbound(); + for (UDPEndpoint endpoint : _endpoints) { + endpoint.clearOutbound(); + } int howMany = _peersByIdent.size(); // use no more than 1/4 of configured bandwidth final int burst = 8; @@ -1341,7 +1547,7 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority * @since 0.9.6 */ RouterAddress getTargetAddress(RouterInfo target) { - List addrs = target.getTargetAddresses(STYLE); + List addrs = getTargetAddresses(target); for (int i = 0; i < addrs.size(); i++) { RouterAddress addr = addrs.get(i); if (addr.getOption("ihost0") == null) { @@ -1349,7 +1555,7 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority int port = addr.getPort(); if (ip == null || port < MIN_PEER_PORT || (!isValid(ip)) || - Arrays.equals(ip, getExternalIP())) { + (Arrays.equals(ip, getExternalIP()) && !allowLocal())) { continue; } } @@ -1457,15 +1663,12 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority // we don't need the following, since we have our own queueing protected void outboundMessageReady() { throw new UnsupportedOperationException("Not used for UDP"); } - public RouterAddress startListening() { + public void startListening() { startup(); - return _externalAddress; } public void stopListening() { shutdown(); - // will this work? - _externalAddress = null; replaceAddress(null); } @@ -1481,45 +1684,99 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority * @since 0.7.12 */ @Override - public RouterAddress updateAddress() { + public List updateAddress() { rebuildExternalAddress(false); - return getCurrentAddress(); + return getCurrentAddresses(); } - private void rebuildExternalAddress() { rebuildExternalAddress(true); } + /** + * Update our IPv4 addresses AND tell the router to rebuild and republish the router info. + * + * @return the new address if changed, else null + */ + private RouterAddress rebuildExternalAddress() { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("REA1"); + return rebuildExternalAddress(true); + } - private void rebuildExternalAddress(boolean allowRebuildRouterInfo) { + /** + * Update our IPv4 address and optionally tell the router to rebuild and republish the router info. + * + * @param allowRebuildRouterInfo whether to tell the router + * @return the new address if changed, else null + */ + private RouterAddress rebuildExternalAddress(boolean allowRebuildRouterInfo) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("REA2 " + allowRebuildRouterInfo); // if the external port is specified, we want to use that to bind to even // if we don't know the external host. - _externalListenPort = _context.getProperty(PROP_EXTERNAL_PORT, -1); + int port = _context.getProperty(PROP_EXTERNAL_PORT, -1); + byte[] ip = null; + String host = null; if (explicitAddressSpecified()) { - try { - String host = _context.getProperty(PROP_EXTERNAL_HOST); - _externalListenHost = InetAddress.getByName(host); - } catch (UnknownHostException uhe) { - _externalListenHost = null; - } + host = _context.getProperty(PROP_EXTERNAL_HOST); + } else { + RouterAddress cur = getCurrentAddress(false); + if (cur != null) + host = cur.getHost(); } + return rebuildExternalAddress(host, port, allowRebuildRouterInfo); + } + /** + * Update our IPv4 or IPv6 address and optionally tell the router to rebuild and republish the router info. + * + * @param ip new ip valid IPv4 or IPv6 or null + * @param port new valid port or -1 + * @param allowRebuildRouterInfo whether to tell the router + * @return the new address if changed, else null + * @since IPv6 + */ + private RouterAddress rebuildExternalAddress(byte[] ip, int port, boolean allowRebuildRouterInfo) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("REA3 " + Addresses.toString(ip, port)); + if (ip == null) + return rebuildExternalAddress((String) null, port, allowRebuildRouterInfo); + if (isValid(ip)) + return rebuildExternalAddress(Addresses.toString(ip), port, allowRebuildRouterInfo); + return null; + } + + /** + * Update our IPv4 or IPv6 address and optionally tell the router to rebuild and republish the router info. + * + * @param host new validated IPv4 or IPv6 or DNS hostname or null + * @param port new validated port or 0/-1 + * @param allowRebuildRouterInfo whether to tell the router + * @return the new address if changed, else null + * @since IPv6 + */ + private RouterAddress rebuildExternalAddress(String host, int port, boolean allowRebuildRouterInfo) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("REA4 " + host + ':' + port); if (_context.router().isHidden()) - return; + return null; - Properties options = new Properties(); + OrderedProperties options = new OrderedProperties(); boolean directIncluded = false; - if ( allowDirectUDP() && (_externalListenPort > 0) && (_externalListenHost != null) && (isValid(_externalListenHost.getAddress())) ) { - options.setProperty(UDPAddress.PROP_PORT, String.valueOf(_externalListenPort)); - options.setProperty(UDPAddress.PROP_HOST, _externalListenHost.getHostAddress()); + // DNS name assumed IPv4 + boolean isIPv6 = host != null && host.contains(":"); + if (allowDirectUDP() && port > 0 && host != null) { + options.setProperty(UDPAddress.PROP_PORT, String.valueOf(port)); + options.setProperty(UDPAddress.PROP_HOST, host); directIncluded = true; } - boolean introducersRequired = introducersRequired(); + boolean introducersRequired = (!isIPv6) && introducersRequired(); boolean introducersIncluded = false; if (introducersRequired || !directIncluded) { int found = _introManager.pickInbound(options, PUBLIC_RELAY_COUNT); if (found > 0) { if (_log.shouldLog(Log.INFO)) - _log.info("Picked peers: " + found); + _log.info("Direct? " + directIncluded + " reqd? " + introducersRequired + + " picked introducers: " + found); _introducersSelectedOn = _context.clock().now(); introducersIncluded = true; } else { @@ -1527,7 +1784,8 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority // maybe we should fail to publish an address at all in this case? // YES that would be better if (_log.shouldLog(Log.WARN)) - _log.warn("Need introducers but we don't know any"); + _log.warn("Direct? " + directIncluded + " reqd? " + introducersRequired + + " no introducers"); } } @@ -1538,48 +1796,71 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority options.setProperty(UDPAddress.PROP_CAPACITY, ""+UDPAddress.CAPACITY_TESTING + UDPAddress.CAPACITY_INTRODUCER); // MTU since 0.9.2 - if (_mtu < PeerState.LARGE_MTU) - options.setProperty(UDPAddress.PROP_MTU, Integer.toString(_mtu)); + int mtu; + if (host == null) { + mtu = _mtu; + } else { + try { + InetAddress ia = InetAddress.getByName(host); + mtu = setMTU(ia); + } catch (UnknownHostException uhe) { + mtu = _mtu; + } + } + if (mtu < PeerState.LARGE_MTU) + options.setProperty(UDPAddress.PROP_MTU, Integer.toString(mtu)); if (directIncluded || introducersIncluded) { // This is called via TransportManager.configTransports() before startup(), prevent NPE + // Note that peers won't connect to us without this - see EstablishmentManager if (_introKey != null) options.setProperty(UDPAddress.PROP_INTRO_KEY, _introKey.toBase64()); - RouterAddress addr = new RouterAddress(); // SSU seems to regulate at about 85%, so make it a little higher. // If this is too low, both NTCP and SSU always have incremented cost and // the whole mechanism is not helpful. + int cost = DEFAULT_COST; if (ADJUST_COST && !haveCapacity(91)) - addr.setCost(DEFAULT_COST + 1); - else - addr.setCost(DEFAULT_COST); - //addr.setExpiration(null); - addr.setTransportStyle(STYLE); - addr.setOptions(options); + cost += CONGESTION_COST_ADJUSTMENT; + if (introducersIncluded) + cost += 2; + if (isIPv6) { + TransportUtil.IPv6Config config = getIPv6Config(); + if (config == IPV6_PREFERRED) + cost--; + else if (config == IPV6_NOT_PREFERRED) + cost++; + } + RouterAddress addr = new RouterAddress(STYLE, options, cost); - boolean wantsRebuild = false; - if ( (_externalAddress == null) || !(_externalAddress.equals(addr)) ) - wantsRebuild = true; + RouterAddress current = getCurrentAddress(isIPv6); + boolean wantsRebuild = !addr.deepEquals(current); - RouterAddress oldAddress = _externalAddress; - _externalAddress = addr; - if (_log.shouldLog(Log.INFO)) - _log.info("Address rebuilt: " + addr); - replaceAddress(addr, oldAddress); - if (allowRebuildRouterInfo && wantsRebuild) - _context.router().rebuildRouterInfo(); - _needsRebuild = false; + if (wantsRebuild) { + if (_log.shouldLog(Log.INFO)) + _log.info("Address rebuilt: " + addr); + replaceAddress(addr); + if (allowRebuildRouterInfo) + _context.router().rebuildRouterInfo(); + } else { + addr = null; + } + if (!isIPv6) + _needsRebuild = false; + return addr; } else { if (_log.shouldLog(Log.WARN)) _log.warn("Wanted to rebuild my SSU address, but couldn't specify either the direct or indirect info (needs introducers? " + introducersRequired + ")", new Exception("source")); _needsRebuild = true; + return null; } } /** - * Replace then tell NTCP that we changed. + * Replace then tell NTCP that we changed. + * + * @param address the new address or null to remove all */ @Override protected void replaceAddress(RouterAddress address) { @@ -1587,6 +1868,13 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority _context.commSystem().notifyReplaceAddress(address); } + /** + * Calls replaceAddress(address), then shuts down the router if + * dynamic keys is enabled, which it never is, so all this is unused. + * + * @param address the new address or null to remove all + */ +/**** protected void replaceAddress(RouterAddress address, RouterAddress oldAddress) { replaceAddress(address); if (oldAddress != null) { @@ -1612,7 +1900,11 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority } } } +****/ + /** + * Do we require introducers? + */ public boolean introducersRequired() { /****************** * Don't do this anymore, as we are removing the checkbox from the UI, @@ -1656,7 +1948,7 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority (!_context.router().isHidden()) && (!introducersRequired()) && haveCapacity() && - (!((FloodfillNetworkDatabaseFacade)_context.netDb()).floodfillEnabled()) && + (!_context.netDb().floodfillEnabled()) && _introManager.introducedCount() < IntroductionManager.MAX_OUTBOUND && _introManager.introducedCount() < getMaxConnections() / 4; } @@ -1674,13 +1966,9 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority return ""; } - /** @since 0.8.8 */ - int getPacketHandlerCount() { - PacketHandler handler = _handler; - if (handler != null) - return handler.getHandlerCount(); - else - return 0; + /** @since IPv6 */ + PacketHandler getPacketHandler() { + return _handler; } public void failed(OutboundMessageState msg) { failed(msg, true); } @@ -1714,6 +2002,9 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority // dropPeer(msg.getPeer(), false); //else if (consecutive > 2 * MAX_CONSECUTIVE_FAILED) // they're sending us data, but we cant reply? // dropPeer(msg.getPeer(), false); + } else { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Failed sending " + msg + " to " + msg.getPeer()); } noteSend(msg, false); if (m != null) @@ -2202,6 +2493,7 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority appendSortLinks(buf, urlBase, sortFlags, _("Sort by peer hash"), FLAG_ALPHA); buf.append("

").append(_("Dir")) + .append("").append(_("IPv6")) .append("").append(_("Idle")).append("
"); appendSortLinks(buf, urlBase, sortFlags, _("Sort by idle inbound"), FLAG_IDLE_IN); buf.append(" / "); @@ -2244,8 +2536,7 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority out.write(buf.toString()); buf.setLength(0); long now = _context.clock().now(); - for (Iterator iter = peers.iterator(); iter.hasNext(); ) { - PeerState peer = (PeerState)iter.next(); + for (PeerState peer : peers) { if (now-peer.getLastReceiveTime() > 60*60*1000) continue; // don't include old peers @@ -2289,6 +2580,13 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority //if (ip != null) // buf.append(' ').append(_context.blocklist().toStr(ip)); buf.append(""); + + buf.append("
"); + if (peer.isIPv6()) + buf.append("✓"); + else + buf.append(" "); + buf.append("

").append(_("SUMMARY")).append("
").append(_("SUMMARY")).append(""); buf.append(formatKBps(bpsIn)).append(THINSP).append(formatKBps(bpsOut)); - long x = numPeers > 0 ? uptimeMsTotal/numPeers : 0; + long x = uptimeMsTotal/numPeers; buf.append("").append(DataHelper.formatDuration2(x)); - x = numPeers > 0 ? offsetTotal/numPeers : 0; + x = offsetTotal/numPeers; buf.append("").append(DataHelper.formatDuration2(x)).append(""); - buf.append(numPeers > 0 ? cwinTotal/(numPeers*1024) + "K" : "0K"); + buf.append(cwinTotal/(numPeers*1024) + "K"); buf.append(" "); - buf.append(numPeers > 0 ? DataHelper.formatDuration2(rttTotal/numPeers) : '0'); + buf.append(DataHelper.formatDuration2(rttTotal/numPeers)); //buf.append(" "); buf.append(""); - buf.append(numPeers > 0 ? DataHelper.formatDuration2(rtoTotal/numPeers) : '0'); + buf.append(DataHelper.formatDuration2(rtoTotal/numPeers)); buf.append("").append(_mtu).append(""); buf.append(sendTotal).append("").append(recvTotal).append("").append(resentTotal); @@ -2449,6 +2748,7 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority buf.append(" pBRH direct: ").append(dir).append(" indirect: ").append(indir); buf.append("
\n"); /***** @@ -2533,7 +2833,8 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority long pingCutoff = now - (2 * 60*60*1000); long pingFirewallCutoff = now - PING_FIREWALL_CUTOFF; boolean shouldPingFirewall = _reachabilityStatus != CommSystemFacade.STATUS_OK; - boolean pingOneOnly = shouldPingFirewall && _externalListenPort == _endpoint.getListenPort(); + int currentListenPort = getListenPort(false); + boolean pingOneOnly = shouldPingFirewall && getExternalPort(false) == currentListenPort; boolean shortLoop = shouldPingFirewall; _expireBuffer.clear(); _runCount++; @@ -2666,27 +2967,53 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority return _reachabilityStatus; } + @Override public void recheckReachability() { _testEvent.runTest(); } - PeerState pickTestPeer(RemoteHostId dontInclude) { + /** + * Pick a Bob (if we are Alice) or a Charlie (if we are Bob). + * + * For Bob (as called from PeerTestEvent below), returns an established IPv4 peer. + * While the protocol allows Alice to select an unestablished Bob, we don't support that. + * + * For Charlie (as called from PeerTestManager), returns an established IPv4 or IPv6 peer. + * (doesn't matter how Bob and Charlie communicate) + * + * Any returned peer must advertise an IPv4 address to prove it is IPv4-capable. + * + * @param peerRole BOB or CHARLIE only + * @param dontInclude may be null + * @return IPv4 peer or null + */ + PeerState pickTestPeer(PeerTestState.Role peerRole, RemoteHostId dontInclude) { + if (peerRole == ALICE) + throw new IllegalArgumentException(); List peers = new ArrayList(_peersByIdent.values()); for (Iterator iter = new RandomIterator(peers); iter.hasNext(); ) { PeerState peer = iter.next(); if ( (dontInclude != null) && (dontInclude.equals(peer.getRemoteHostId())) ) continue; + // enforce IPv4 connection for BOB + byte[] ip = peer.getRemoteIP(); + if (peerRole == BOB && ip.length != 4) + continue; + // enforce IPv4 advertised for all RouterInfo peerInfo = _context.netDb().lookupRouterInfoLocally(peer.getRemotePeer()); if (peerInfo == null) continue; - RouterAddress addr = peerInfo.getTargetAddress(STYLE); - if (addr == null) - continue; - byte[] ip = addr.getIP(); + ip = null; + List addrs = getTargetAddresses(peerInfo); + for (RouterAddress addr : addrs) { + ip = addr.getIP(); + if (ip != null && ip.length == 4) + break; + } if (ip == null) continue; - if (DataHelper.eq(ip, 0, getExternalIP(), 0, 2)) + if (isTooClose(ip)) continue; return peer; } @@ -2699,6 +3026,9 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority //return ( (val != null) && ("true".equals(val)) ); } + /** + * Initiate a test (we are Alice) + */ private class PeerTestEvent extends SimpleTimer2.TimedEvent { private volatile boolean _alive; /** when did we last test our reachability */ @@ -2725,7 +3055,7 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority } private void runTest() { - PeerState bob = pickTestPeer(null); + PeerState bob = pickTestPeer(BOB, null); if (bob != null) { if (_log.shouldLog(Log.INFO)) _log.info("Running periodic test with bob = " + bob); diff --git a/router/java/src/net/i2p/router/tunnel/TunnelGatewayPumper.java b/router/java/src/net/i2p/router/tunnel/TunnelGatewayPumper.java index 7dfe8dc764..680a23966e 100644 --- a/router/java/src/net/i2p/router/tunnel/TunnelGatewayPumper.java +++ b/router/java/src/net/i2p/router/tunnel/TunnelGatewayPumper.java @@ -42,10 +42,14 @@ class TunnelGatewayPumper implements Runnable { _context = ctx; _wantsPumping = new LinkedHashSet(16); _backlogged = new HashSet(16); - long maxMemory = Runtime.getRuntime().maxMemory(); - if (maxMemory == Long.MAX_VALUE) - maxMemory = 96*1024*1024l; - _pumpers = (int) Math.max(MIN_PUMPERS, Math.min(MAX_PUMPERS, 1 + (maxMemory / (32*1024*1024)))); + if (ctx.getBooleanProperty("i2p.dummyTunnelManager")) { + _pumpers = 1; + } else { + long maxMemory = Runtime.getRuntime().maxMemory(); + if (maxMemory == Long.MAX_VALUE) + maxMemory = 96*1024*1024l; + _pumpers = (int) Math.max(MIN_PUMPERS, Math.min(MAX_PUMPERS, 1 + (maxMemory / (32*1024*1024)))); + } for (int i = 0; i < _pumpers; i++) new I2PThread(this, "Tunnel GW pumper " + (i+1) + '/' + _pumpers, true).start(); } diff --git a/router/java/test/junit/net/i2p/router/SSUDemo.java b/router/java/test/junit/net/i2p/router/SSUDemo.java index 30db9d1736..fb622294c0 100644 --- a/router/java/test/junit/net/i2p/router/SSUDemo.java +++ b/router/java/test/junit/net/i2p/router/SSUDemo.java @@ -27,14 +27,16 @@ public class SSUDemo { RouterContext _us; public static void main(String args[]) { + boolean testNTCP = args.length > 0 && args[0].equals("ntcp"); SSUDemo demo = new SSUDemo(); - demo.run(); + demo.run(testNTCP); } public SSUDemo() {} - public void run() { + + public void run(boolean testNTCP) { String cfgFile = "router.config"; - Properties envProps = getEnv(); + Properties envProps = getEnv(testNTCP); Router r = new Router(cfgFile, envProps); r.runRouter(); _us = r.getContext(); @@ -50,21 +52,36 @@ public class SSUDemo { loadPeers(); } - private Properties getEnv() { - Properties envProps = System.getProperties(); - // disable the TCP transport, as its deprecated - envProps.setProperty("i2np.tcp.disable", "true"); + private static Properties getEnv(boolean testNTCP) { + Properties envProps = new Properties(); + // disable one of the transports and UPnP + if (testNTCP) + envProps.setProperty("i2np.udp.enable", "false"); + else + envProps.setProperty("i2np.ntcp.enable", "false"); + envProps.setProperty("i2np.upnp.enable", "false"); // we want SNTP synchronization for replay prevention envProps.setProperty("time.disabled", "false"); // allow 127.0.0.1/10.0.0.1/etc (useful for testing). If this is false, // peers who say they're on an invalid IP are banlisted envProps.setProperty("i2np.udp.allowLocal", "true"); + envProps.setProperty("i2np.ntcp.allowLocal", "true"); + // IPv6 + envProps.setProperty("i2np.udp.ipv6", "enable"); + envProps.setProperty("i2np.ntcp.ipv6", "enable"); // explicit IP+port. at least one router on the net has to have their IP+port // set, since there has to be someone to detect one's IP off. most don't need // to set these though - envProps.setProperty("i2np.udp.host", "127.0.0.1"); - envProps.setProperty("i2np.udp.internalPort", "12000"); - envProps.setProperty("i2np.udp.port", "12000"); + //envProps.setProperty("i2np.udp.host", "127.0.0.1"); + envProps.setProperty("i2np.udp.host", "::1"); + envProps.setProperty("i2np.ntcp.autoip", "false"); + envProps.setProperty("i2np.ntcp.hostname", "::1"); + // we don't have a context yet to use its random + String port = Integer.toString(44000 + (((int) System.currentTimeMillis()) & (16384 - 1))); + envProps.setProperty("i2np.udp.internalPort", port); + envProps.setProperty("i2np.udp.port", port); + envProps.setProperty("i2np.ntcp.autoport", "false"); + envProps.setProperty("i2np.ntcp.port", port); // disable I2CP, the netDb, peer testing/profile persistence, and tunnel // creation/management envProps.setProperty("i2p.dummyClientFacade", "true"); @@ -73,18 +90,28 @@ public class SSUDemo { envProps.setProperty("i2p.dummyTunnelManager", "true"); // set to false if you want to use HMAC-SHA256-128 instead of HMAC-MD5-128 as // the SSU MAC - envProps.setProperty("i2p.HMACMD5", "true"); + //envProps.setProperty("i2p.HMACMD5", "true"); // if you're using the HMAC MD5, by default it will use a 32 byte MAC field, // which is a bug, as it doesn't generate the same values as a 16 byte MAC field. // set this to false if you don't want the bug - envProps.setProperty("i2p.HMACBrokenSize", "false"); + //envProps.setProperty("i2p.HMACBrokenSize", "false"); // no need to include any stats in the routerInfo we send to people on SSU // session establishment envProps.setProperty("router.publishPeerRankings", "false"); // write the logs to ./logs/log-router-*.txt (logger configured with the file // ./logger.config, or another config file specified as // -Dlogger.configLocation=blah) - envProps.setProperty("loggerFilenameOverride", "logs/log-router-@.txt"); + // avoid conflicts over log + envProps.setProperty("loggerFilenameOverride", "logs/log-router-" + port + "-@.txt"); + System.setProperty("wrapper.logfile", "wrapper-" + port + ".log"); + // avoid conflicts over key backup etc. so we don't all use the same keys + envProps.setProperty("router.keyBackupDir", "keyBackup/router-" + port); + envProps.setProperty("router.info.location", "router-" + port + ".info"); + envProps.setProperty("router.keys.location", "router-" + port + ".keys"); + envProps.setProperty("router.configLocation", "router-" + port + ".config"); + envProps.setProperty("router.pingFile", "router-" + port + ".ping"); + // avoid conflicts over blockfile + envProps.setProperty("i2p.naming.impl", "net.i2p.client.naming.HostsTxtNamingService"); return envProps; } @@ -98,14 +125,15 @@ public class SSUDemo { } /** random place for storing router info files - written as $dir/base64(SHA256(info.getIdentity)) */ - private File getInfoDir() { return new File("/tmp/ssuDemoInfo/"); } + private static File getInfoDir() { return new File("/tmp/ssuDemoInfo/"); } - private void storeMyInfo(RouterInfo info) { + private static void storeMyInfo(RouterInfo info) { File infoDir = getInfoDir(); if (!infoDir.exists()) infoDir.mkdirs(); FileOutputStream fos = null; File infoFile = new File(infoDir, info.getIdentity().calculateHash().toBase64()); + infoFile.deleteOnExit(); try { fos = new FileOutputStream(infoFile); info.writeBytes(fos); @@ -165,7 +193,8 @@ public class SSUDemo { out.setPriority(100); out.setTarget(ri); FooMessage data = new FooMessage(_us, new byte[] { 0x0, 0x1, 0x2, 0x3 }); - System.out.println("SEND: " + Base64.encode(data.getData())); + System.out.println("SEND: " + Base64.encode(data.getData()) + " to " + + ri.getIdentity().calculateHash()); out.setMessage(data); // job fired if we can't contact them, or if it takes too long to get an ACK out.setOnFailedSendJob(null); @@ -198,45 +227,57 @@ public class SSUDemo { public FooJobBuilder() { I2NPMessageImpl.registerBuilder(new FooBuilder(), FooMessage.MESSAGE_TYPE); } + public Job createJob(I2NPMessage receivedMessage, RouterIdentity from, Hash fromHash) { return new FooHandleJob(_us, receivedMessage, from, fromHash); } } - private class FooHandleJob extends JobImpl { - private I2NPMessage _msg; + + private static class FooHandleJob extends JobImpl { + private final I2NPMessage _msg; + private final Hash _from; + public FooHandleJob(RouterContext ctx, I2NPMessage receivedMessage, RouterIdentity from, Hash fromHash) { super(ctx); _msg = receivedMessage; + _from = fromHash; } + public void runJob() { // we know its a FooMessage, since thats the type of message that the handler // is registered as FooMessage m = (FooMessage)_msg; - System.out.println("RECV: " + Base64.encode(m.getData())); + System.out.println("RECV FooMessage: " + Base64.encode(m.getData()) + " from " + _from); } public String getName() { return "Handle Foo message"; } } - private class FooBuilder implements I2NPMessageImpl.Builder { + + private static class FooBuilder implements I2NPMessageImpl.Builder { public I2NPMessage build(I2PAppContext ctx) { return new FooMessage(ctx, null); } } /** * Just carry some data... */ - class FooMessage extends I2NPMessageImpl { + private static class FooMessage extends I2NPMessageImpl { private byte[] _data; public static final int MESSAGE_TYPE = 17; + public FooMessage(I2PAppContext ctx, byte data[]) { super(ctx); _data = data; } + /** pull the read data off */ public byte[] getData() { return _data; } + /** specify the payload to be sent */ public void setData(byte data[]) { _data = data; } public int getType() { return MESSAGE_TYPE; } + protected int calculateWrittenLength() { return _data.length; } + public void readMessage(byte[] data, int offset, int dataSize, int type) throws I2NPMessageException { _data = new byte[dataSize]; System.arraycopy(data, offset, _data, 0, dataSize); @@ -260,22 +301,27 @@ public class SSUDemo { return new HandleJob(_us, receivedMessage, from, fromHash); } } + private class HandleJob extends JobImpl { - private I2NPMessage _msg; + private final I2NPMessage _msg; + public HandleJob(RouterContext ctx, I2NPMessage receivedMessage, RouterIdentity from, Hash fromHash) { super(ctx); _msg = receivedMessage; } + public void runJob() { // we know its a DatabaseStoreMessage, since thats the type of message that the handler // is registered as DatabaseStoreMessage m = (DatabaseStoreMessage)_msg; + System.out.println("RECV: " + m); try { _us.netDb().store(m.getKey(), (RouterInfo) m.getEntry()); } catch (IllegalArgumentException iae) { iae.printStackTrace(); } } + public String getName() { return "Handle netDb store"; } } } diff --git a/router/java/test/junit/net/i2p/router/transport/udp/UDPEndpointTestStandalone.java b/router/java/test/junit/net/i2p/router/transport/udp/UDPEndpointTestStandalone.java index 5b9d3de59c..5e304f5602 100644 --- a/router/java/test/junit/net/i2p/router/transport/udp/UDPEndpointTestStandalone.java +++ b/router/java/test/junit/net/i2p/router/transport/udp/UDPEndpointTestStandalone.java @@ -1,6 +1,8 @@ package net.i2p.router.transport.udp; +import java.net.InetAddress; import java.net.SocketException; +import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -18,11 +20,11 @@ import net.i2p.util.Log; * --zab */ public class UDPEndpointTestStandalone { - private RouterContext _context; - private Log _log; + private final RouterContext _context; + private final Log _log; private UDPEndpoint _endpoints[]; - private boolean _beginTest; - private List _sentNotReceived; + private volatile boolean _beginTest; + private final List _sentNotReceived; public UDPEndpointTestStandalone(RouterContext ctx) { _context = ctx; @@ -33,12 +35,17 @@ public class UDPEndpointTestStandalone { public void runTest(int numPeers) { _log.debug("Run test("+numPeers+")"); _endpoints = new UDPEndpoint[numPeers]; - int base = 2000 + _context.random().nextInt(10000); + int base = 44000 + _context.random().nextInt(10000); for (int i = 0; i < numPeers; i++) { _log.debug("Building " + i); UDPEndpoint endpoint = new UDPEndpoint(_context, null, base + i, null); _endpoints[i] = endpoint; - endpoint.startup(); + try { + endpoint.startup(); + } catch (SocketException se) { + _log.error("die", se); + throw new RuntimeException(se); + } I2PThread read = new I2PThread(new TestRead(endpoint), "Test read " + i); I2PThread write = new I2PThread(new TestWrite(endpoint), "Test write " + i); //read.setDaemon(true); @@ -51,7 +58,7 @@ public class UDPEndpointTestStandalone { } private class TestRead implements Runnable { - private UDPEndpoint _endpoint; + private final UDPEndpoint _endpoint; public TestRead(UDPEndpoint peer) { _endpoint = peer; } @@ -83,7 +90,7 @@ public class UDPEndpointTestStandalone { } private class TestWrite implements Runnable { - private UDPEndpoint _endpoint; + private final UDPEndpoint _endpoint; public TestWrite(UDPEndpoint peer) { _endpoint = peer; } @@ -92,8 +99,16 @@ public class UDPEndpointTestStandalone { try { Thread.sleep(2000); } catch (InterruptedException ie) {} } try { Thread.sleep(2000); } catch (InterruptedException ie) {} + PacketBuilder builder = new PacketBuilder(_context, null); + InetAddress localhost = null; + try { + localhost = InetAddress.getLocalHost(); + } catch (UnknownHostException uhe) { + _log.error("die", uhe); + System.exit(0); + } _log.debug("Beginning to write"); - for (int curPacket = 0; curPacket < 10000; curPacket++) { + for (int curPacket = 0; curPacket < 2000; curPacket++) { byte data[] = new byte[1024]; _context.random().nextBytes(data); int curPeer = (curPacket % _endpoints.length); @@ -101,27 +116,21 @@ public class UDPEndpointTestStandalone { curPeer++; if (curPeer >= _endpoints.length) curPeer = 0; - short priority = 1; - long expiration = -1; - UDPPacket packet = UDPPacket.acquire(_context, true); - //try { - if (true) throw new RuntimeException("fixme"); - //packet.initialize(priority, expiration, InetAddress.getLocalHost(), _endpoints[curPeer].getListenPort()); - // Following method is commented out in UDPPacket - //packet.writeData(data, 0, 1024); - packet.getPacket().setLength(1024); + UDPPacket packet = builder.buildPacket(data, localhost, _endpoints[curPeer].getListenPort()); int outstanding = _sentNotReceived.size() + 1; _sentNotReceived.add(new ByteArray(data, 0, 1024)); _log.debug("Sending packet " + curPacket + " with outstanding " + outstanding); + try { _endpoint.send(packet); - //try { Thread.sleep(10); } catch (InterruptedException ie) {} - //} catch (UnknownHostException uhe) { - // _log.error("foo!", uhe); - //} - //if (_log.shouldLog(Log.DEBUG)) { + } catch (Exception e) { + _log.error("die", e); + break; + } + try { Thread.sleep(3); } catch (InterruptedException ie) {} + //if (_log.shouldLog(Log.DEBUG)) // _log.debug("Sent to " + _endpoints[curPeer].getListenPort() + " from " + _endpoint.getListenPort()); - //} } + _log.debug("Done sending packets"); try { Thread.sleep(10*1000); } catch (InterruptedException e) {} System.exit(0); } @@ -133,7 +142,16 @@ public class UDPEndpointTestStandalone { Properties props = new Properties(); props.setProperty("stat.logFile", "udpEndpointTest.stats"); props.setProperty("stat.logFilters", "*"); - UDPEndpointTestStandalone test = new UDPEndpointTestStandalone(new RouterContext(null, props)); + props.setProperty("i2p.dummyClientFacade", "true"); + props.setProperty("i2p.dummyNetDb", "true"); + props.setProperty("i2p.dummyPeerManager", "true"); + props.setProperty("i2p.dummyTunnelManager", "true"); + props.setProperty("i2p.vmCommSystem", "true"); + props.setProperty("i2np.bandwidth.inboundKBytesPerSecond", "9999"); + props.setProperty("i2np.bandwidth.outboundKBytesPerSecond", "9999"); + RouterContext ctx = new RouterContext(null, props); + ctx.initAll(); + UDPEndpointTestStandalone test = new UDPEndpointTestStandalone(ctx); test.runTest(2); } }