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.net.InetAddress;
import java.net.Inet6Address;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.Vector;
import java.util.concurrent.ConcurrentHashMap;
import net.i2p.crypto.SigType;
import net.i2p.data.Hash;
import net.i2p.data.router.RouterAddress;
import net.i2p.data.router.RouterIdentity;
import net.i2p.data.router.RouterInfo;
import net.i2p.data.i2np.I2NPMessage;
import net.i2p.router.CommSystemFacade.Status;
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;
import net.i2p.util.Addresses;
import net.i2p.util.Log;
import net.i2p.util.SystemVersion;
import net.i2p.util.Translate;
import net.i2p.util.VersionComparator;
public class TransportManager implements TransportEventListener {
private final Log _log;
/**
* Converted from List to prevent concurrent modification exceptions.
* If we want more than one transport with the same style we will have to change this.
*/
private final Map<String, Transport> _transports;
/** locking: this */
private final Map<String, Transport> _pluggableTransports;
private final RouterContext _context;
private final UPnPManager _upnpManager;
private final DHSessionKeyBuilder.PrecalcRunner _dhThread;
/** default true */
public final static String PROP_ENABLE_UDP = "i2np.udp.enable";
/** default true */
public final static String PROP_ENABLE_NTCP = "i2np.ntcp.enable";
/** default true */
public final static String PROP_ENABLE_UPNP = "i2np.upnp.enable";
private static final String PROP_ADVANCED = "routerconsole.advanced";
/** not forever, since they may update */
private static final long SIGTYPE_BANLIST_DURATION = 36*60*60*1000L;
public TransportManager(RouterContext context) {
_context = context;
_log = _context.logManager().getLog(TransportManager.class);
_context.statManager().createRateStat("transport.banlistOnUnreachable", "Add a peer to the banlist since none of the transports can reach them", "Transport", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
_context.statManager().createRateStat("transport.banlistOnUsupportedSigType", "Add a peer to the banlist since signature type is unsupported", "Transport", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
_context.statManager().createRateStat("transport.noBidsYetNotAllUnreachable", "Add a peer to the banlist since none of the transports can reach them", "Transport", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
_context.statManager().createRateStat("transport.bidFailBanlisted", "Could not attempt to bid on message, as they were banlisted", "Transport", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
_context.statManager().createRateStat("transport.bidFailSelf", "Could not attempt to bid on message, as it targeted ourselves", "Transport", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
_context.statManager().createRateStat("transport.bidFailNoTransports", "Could not attempt to bid on message, as none of the transports could attempt it", "Transport", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
_context.statManager().createRateStat("transport.bidFailAllTransports", "Could not attempt to bid on message, as all of the transports had failed", "Transport", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
_transports = new ConcurrentHashMap<String, Transport>(2);
_pluggableTransports = new HashMap<String, Transport>(2);
if (_context.getBooleanPropertyDefaultTrue(PROP_ENABLE_UPNP))
_upnpManager = new UPnPManager(context, this);
else
_upnpManager = null;
_dhThread = new DHSessionKeyBuilder.PrecalcRunner(context);
}
/**
* Pluggable transports. Not for NTCP or SSU.
*
* @since 0.9.16
*/
synchronized void registerAndStart(Transport t) {
String style = t.getStyle();
if (style.equals(NTCPTransport.STYLE) || style.equals(UDPTransport.STYLE))
throw new IllegalArgumentException("Builtin transport");
if (_transports.containsKey(style) || _pluggableTransports.containsKey(style))
throw new IllegalStateException("Dup transport");
boolean shouldStart = !_transports.isEmpty();
_pluggableTransports.put(style, t);
addTransport(t);
t.setListener(this);
if (shouldStart) {
initializeAddress(t);
t.startListening();
_context.router().rebuildRouterInfo();
} // else will be started by configTransports() (unlikely)
}
/**
* Pluggable transports. Not for NTCP or SSU.
*
* @since 0.9.16
*/
synchronized void stopAndUnregister(Transport t) {
String style = t.getStyle();
if (style.equals(NTCPTransport.STYLE) || style.equals(UDPTransport.STYLE))
throw new IllegalArgumentException("Builtin transport");
t.setListener(null);
_pluggableTransports.remove(style);
removeTransport(t);
t.stopListening();
_context.router().rebuildRouterInfo();
}
/**
* Hook for pluggable transport creation.
*
* @since 0.9.16
*/
DHSessionKeyBuilder.Factory getDHFactory() {
return _dhThread;
}
private void addTransport(Transport transport) {
if (transport == null) return;
Transport old = _transports.put(transport.getStyle(), transport);
if (old != null && old != transport && _log.shouldLog(Log.WARN))
_log.warn("Replacing transport " + transport.getStyle());
transport.setListener(this);
}
private void removeTransport(Transport transport) {
if (transport == null) return;
Transport old = _transports.remove(transport.getStyle());
if (old != null && _log.shouldLog(Log.WARN))
_log.warn("Removing transport " + transport.getStyle());
transport.setListener(null);
}
private void configTransports() {
boolean enableUDP = _context.getBooleanPropertyDefaultTrue(PROP_ENABLE_UDP);
Transport udp = null;
if (enableUDP) {
udp = new UDPTransport(_context, _dhThread);
addTransport(udp);
initializeAddress(udp);
}
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, (byte[]) null, port);
}
}
if (_transports.isEmpty())
_log.log(Log.CRIT, "No transports are enabled");
}
public static boolean isNTCPEnabled(RouterContext ctx) {
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) {
Set<String> ipset = Addresses.getAddresses(false, true); // non-local, include IPv6
//
// Avoid IPv6 temporary addresses if we have a non-temporary one
//
boolean hasNonTempV6Address = false;
List<InetAddress> addresses = new ArrayList<InetAddress>(4);
List<Inet6Address> tempV6Addresses = new ArrayList<Inet6Address>(4);
for (String ips : ipset) {
try {
InetAddress addr = InetAddress.getByName(ips);
if (ips.contains(":") && (addr instanceof Inet6Address)) {
Inet6Address v6addr = (Inet6Address) addr;
// getAddresses(false, true) will not return deprecated addresses
//if (Addresses.isDeprecated(v6addr)) {
// if (_log.shouldWarn())
// _log.warn("Not binding to deprecated temporary address " + bt);
// continue;
//}
if (Addresses.isTemporary(v6addr)) {
// Save temporary addresses
// we only use these if we don't have a non-temporary adress
tempV6Addresses.add(v6addr);
continue;
}
hasNonTempV6Address = true;
}
addresses.add(addr);
} catch (UnknownHostException e) {
_log.error("UDP failed to bind to local address", e);
}
}
// we only use these if we don't have a non-temporary adress
if (!tempV6Addresses.isEmpty()) {
if (hasNonTempV6Address) {
if (_log.shouldWarn()) {
for (Inet6Address addr : tempV6Addresses) {
_log.warn("Not binding to temporary address " + addr.getHostAddress());
}
}
} else {
addresses.addAll(tempV6Addresses);
}
}
for (InetAddress ia : addresses) {
byte[] ip = ia.getAddress();
t.externalAddressReceived(SOURCE_INTERFACE, ip, 0);
}
}
/**
* Initialize from interfaces, and callback from UPnP or SSU.
* See CSFI.notifyReplaceAddress().
* Tell all transports... but don't loop.
*
*/
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);
}
}
/**
* Remove all ipv4 or ipv6 addresses.
* See CSFI.notifyRemoveAddress().
* Tell all transports... but don't loop.
*
* @since 0.9.20
*/
public void externalAddressRemoved(Transport.AddressSource source, boolean ipv6) {
for (Transport t : _transports.values()) {
// don't loop
if (!(source == SOURCE_SSU && t.getStyle().equals(UDPTransport.STYLE)))
t.externalAddressRemoved(source, ipv6);
}
}
/**
* callback from UPnP
*
*/
public void forwardPortStatus(String style, byte[] ip, int port, int externalPort, boolean success, String reason) {
Transport t = getTransport(style);
if (t != null)
t.forwardPortStatus(ip, port, externalPort, success, reason);
}
public synchronized void startListening() {
if (_dhThread.getState() == Thread.State.NEW)
_dhThread.start();
// For now, only start UPnP if we have no publicly-routable addresses
// so we don't open the listener ports to the world.
// Maybe we need a config option to force on? Probably not.
// What firewall supports UPnP and is configured with a public address on the LAN side?
// Unlikely.
if (_upnpManager != null && Addresses.getAnyAddress() == null)
_upnpManager.start();
configTransports();
_log.debug("Starting up the transport manager");
// Let's do this in a predictable order to make testing easier
// Start NTCP first so it can get notified from SSU
List<Transport> tps = new ArrayList<Transport>();
Transport tp = getTransport(NTCPTransport.STYLE);
if (tp != null)
tps.add(tp);
tp = getTransport(UDPTransport.STYLE);
if (tp != null)
tps.add(tp);
// now add any others (pluggable)
for (Transport t : _pluggableTransports.values()) {
tps.add(t);
}
for (Transport t : tps) {
t.startListening();
if (_log.shouldLog(Log.DEBUG))
_log.debug("Transport " + t.getStyle() + " started");
}
// kick UPnP - Do this to get the ports opened even before UDP registers an address
transportAddressChanged();
_log.debug("Done start listening on transports");
_context.router().rebuildRouterInfo();
}
public synchronized void restart() {
stopListening();
try { Thread.sleep(5*1000); } catch (InterruptedException ie) {}
startListening();
}
/**
* Can be restarted.
*/
public synchronized void stopListening() {
if (_upnpManager != null)
_upnpManager.stop();
for (Transport t : _transports.values()) {
t.stopListening();
}
_transports.clear();
}
/**
* Cannot be restarted.
* @since 0.9
*/
public synchronized void shutdown() {
stopListening();
_dhThread.shutdown();
Addresses.clearCaches();
TransportImpl.clearCaches();
}
public Transport getTransport(String style) {
return _transports.get(style);
}
int getTransportCount() { return _transports.size(); }
/**
* 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.
*/
public int countActivePeers() {
int peers = 0;
for (Transport t : _transports.values()) {
peers += t.countActivePeers();
}
return peers;
}
/**
* 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.
*/
public int countActiveSendPeers() {
int peers = 0;
for (Transport t : _transports.values()) {
peers += t.countActiveSendPeers();
}
return peers;
}
/**
* Is at least one transport below its outbound connection limit + some margin
* Use for throttling in the router.
*
* @param pct percent of limit 0-100
*/
public boolean haveOutboundCapacity(int pct) {
for (Transport t : _transports.values()) {
if (t.haveCapacity(pct))
return true;
}
return false;
}
private static final int HIGH_CAPACITY_PCT = 50;
/**
* Are all transports well below their outbound connection limit
* Use for throttling in the router.
*/
public boolean haveHighOutboundCapacity() {
if (_transports.isEmpty())
return false;
for (Transport t : _transports.values()) {
if (!t.haveCapacity(HIGH_CAPACITY_PCT))
return false;
}
return true;
}
/**
* Is at least one transport below its inbound connection limit + some margin
* Use for throttling in the router.
*
* @param pct percent of limit 0-100
*/
public boolean haveInboundCapacity(int pct) {
for (Transport t : _transports.values()) {
if (t.hasCurrentAddress() && t.haveCapacity(pct))
return true;
}
return false;
}
/**
* Return our peer clock skews on all transports.
* Vector composed of Long, each element representing a peer skew in seconds.
* A positive number means our clock is ahead of theirs.
* Note: this method returns them in whimsical order.
*/
public Vector<Long> getClockSkews() {
Vector<Long> skews = new Vector<Long>();
for (Transport t : _transports.values()) {
Vector<Long> tempSkews = t.getClockSkews();
if ((tempSkews == null) || (tempSkews.isEmpty())) continue;
skews.addAll(tempSkews);
}
//if (_log.shouldLog(Log.DEBUG))
// _log.debug("Transport manager returning " + skews.size() + " peer clock skews.");
return skews;
}
/**
* Previously returned short, now enum as of 0.9.20
* @return the best status of any transport
*/
public Status getReachabilityStatus() {
Status rv = Status.UNKNOWN;
for (Transport t : _transports.values()) {
Status s = t.getReachabilityStatus();
if (s.getCode() < rv.getCode())
rv = s;
}
return rv;
}
/**
* @deprecated unused
*/
@Deprecated
public void recheckReachability() {
for (Transport t : _transports.values())
t.recheckReachability();
}
public boolean isBacklogged(Hash peer) {
for (Transport t : _transports.values()) {
if (t.isBacklogged(peer))
return true;
}
return false;
}
public boolean isEstablished(Hash peer) {
for (Transport t : _transports.values()) {
if (t.isEstablished(peer))
return true;
}
return false;
}
/**
* Tell the transports that we may disconnect from this peer.
* This is advisory only.
*
* @since 0.9.24
*/
public void mayDisconnect(Hash peer) {
for (Transport t : _transports.values()) {
t.mayDisconnect(peer);
}
}
/**
* Was the peer UNreachable (outbound only) on any transport,
* based on the last time we tried it for each transport?
* This is NOT reset if the peer contacts us.
*/
public boolean wasUnreachable(Hash peer) {
for (Transport t : _transports.values()) {
if (!t.wasUnreachable(peer))
return false;
}
return true;
}
/**
* IP of the peer from the last connection (in or out, any transport).
* This may be different from that advertised in the netDb,
* as the peer may be hidden, or connect from a different IP, or
* change his netDb later, in an attempt to avoid restrictions.
*
* 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 peer) {
return TransportImpl.getIP(peer);
}
/**
* This forces a rebuild
*/
public List<RouterAddress> getAddresses() {
List<RouterAddress> rv = new ArrayList<RouterAddress>(4);
// do this first since SSU may force a NTCP change
for (Transport t : _transports.values())
t.updateAddress();
for (Transport t : _transports.values()) {
rv.addAll(t.getCurrentAddresses());
}
return rv;
}
/**
* @since IPv6
*/
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<Port> getPorts() {
Set<Port> rv = new HashSet<Port>(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(NTCPTransport.PROP_I2NP_NTCP_AUTO_PORT)) {
Transport udp = getTransport(UDPTransport.STYLE);
if (udp != null)
port = t.getRequestedPort();
}
if (port > 0)
rv.add(new Port(t.getStyle(), port));
}
return rv;
}
public TransportBid getBid(OutNetMessage msg) {
List<TransportBid> bids = getBids(msg);
if ( (bids == null) || (bids.isEmpty()) )
return null;
else
return bids.get(0);
}
public List<TransportBid> getBids(OutNetMessage msg) {
if (msg == null)
throw new IllegalArgumentException("Null message? no bidding on a null outNetMessage!");
if (_context.router().getRouterInfo().equals(msg.getTarget()))
throw new IllegalArgumentException("Bids for a message bound to ourselves?");
List<TransportBid> rv = new ArrayList<TransportBid>(_transports.size());
Set<String> failedTransports = msg.getFailedTransports();
for (Transport t : _transports.values()) {
if (failedTransports.contains(t.getStyle())) {
if (_log.shouldLog(Log.DEBUG))
_log.debug("Skipping transport " + t.getStyle() + " as it already failed");
continue;
}
// we always want to try all transports, in case there is a faster bidirectional one
// already connected (e.g. peer only has a public PHTTP address, but they've connected
// to us via TCP, send via TCP)
TransportBid bid = t.bid(msg.getTarget(), msg.getMessageSize());
if (bid != null) {
if (_log.shouldLog(Log.DEBUG))
_log.debug("Transport " + t.getStyle() + " bid: " + bid);
rv.add(bid);
} else {
if (_log.shouldLog(Log.DEBUG))
_log.debug("Transport " + t.getStyle() + " did not produce a bid");
}
}
return rv;
}
public TransportBid getNextBid(OutNetMessage msg) {
int unreachableTransports = 0;
Hash peer = msg.getTarget().getIdentity().calculateHash();
Set<String> failedTransports = msg.getFailedTransports();
TransportBid rv = null;
for (Transport t : _transports.values()) {
if (t.isUnreachable(peer)) {
unreachableTransports++;
// this keeps GetBids() from banlisting for "no common transports"
// right after we banlisted for "unreachable on any transport" below...
msg.transportFailed(t.getStyle());
continue;
}
if (failedTransports.contains(t.getStyle())) {
if (_log.shouldLog(Log.DEBUG))
_log.debug("Skipping transport " + t.getStyle() + " as it already failed");
continue;
}
// we always want to try all transports, in case there is a faster bidirectional one
// already connected (e.g. peer only has a public PHTTP address, but they've connected
// to us via TCP, send via TCP)
TransportBid bid = t.bid(msg.getTarget(), msg.getMessageSize());
if (bid != null) {
if (bid.getLatencyMs() == TransportBid.TRANSIENT_FAIL)
// this keeps GetBids() from banlisting for "no common transports"
msg.transportFailed(t.getStyle());
else if ( (rv == null) || (rv.getLatencyMs() > bid.getLatencyMs()) )
rv = bid;
if (_log.shouldLog(Log.DEBUG))
_log.debug("Transport " + t.getStyle() + " bid: " + bid + " currently winning? " + (rv == bid)
+ " (winning latency: " + rv.getLatencyMs() + " / " + rv + ")");
} else {
if (_log.shouldLog(Log.DEBUG))
_log.debug("Transport " + t.getStyle() + " did not produce a bid");
if (t.isUnreachable(peer))
unreachableTransports++;
}
}
if (unreachableTransports >= _transports.size()) {
if (msg.getTarget().getIdentity().getSigningPublicKey().getType() == null) {
// we don't support his crypto
_context.statManager().addRateData("transport.banlistOnUnsupportedSigType", 1);
_context.banlist().banlistRouterForever(peer, _x("Unsupported signature type"));
} else if (unreachableTransports >= _transports.size() && countActivePeers() > 0) {
// Don't banlist if we aren't talking to anybody, as we may have a network connection issue
boolean incompat = false;
RouterInfo us = _context.router().getRouterInfo();
if (us != null) {
RouterIdentity id = us.getIdentity();
if (id.getSigType() != SigType.DSA_SHA1) {
String v = msg.getTarget().getVersion();
// NTCP is earlier than SSU, use that one
if (VersionComparator.comp(v, NTCPTransport.MIN_SIGTYPE_VERSION) < 0)
incompat = true;
}
}
if (incompat) {
// they don't support our crypto
_context.statManager().addRateData("transport.banlistOnUnsupportedSigType", 1);
_context.banlist().banlistRouter(peer, _x("No support for our signature type"), null, null,
_context.clock().now() + SIGTYPE_BANLIST_DURATION);
} else {
_context.statManager().addRateData("transport.banlistOnUnreachable", msg.getLifetime(), msg.getLifetime());
_context.banlist().banlistRouter(peer, _x("Unreachable on any transport"));
}
}
} else if (rv == null) {
_context.statManager().addRateData("transport.noBidsYetNotAllUnreachable", unreachableTransports, msg.getLifetime());
}
return rv;
}
/**
* Message received
*
* @param message non-null
* @param fromRouter may be null
* @param fromRouterHash may be null, calculated from fromRouter if null
*/
public void messageReceived(I2NPMessage message, RouterIdentity fromRouter, Hash fromRouterHash) {
if (_log.shouldLog(Log.DEBUG))
_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");
} catch (IllegalArgumentException iae) {
if (_log.shouldLog(Log.WARN))
_log.warn("Error receiving message", iae);
}
}
public void transportAddressChanged() {
if (_upnpManager != null) {
_upnpManager.rescan();
// should really delay the following by 5 seconds?
_upnpManager.update(getPorts());
}
}
public List<String> getMostRecentErrorMessages() {
List<String> rv = new ArrayList<String>(16);
for (Transport t : _transports.values()) {
rv.addAll(t.getMostRecentErrorMessages());
}
return rv;
}
/**
* Warning - blocking, very slow, queries the active UPnP router,
* will take many seconds if it has vanished.
*/
public void renderStatusHTML(Writer out, String urlBase, int sortFlags) throws IOException {
if (_context.getBooleanProperty(PROP_ADVANCED)) {
out.write("<p><b>");
out.write(_t("Status"));
out.write(": ");
out.write(_t(getReachabilityStatus().toStatusString()));
out.write("</b></p>");
}
TreeMap<String, Transport> transports = new TreeMap<String, Transport>();
for (Transport t : _transports.values()) {
transports.put(t.getStyle(), t);
}
for (Transport t : transports.values()) {
t.renderStatusHTML(out, urlBase, sortFlags);
}
if (!_transports.isEmpty()) {
out.write(getTransportsLegend());
}
StringBuilder buf = new StringBuilder(4*1024);
buf.append("<h3>").append(_t("Router Transport Addresses")).append("</h3><pre>\n");
for (Transport t : _transports.values()) {
if (t.hasCurrentAddress()) {
for (RouterAddress ra : t.getCurrentAddresses()) {
buf.append(ra.toString());
buf.append("\n\n");
}
} else {
buf.append(_t("{0} is used for outbound connections only", t.getStyle()));
buf.append("\n\n");
}
}
buf.append("</pre>\n");
out.write(buf.toString());
if (SystemVersion.isAndroid()) {
// newer androids crash w/ network on IO thread
} else if (_upnpManager != null) {
out.write(_upnpManager.renderStatusHTML());
} else {
out.write("<h3><a name=\"upnp\"></a>" + _t("UPnP is not enabled") + "</h3>\n");
}
out.write("</p>\n");
out.flush();
}
private final String getTransportsLegend() {
StringBuilder buf = new StringBuilder(1024);
buf.append("<h3 id=\"help\">").append(_t("Help")).append("</h3><div class=\"configure\"><p>")
.append(_t("Your transport connection limits are automatically set based on your configured bandwidth."))
.append('\n')
.append(_t("To override these limits, add the settings i2np.ntcp.maxConnections=nnn and i2np.udp.maxConnections=nnn on the advanced configuration page."))
.append("</p></div>\n");
buf.append("<h3>").append(_t("Definitions")).append("</h3><div class=\"configure\">" +
"<p><b id=\"def.peer\">").append(_t("Peer")).append("</b>: ").append(_t("The remote peer, identified by router hash")).append("<br>\n" +
"<b id=\"def.dir\">").append(_t("Dir")).append("</b>: " +
"<img alt=\"Inbound\" src=\"/themes/console/images/inbound.png\"> ").append(_t("Inbound connection")).append("<br>\n" +
" " +
"<img alt=\"Outbound\" src=\"/themes/console/images/outbound.png\"> ").append(_t("Outbound connection")).append("<br>\n" +
" " +
"<img src=\"/themes/console/images/inbound.png\" alt=\"V\" height=\"8\" width=\"12\"> ").append(_t("They offered to introduce us (help other peers traverse our firewall)")).append("<br>\n" +
" " +
"<img src=\"/themes/console/images/outbound.png\" alt=\"^\" height=\"8\" width=\"12\"> ").append(_t("We offered to introduce them (help other peers traverse their firewall)")).append("<br>\n" +
"<b id=\"def.idle\">").append(_t("Idle")).append("</b>: ").append(_t("How long since a packet has been received / sent")).append("<br>\n" +
"<b id=\"def.rate\">").append(_t("In/Out")).append("</b>: ").append(_t("The smoothed inbound / outbound transfer rate (KBytes per second)")).append("<br>\n" +
"<b id=\"def.up\">").append(_t("Up")).append("</b>: ").append(_t("How long ago this connection was established")).append("<br>\n" +
"<b id=\"def.skew\">").append(_t("Skew")).append("</b>: ").append(_t("The difference between the peer's clock and your own")).append("<br>\n" +
"<b id=\"def.cwnd\">CWND</b>: ").append(_t("The congestion window, which is how many bytes can be sent without an acknowledgement")).append(" / <br>\n" +
" ").append(_t("The number of sent messages awaiting acknowledgement")).append(" /<br>\n" +
" ").append(_t("The maximum number of concurrent messages to send")).append(" /<br>\n"+
" ").append(_t("The number of pending sends which exceed congestion window")).append("<br>\n" +
"<b id=\"def.ssthresh\">SST</b>: ").append(_t("The slow start threshold")).append("<br>\n" +
"<b id=\"def.rtt\">RTT</b>: ").append(_t("The round trip time in milliseconds")).append("<br>\n" +
//"<b id=\"def.dev\">").append(_t("Dev")).append("</b>: ").append(_t("The standard deviation of the round trip time in milliseconds")).append("<br>\n" +
"<b id=\"def.rto\">RTO</b>: ").append(_t("The retransmit timeout in milliseconds")).append("<br>\n" +
"<b id=\"def.mtu\">MTU</b>: ").append(_t("Current maximum send packet size / estimated maximum receive packet size (bytes)")).append("<br>\n" +
"<b id=\"def.send\">").append(_t("TX")).append("</b>: ").append(_t("The total number of messages sent to the peer")).append("<br>\n" +
"<b id=\"def.recv\">").append(_t("RX")).append("</b>: ").append(_t("The total number of messages received from the peer")).append("<br>\n" +
"<b id=\"def.resent\">").append(_t("Dup TX")).append("</b>: ").append(_t("The total number of packets retransmitted to the peer")).append("<br>\n" +
"<b id=\"def.dupRecv\">").append(_t("Dup RX")).append("</b>: ").append(_t("The total number of duplicate packets received from the peer")).append("</p>" +
"</div>\n");
return buf.toString();
}
/**
* Mark a string for extraction by xgettext and translation.
* Use this only in static initializers.
* It does not translate!
* @return s
*/
private static final String _x(String s) {
return s;
}
private static final String BUNDLE_NAME = "net.i2p.router.web.messages";
/**
* Translate
*/
private final String _t(String s) {
return Translate.getString(s, _context, BUNDLE_NAME);
}
/**
* Translate
*/
private final String _t(String s, Object o) {
return Translate.getString(s, o, _context, BUNDLE_NAME);
}
}