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); } }