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.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Vector;
import net.i2p.data.Hash;
import net.i2p.data.router.RouterAddress;
import net.i2p.data.router.RouterInfo;
import net.i2p.router.CommSystemFacade;
import net.i2p.router.OutNetMessage;
import net.i2p.router.RouterContext;
import net.i2p.router.transport.crypto.DHSessionKeyBuilder;
import net.i2p.router.transport.udp.UDPTransport;
import net.i2p.router.util.EventLog;
import net.i2p.util.Addresses;
import net.i2p.util.I2PThread;
import net.i2p.util.Log;
import net.i2p.util.SimpleTimer;
import net.i2p.util.SimpleTimer2;
import net.i2p.util.Translate;
public class CommSystemFacadeImpl extends CommSystemFacade {
private final Log _log;
private final RouterContext _context;
private final TransportManager _manager;
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;
_log = _context.logManager().getLog(CommSystemFacadeImpl.class);
//_context.statManager().createRateStat("transport.getBidsJobTime", "How long does it take?", "Transport", new long[] { 10*60*1000l });
_netMonitorStatus = true;
_geoIP = new GeoIP(_context);
_manager = new TransportManager(_context);
startGeoIP();
}
public synchronized void startup() {
_log.info("Starting up the comm system");
_manager.startListening();
startTimestamper();
startNetMonitor();
_wasStarted = true;
}
/**
* Cannot be restarted after calling this. Use restart() for that.
*/
public synchronized void shutdown() {
_manager.shutdown();
_geoIP.shutdown();
}
public synchronized void restart() {
if (!_wasStarted)
startup();
else
_manager.restart();
}
/**
* How many peers are we currently connected to, that we have
* sent a message to or received a message from in the last five minutes.
*/
@Override
public int countActivePeers() { return _manager.countActivePeers(); }
/**
* How many peers are we currently connected to, that we have
* sent a message to in the last minute.
* Unused for anything, to be removed.
*/
@Override
public int countActiveSendPeers() { return _manager.countActiveSendPeers(); }
@Override
public boolean haveInboundCapacity(int pct) { return _manager.haveInboundCapacity(pct); }
@Override
public boolean haveOutboundCapacity(int pct) { return _manager.haveOutboundCapacity(pct); }
@Override
public boolean haveHighOutboundCapacity() { return _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.
*
* A positive number means our clock is ahead of theirs.
*
* Todo: change Vectors to milliseconds
*/
@Override
public long getFramedAveragePeerClockSkew(int percentToInclude) {
Vector<Long> 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 = 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;
}
/** 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, _manager, msg);
// < 0.4 ms
//_context.statManager().addRateData("transport.getBidsJobTime", _context.clock().now() - before);
}
@Override
public boolean isBacklogged(Hash peer) {
return _manager.isBacklogged(peer);
}
@Override
public boolean isEstablished(Hash peer) {
return _manager.isEstablished(peer);
}
@Override
public boolean wasUnreachable(Hash peer) {
return _manager.wasUnreachable(peer);
}
@Override
public byte[] getIP(Hash peer) {
return _manager.getIP(peer);
}
/**
* Tell the comm system that we may disconnect from this peer.
* This is advisory only.
*
* @since 0.9.24
*/
@Override
public void mayDisconnect(Hash peer) {
_manager.mayDisconnect(peer);
}
@Override
public List<String> getMostRecentErrorMessages() {
return _manager.getMostRecentErrorMessages();
}
/**
* @since 0.9.20
*/
@Override
public Status getStatus() {
if (!_netMonitorStatus)
return Status.DISCONNECTED;
Status rv = _manager.getReachabilityStatus();
if (rv != Status.HOSED && _context.router().isHidden())
return Status.OK;
return rv;
}
/**
* @deprecated unused
*/
@Override
@Deprecated
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 List<RouterAddress> createAddresses() {
// No, don't do this, it makes it almost impossible to build inbound tunnels
//if (_context.router().isHidden())
// return Collections.EMPTY_SET;
List<RouterAddress> addresses = new ArrayList<RouterAddress>(_manager.getAddresses());
if (_log.shouldLog(Log.INFO))
_log.info("Creating addresses: " + addresses, new Exception("creator"));
return addresses;
}
/**
* UDP changed addresses, tell NTCP and restart
*
* All the work moved to NTCPTransport.externalAddressReceived()
*
* @param udpAddr may be null; or udpAddr's host/IP may be null
*/
@Override
public void notifyReplaceAddress(RouterAddress udpAddr) {
byte[] ip = null;
int port = 0;
// Don't pass IP along if address has introducers
// Right now we publish the direct UDP address, even if publishing introducers,
// we probably shouldn't, see UDPTransport rebuildExternalAddress() TODO
if (udpAddr != null && udpAddr.getOption("ihost0") == null) {
ip = udpAddr.getIP();
port = udpAddr.getPort();
}
if (port < 0) {
Transport udp = _manager.getTransport(UDPTransport.STYLE);
if (udp != null)
port = udp.getRequestedPort();
}
if (ip != null || port > 0)
_manager.externalAddressReceived(Transport.AddressSource.SOURCE_SSU, ip, port);
else
notifyRemoveAddress(false);
}
/**
* Tell other transports our address changed
*
* @param address non-null; but address's host/IP may be null
* @since 0.9.20
*/
@Override
public void notifyRemoveAddress(RouterAddress address) {
// just keep this simple for now, multiple v4 or v6 addresses not yet supported
notifyRemoveAddress(address != null &&
address.getIP() != null &&
address.getIP().length == 16);
}
/**
* Tell other transports our address changed
*
* @since 0.9.20
*/
@Override
public void notifyRemoveAddress(boolean ipv6) {
_manager.externalAddressRemoved(Transport.AddressSource.SOURCE_SSU, ipv6);
}
/**
* Pluggable transports. Not for NTCP or SSU.
*
* Do not call from transport constructor. Transport must be ready to be started.
*
* Following transport methods will be called:
* setListener()
* externalAddressReceived() (zero or more times, one for each known address)
* startListening();
*
* @since 0.9.16
*/
@Override
public void registerTransport(Transport t) {
_manager.registerAndStart(t);
}
/**
* Pluggable transports. Not for NTCP or SSU.
*
* Following transport methods will be called:
* setListener(null)
* stoptListening();
*
* @since 0.9.16
*/
@Override
public void unregisterTransport(Transport t) {
_manager.stopAndUnregister(t);
}
/**
* Hook for pluggable transport creation.
*
* @since 0.9.16
*/
@Override
public DHSessionKeyBuilder.Factory getDHFactory() {
return _manager.getDHFactory();
}
/*
* 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, banlisting, 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() {
_context.simpleTimer2().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 (Hash h : _context.netDb().getAllRouters()) {
RouterInfo ri = _context.netDb().lookupRouterInfoLocally(h);
if (ri == null)
continue;
byte[] ip = getIP(ri);
if (ip == null)
continue;
_geoIP.add(ip);
}
_context.simpleTimer2().addPeriodicEvent(new Lookup(), 5000, LOOKUP_TIME);
}
}
private class Lookup implements SimpleTimer.TimedEvent {
public void timeReached() {
(new LookupThread()).start();
}
}
/**
* This takes too long to run on the SimpleTimer2 queue
* @since 0.9.10
*/
private class LookupThread extends I2PThread {
public LookupThread() {
super("GeoIP Lookup");
setDaemon(true);
}
public void run() {
long start = System.currentTimeMillis();
_geoIP.blockingLookup();
if (_log.shouldLog(Log.INFO))
_log.info("GeoIP lookup took " + (System.currentTimeMillis() - start));
}
}
@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
*/
@Override
public boolean isInBadCountry() {
String us = getOurCountry();
return (us != null && BadCountries.contains(us)) || _context.getBooleanProperty("router.forceBadCountry");
}
/**
* Are they in a bad place
* @param peer non-null
* @since 0.9.16
*/
@Override
public boolean isInBadCountry(Hash peer) {
String c = getCountry(peer);
return c != null && BadCountries.contains(c);
}
/**
* Are they in a bad place
* @param ri non-null
* @since 0.9.16
*/
@Override
public boolean isInBadCountry(RouterInfo ri) {
byte[] ip = getIP(ri);
if (ip == null)
return false;
String c = _geoIP.get(ip);
return c != null && BadCountries.contains(c);
}
/**
* Uses the transport IP first because that lookup is fast,
* then the IP from the netDb.
*
* @param peer not ourselves - use getOurCountry() for that
* @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;
ip = getValidIP(ri);
if (ip != null)
return _geoIP.get(ip);
return null;
}
/**
* Return first IP (v4 or v6) we find, any transport.
* Not validated, may be local, etc.
*/
private static byte[] getIP(RouterInfo ri) {
for (RouterAddress ra : ri.getAddresses()) {
byte[] rv = ra.getIP();
if (rv != null)
return rv;
}
return null;
}
/**
* Return first valid IP (v4 or v6) we find, any transport.
* Local and other invalid IPs will not be returned.
*
* @since 0.9.18
*/
private static byte[] getValidIP(RouterInfo ri) {
for (RouterAddress ra : ri.getAddresses()) {
byte[] rv = ra.getIP();
if (rv != null && TransportUtil.isPubliclyRoutable(rv, true))
return rv;
}
return null;
}
/** 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";
private static final String COUNTRY_BUNDLE_NAME = "net.i2p.router.countries.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, COUNTRY_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(_t("NetDb entry")).append("\" href=\"netdb?r=").append(h).append("\">");
buf.append(h);
if (found)
buf.append("</a>");
buf.append("</tt>");
return buf.toString();
}
/**
* Is everything disabled for testing?
* @since 0.8.13
*/
@Override
public boolean isDummy() {
return _context.getBooleanProperty(PROP_DISABLED);
}
/**
* Translate
*/
private final String _t(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.simpleTimer2().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(33);
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);
}
}
/** @since 0.9.4 */
private void startNetMonitor() {
new NetMonitor();
}
/**
* Simple check to see if we have a network connection
* @since 0.9.4
*/
private class NetMonitor extends SimpleTimer2.TimedEvent {
private static final long SHORT_DELAY = 15*1000;
private static final long LONG_DELAY = 3*60*1000;
public NetMonitor() {
super(_context.simpleTimer2(), 0);
}
public void timeReached() {
boolean good = Addresses.isConnected();
if (_netMonitorStatus != good) {
_context.router().eventLog().addEvent(EventLog.NETWORK, good ? "connected" : "disconnected");
_netMonitorStatus = good;
}
reschedule(good ? LONG_DELAY : SHORT_DELAY);
}
}
}