Files
i2p.i2p/router/java/src/net/i2p/router/transport/CommSystemFacadeImpl.java

582 lines
22 KiB
Java

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.io.IOException;
import java.io.Writer;
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;
import net.i2p.data.RouterAddress;
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.Log;
import net.i2p.util.SimpleScheduler;
import net.i2p.util.SimpleTimer;
import net.i2p.util.Translate;
public class CommSystemFacadeImpl extends CommSystemFacade {
private final Log _log;
private final RouterContext _context;
private TransportManager _manager;
private GeoIP _geoIP;
public CommSystemFacadeImpl(RouterContext context) {
_context = context;
_log = _context.logManager().getLog(CommSystemFacadeImpl.class);
_context.statManager().createRateStat("transport.getBidsJobTime", "How long does it take?", "Transport", new long[] { 10*60*1000l });
startGeoIP();
}
public void startup() {
_log.info("Starting up the comm system");
_manager = new TransportManager(_context);
_manager.startListening();
startTimestamper();
}
/**
* Cannot be restarted.
*/
public void shutdown() {
if (_manager != null)
_manager.shutdown();
}
public void restart() {
if (_manager == null)
startup();
else
_manager.restart();
}
@Override
public int countActivePeers() { return (_manager == null ? 0 : _manager.countActivePeers()); }
@Override
public int countActiveSendPeers() { return (_manager == null ? 0 : _manager.countActiveSendPeers()); }
@Override
public boolean haveInboundCapacity(int pct) { return (_manager == null ? false : _manager.haveInboundCapacity(pct)); }
@Override
public boolean haveOutboundCapacity(int pct) { return (_manager == null ? false : _manager.haveOutboundCapacity(pct)); }
@Override
public boolean haveHighOutboundCapacity() { return (_manager == null ? false : _manager.haveHighOutboundCapacity()); }
/**
* @param percentToInclude 1-100
* @return Framed average clock skew of connected peers in milliseconds, or the clock offset if we cannot answer.
* Average is calculated over the middle "percentToInclude" peers.
* Todo: change Vectors to milliseconds
*/
@Override
public long getFramedAveragePeerClockSkew(int percentToInclude) {
if (_manager == null) {
return _context.clock().getOffset();
}
Vector skews = _manager.getClockSkews();
if (skews == null ||
skews.isEmpty() ||
(skews.size() < 5 && _context.clock().getUpdatedSuccessfully())) {
return _context.clock().getOffset();
}
// Going to calculate, sort them
Collections.sort(skews);
if (_log.shouldLog(Log.DEBUG))
_log.debug("Clock skews: " + skews);
// Calculate frame size
int frameSize = Math.max((skews.size() * percentToInclude / 100), 1);
int first = (skews.size() / 2) - (frameSize / 2);
int last = Math.min((skews.size() / 2) + (frameSize / 2), skews.size() - 1);
// Sum skew values
long sum = 0;
for (int i = first; i <= last; i++) {
long value = ((Long) (skews.get(i))).longValue();
//if (_log.shouldLog(Log.DEBUG))
// _log.debug("Adding clock skew " + i + " valued " + value + " s.");
sum = sum + value;
}
// Calculate average
return sum * 1000 / frameSize;
}
public List<TransportBid> 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) {
//GetBidsJob j = new GetBidsJob(_context, this, msg);
//j.runJob();
long before = _context.clock().now();
GetBidsJob.getBids(_context, this, msg);
_context.statManager().addRateData("transport.getBidsJobTime", _context.clock().now() - before, 0);
}
@Override
public boolean isBacklogged(Hash dest) {
return _manager != null && _manager.isBacklogged(dest);
}
@Override
public boolean isEstablished(Hash dest) {
return _manager != null && _manager.isEstablished(dest);
}
@Override
public boolean wasUnreachable(Hash dest) {
return _manager != null && _manager.wasUnreachable(dest);
}
@Override
public byte[] getIP(Hash dest) {
return _manager.getIP(dest);
}
@Override
public List getMostRecentErrorMessages() {
return _manager.getMostRecentErrorMessages();
}
@Override
public short getReachabilityStatus() {
if (_manager == null) return STATUS_UNKNOWN;
if (_context.router().isHidden()) return STATUS_OK;
return _manager.getReachabilityStatus();
}
@Override
public void recheckReachability() { _manager.recheckReachability(); }
@Override
public void renderStatusHTML(Writer out, String urlBase, int sortFlags) throws IOException {
_manager.renderStatusHTML(out, urlBase, sortFlags);
}
/** @return non-null, possibly empty */
@Override
public Set<RouterAddress> createAddresses() {
// No, don't do this, it makes it almost impossible to build inbound tunnels
//if (_context.router().isHidden())
// return Collections.EMPTY_SET;
Map<String, RouterAddress> addresses = null;
boolean newCreated = false;
if (_manager != null) {
addresses = _manager.getAddresses();
} else {
addresses = new HashMap(1);
newCreated = true;
}
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);
}
if (_log.shouldLog(Log.INFO))
_log.info("Creating addresses: " + addresses + " isNew? " + newCreated, new Exception("creator"));
return new HashSet(addresses.values());
}
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<String, String> 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?
*/
@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
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)) {
nport = UDPAddr.getOption(UDPAddress.PROP_PORT);
}
if (_log.shouldLog(Log.INFO))
_log.info("old: " + oport + " config: " + cport + " new: " + nport);
if (nport == null || nport.length() <= 0)
return;
if (oport == null || ! oport.equals(nport)) {
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.valueOf(enabled).booleanValue() && 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.valueOf(enabled).booleanValue() && 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;
}
/*
* GeoIP stuff
*
* This is only used in the router console for now, but we put it here because
* 1) it's a lot easier, and 2) we could use it in the future for peer selection,
* tunnel selection, shitlisting, etc.
*/
/* We hope the routerinfos are read in and things have settled down by now, but it's not required to be so */
private static final int START_DELAY = 5*60*1000;
private static final int LOOKUP_TIME = 30*60*1000;
private void startGeoIP() {
_geoIP = new GeoIP(_context);
_context.simpleScheduler().addEvent(new QueueAll(), START_DELAY);
}
/**
* Collect the IPs for all routers in the DB, and queue them for lookup,
* then fire off the periodic lookup task for the first time.
*/
private class QueueAll implements SimpleTimer.TimedEvent {
public void timeReached() {
for (Iterator<Hash> iter = _context.netDb().getAllRouters().iterator(); iter.hasNext(); ) {
RouterInfo ri = _context.netDb().lookupRouterInfoLocally(iter.next());
if (ri == null)
continue;
String host = getIPString(ri);
if (host == null)
continue;
_geoIP.add(host);
}
_context.simpleScheduler().addPeriodicEvent(new Lookup(), 5000, LOOKUP_TIME);
}
}
private class Lookup implements SimpleTimer.TimedEvent {
public void timeReached() {
_geoIP.blockingLookup();
}
}
@Override
public void queueLookup(byte[] ip) {
_geoIP.add(ip);
}
/**
* @return two-letter lower-case country code or null
* @since 0.8.11
*/
@Override
public String getOurCountry() {
return _context.getProperty(GeoIP.PROP_IP_COUNTRY);
}
/**
* Are we in a bad place
* @since 0.8.13
*/
public boolean isInBadCountry() {
String us = getOurCountry();
return us != null && (BadCountries.contains(us) || _context.getBooleanProperty("router.forceBadCountry"));
}
/**
* Uses the transport IP first because that lookup is fast,
* then the SSU 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)
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);
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");
}
/** full name for a country code, or the code if we don't know the name */
@Override
public String getCountryName(String c) {
if (_geoIP == null)
return c;
String n = _geoIP.fullName(c);
if (n == null)
return c;
return n;
}
private static final String BUNDLE_NAME = "net.i2p.router.web.messages";
/** Provide a consistent "look" for displaying router IDs in the console */
@Override
public String renderPeerHTML(Hash peer) {
String h = peer.toBase64().substring(0, 4);
StringBuilder buf = new StringBuilder(128);
String c = getCountry(peer);
if (c != null) {
String countryName = getCountryName(c);
if (countryName.length() > 2)
countryName = Translate.getString(countryName, _context, BUNDLE_NAME);
buf.append("<img height=\"11\" width=\"16\" alt=\"").append(c.toUpperCase(Locale.US)).append("\" title=\"");
buf.append(countryName);
buf.append("\" src=\"/flags.jsp?c=").append(c).append("\"> ");
}
buf.append("<tt>");
boolean found = _context.netDb().lookupRouterInfoLocally(peer) != null;
if (found)
buf.append("<a title=\"").append(_("NetDb entry")).append("\" href=\"netdb?r=").append(h).append("\">");
buf.append(h);
if (found)
buf.append("</a>");
buf.append("</tt>");
return buf.toString();
}
/** @since 0.8.13 */
@Override
public boolean isDummy() { return false; }
/**
* Translate
*/
private final String _(String s) {
return Translate.getString(s, _context, BUNDLE_NAME);
}
/*
* Timestamper stuff
*
* This is used as a backup to NTP over UDP.
* @since 0.7.12
*/
private static final int TIME_START_DELAY = 5*60*1000;
private static final int TIME_REPEAT_DELAY = 10*60*1000;
/** @since 0.7.12 */
private void startTimestamper() {
_context.simpleScheduler().addPeriodicEvent(new Timestamper(), TIME_START_DELAY, TIME_REPEAT_DELAY);
}
/**
* Update the clock offset based on the average of the peers.
* This uses the default stratum which is lower than any reasonable
* NTP source, so it will be ignored unless NTP is broken.
* @since 0.7.12
*/
private class Timestamper implements SimpleTimer.TimedEvent {
public void timeReached() {
// use the same % as in RouterClock so that check will never fail
// This is their our offset w.r.t. them...
long peerOffset = getFramedAveragePeerClockSkew(50);
if (peerOffset == 0)
return;
long currentOffset = _context.clock().getOffset();
// ... so we subtract it to get in sync with them
long newOffset = currentOffset - peerOffset;
_context.clock().setOffset(newOffset);
}
}
}