use of net.i2p.router.tunnel.HopConfig in project i2p.i2p by i2p.
the class BuildHandler method handleReq.
/**
* If we are dropping lots of requests before even trying to handle them,
* I suppose you could call us "overloaded"
*/
/**
** unused, see handleReq() below
* private final static int MAX_PROACTIVE_DROPS = 240;
*
* private int countProactiveDrops() {
* int dropped = 0;
* dropped += countEvents("tunnel.dropLoadProactive", 60*1000);
* dropped += countEvents("tunnel.dropLoad", 60*1000);
* dropped += countEvents("tunnel.dropLoadBacklog", 60*1000);
* dropped += countEvents("tunnel.dropLoadDelay", 60*1000);
* return dropped;
* }
*
* private int countEvents(String stat, long period) {
* RateStat rs = _context.statManager().getRate(stat);
* if (rs != null) {
* Rate r = rs.getRate(period);
* if (r != null)
* return (int)r.getCurrentEventCount();
* }
* return 0;
* }
***
*/
/**
* Actually process the request and send the reply.
*
* Todo: Replies are not subject to RED for bandwidth reasons,
* and the bandwidth is not credited to any tunnel.
* If we did credit the reply to the tunnel, it would
* prevent the classification of the tunnel as 'inactive' on tunnels.jsp.
*/
private void handleReq(RouterInfo nextPeerInfo, BuildMessageState state, BuildRequestRecord req, Hash nextPeer) {
long ourId = req.readReceiveTunnelId();
long nextId = req.readNextTunnelId();
boolean isInGW = req.readIsInboundGateway();
boolean isOutEnd = req.readIsOutboundEndpoint();
Hash from = state.fromHash;
if (from == null && state.from != null)
from = state.from.calculateHash();
if (isInGW && isOutEnd) {
_context.statManager().addRateData("tunnel.rejectHostile", 1);
_log.error("Dropping build request, IBGW+OBEP: " + req);
if (from != null)
_context.commSystem().mayDisconnect(from);
return;
}
if (ourId <= 0 || ourId > TunnelId.MAX_ID_VALUE || nextId <= 0 || nextId > TunnelId.MAX_ID_VALUE) {
_context.statManager().addRateData("tunnel.rejectHostile", 1);
if (_log.shouldWarn())
_log.warn("Dropping build request, bad tunnel ID: " + req);
if (from != null)
_context.commSystem().mayDisconnect(from);
return;
}
// Loop checks
if ((!isOutEnd) && _context.routerHash().equals(nextPeer)) {
_context.statManager().addRateData("tunnel.rejectHostile", 1);
// old i2pd
if (_log.shouldWarn())
_log.warn("Dropping build request, we are the next hop: " + req);
if (from != null)
_context.commSystem().mayDisconnect(from);
return;
}
if (!isInGW) {
// but if not, something is seriously wrong here.
if (from == null || _context.routerHash().equals(from)) {
_context.statManager().addRateData("tunnel.rejectHostile", 1);
if (_log.shouldWarn())
_log.warn("Dropping build request, we are the previous hop: " + req);
return;
}
}
if ((!isOutEnd) && (!isInGW)) {
// A-B-C-A is not preventable
if (nextPeer.equals(from)) {
// i2pd does this
_context.statManager().addRateData("tunnel.rejectHostile", 1);
if (_log.shouldLog(Log.WARN))
_log.warn("Dropping build request with the same previous and next hop: " + req);
_context.commSystem().mayDisconnect(from);
return;
}
}
// time is in hours, rounded down.
// tunnel-alt-creation.html specifies that this is enforced +/- 1 hour but it was not.
// As of 0.9.16, allow + 5 minutes to - 65 minutes.
long time = req.readRequestTime();
long now = (_context.clock().now() / (60l * 60l * 1000l)) * (60 * 60 * 1000);
long timeDiff = now - time;
if (timeDiff > MAX_REQUEST_AGE) {
_context.statManager().addRateData("tunnel.rejectTooOld", 1);
if (_log.shouldLog(Log.WARN))
_log.warn("Dropping build request too old... replay attack? " + DataHelper.formatDuration(timeDiff) + ": " + req);
if (from != null)
_context.commSystem().mayDisconnect(from);
return;
}
if (timeDiff < 0 - MAX_REQUEST_FUTURE) {
_context.statManager().addRateData("tunnel.rejectFuture", 1);
if (_log.shouldLog(Log.WARN))
_log.warn("Dropping build request too far in future " + DataHelper.formatDuration(0 - timeDiff) + ": " + req);
if (from != null)
_context.commSystem().mayDisconnect(from);
return;
}
int response;
if (_context.router().isHidden()) {
_context.throttle().setTunnelStatus(_x("Rejecting tunnels: Hidden mode"));
response = TunnelHistory.TUNNEL_REJECT_BANDWIDTH;
} else {
response = _context.throttle().acceptTunnelRequest();
}
// This only checked OUR tunnels, so the log message was wrong.
// Now checked by TunnelDispatcher.joinXXX()
// and returned as success value, checked below.
// if (_context.tunnelManager().getTunnelInfo(new TunnelId(ourId)) != null) {
// if (_log.shouldLog(Log.ERROR))
// _log.error("Already participating in a tunnel with the given Id (" + ourId + "), so gotta reject");
// if (response == 0)
// response = TunnelHistory.TUNNEL_REJECT_PROBABALISTIC_REJECT;
// }
// if ( (response == 0) && (_context.random().nextInt(50) <= 1) )
// response = TunnelHistory.TUNNEL_REJECT_PROBABALISTIC_REJECT;
long recvDelay = _context.clock().now() - state.recvTime;
if (response == 0) {
// unused
// int proactiveDrops = countProactiveDrops();
float pDrop = ((float) recvDelay) / (float) (BuildRequestor.REQUEST_TIMEOUT * 3);
pDrop = (float) Math.pow(pDrop, 16);
if (_context.random().nextFloat() < pDrop) {
// || (proactiveDrops > MAX_PROACTIVE_DROPS) ) ) {
_context.statManager().addRateData("tunnel.rejectOverloaded", recvDelay);
_context.throttle().setTunnelStatus(_x("Rejecting tunnels: Request overload"));
// if (true || (proactiveDrops < MAX_PROACTIVE_DROPS*2))
response = TunnelHistory.TUNNEL_REJECT_TRANSIENT_OVERLOAD;
// else
// response = TunnelHistory.TUNNEL_REJECT_BANDWIDTH;
} else {
_context.statManager().addRateData("tunnel.acceptLoad", recvDelay);
}
}
/*
* Being a IBGW or OBEP generally leads to more connections, so if we are
* approaching our connection limit (i.e. !haveCapacity()),
* reject this request.
*
* Don't do this for class N or O, under the assumption that they are already talking
* to most of the routers, so there's no reason to reject. This may drive them
* to their conn. limits, but it's hopefully a temporary solution to the
* tunnel build congestion. As the net grows this will have to be revisited.
*/
RouterInfo ri = _context.router().getRouterInfo();
if (response == 0) {
if (ri == null) {
// ?? We should always have a RI
response = TunnelHistory.TUNNEL_REJECT_BANDWIDTH;
} else {
char bw = ri.getBandwidthTier().charAt(0);
if (bw != 'O' && bw != 'N' && bw != 'P' && bw != 'X' && ((isInGW && !_context.commSystem().haveInboundCapacity(87)) || (isOutEnd && !_context.commSystem().haveOutboundCapacity(87)))) {
_context.statManager().addRateData("tunnel.rejectConnLimits", 1);
_context.throttle().setTunnelStatus(_x("Rejecting tunnels: Connection limit"));
response = TunnelHistory.TUNNEL_REJECT_BANDWIDTH;
}
}
}
// We may need another counter above for requests.
if (response == 0 && !isInGW) {
if (from != null && _throttler.shouldThrottle(from)) {
if (_log.shouldLog(Log.WARN))
_log.warn("Rejecting tunnel (hop throttle), previous hop: " + from + ": " + req);
// no setTunnelStatus() indication
_context.statManager().addRateData("tunnel.rejectHopThrottle", 1);
response = TunnelHistory.TUNNEL_REJECT_BANDWIDTH;
}
}
if (response == 0 && (!isOutEnd) && _throttler.shouldThrottle(nextPeer)) {
if (_log.shouldLog(Log.WARN))
_log.warn("Rejecting tunnel (hop throttle), next hop: " + req);
_context.statManager().addRateData("tunnel.rejectHopThrottle", 1);
// no setTunnelStatus() indication
response = TunnelHistory.TUNNEL_REJECT_BANDWIDTH;
}
HopConfig cfg = null;
if (response == 0) {
cfg = new HopConfig();
cfg.setCreation(_context.clock().now());
cfg.setExpiration(_context.clock().now() + 10 * 60 * 1000);
cfg.setIVKey(req.readIVKey());
cfg.setLayerKey(req.readLayerKey());
if (isInGW) {
// default
// cfg.setReceiveFrom(null);
} else {
if (from != null) {
cfg.setReceiveFrom(from);
} else {
// b0rk
return;
}
}
cfg.setReceiveTunnelId(DataHelper.toLong(4, ourId));
if (isOutEnd) {
// default
// cfg.setSendTo(null);
// cfg.setSendTunnelId(null);
} else {
cfg.setSendTo(nextPeer);
cfg.setSendTunnelId(DataHelper.toLong(4, nextId));
}
// now "actually" join
boolean success;
if (isOutEnd)
success = _context.tunnelDispatcher().joinOutboundEndpoint(cfg);
else if (isInGW)
success = _context.tunnelDispatcher().joinInboundGateway(cfg);
else
success = _context.tunnelDispatcher().joinParticipant(cfg);
if (success) {
if (_log.shouldLog(Log.DEBUG))
_log.debug("Joining: " + req);
} else {
// Dup Tunnel ID. This can definitely happen (birthday paradox).
// Probability in 11 minutes (per hop type):
// 0.1% for 2900 tunnels; 1% for 9300 tunnels
response = TunnelHistory.TUNNEL_REJECT_BANDWIDTH;
_context.statManager().addRateData("tunnel.rejectDupID", 1);
if (_log.shouldLog(Log.WARN))
_log.warn("DUP ID failure: " + req);
}
}
if (response != 0) {
_context.statManager().addRateData("tunnel.reject." + response, 1);
_context.messageHistory().tunnelRejected(from, new TunnelId(ourId), nextPeer, // (isOutEnd ? "outbound endpoint" : isInGW ? "inbound gw" : "participant"));
Integer.toString(response));
if (from != null)
_context.commSystem().mayDisconnect(from);
// 81% = between 75% control measures in Transports and 87% rejection above
if ((!_context.routerHash().equals(nextPeer)) && (!_context.commSystem().haveOutboundCapacity(81)) && (!_context.commSystem().isEstablished(nextPeer))) {
_context.statManager().addRateData("tunnel.dropConnLimits", 1);
if (_log.shouldLog(Log.WARN))
_log.warn("Not sending rejection due to conn limits: " + req);
return;
}
} else if (isInGW && from != null) {
// we're the start of the tunnel, no use staying connected
_context.commSystem().mayDisconnect(from);
}
if (_log.shouldLog(Log.DEBUG))
_log.debug("Responding to " + state.msg.getUniqueId() + " after " + recvDelay + " with " + response + " from " + (from != null ? from : "tunnel") + ": " + req);
EncryptedBuildRecord reply = BuildResponseRecord.create(_context, response, req.readReplyKey(), req.readReplyIV(), state.msg.getUniqueId());
int records = state.msg.getRecordCount();
int ourSlot = -1;
for (int j = 0; j < records; j++) {
if (state.msg.getRecord(j) == null) {
ourSlot = j;
state.msg.setRecord(j, reply);
// + ": " + Base64.encode(reply));
break;
}
}
if (_log.shouldLog(Log.DEBUG))
_log.debug("Read slot " + ourSlot + " containing: " + req + " accepted? " + response + " recvDelay " + recvDelay + " replyMessage " + req.readReplyMessageId());
// now actually send the response
long expires = _context.clock().now() + NEXT_HOP_SEND_TIMEOUT;
if (!isOutEnd) {
state.msg.setUniqueId(req.readReplyMessageId());
state.msg.setMessageExpiration(expires);
OutNetMessage msg = new OutNetMessage(_context, state.msg, expires, PRIORITY, nextPeerInfo);
if (response == 0)
msg.setOnFailedSendJob(new TunnelBuildNextHopFailJob(_context, cfg));
_context.outNetMessagePool().add(msg);
} else {
// We are the OBEP.
// send it to the reply tunnel on the reply peer within a new TunnelBuildReplyMessage
// (enough layers jrandom?)
TunnelBuildReplyMessage replyMsg;
if (records == TunnelBuildMessage.MAX_RECORD_COUNT)
replyMsg = new TunnelBuildReplyMessage(_context);
else
replyMsg = new VariableTunnelBuildReplyMessage(_context, records);
for (int i = 0; i < records; i++) replyMsg.setRecord(i, state.msg.getRecord(i));
replyMsg.setUniqueId(req.readReplyMessageId());
replyMsg.setMessageExpiration(expires);
TunnelGatewayMessage m = new TunnelGatewayMessage(_context);
m.setMessage(replyMsg);
m.setMessageExpiration(expires);
m.setTunnelId(new TunnelId(nextId));
if (_context.routerHash().equals(nextPeer)) {
// ok, we are the gateway, so inject it
if (_log.shouldLog(Log.DEBUG))
_log.debug("We are the reply gateway for " + nextId + " when replying to replyMessage " + req);
_context.tunnelDispatcher().dispatch(m);
} else {
// ok, the gateway is some other peer, shove 'er across
OutNetMessage outMsg = new OutNetMessage(_context, m, expires, PRIORITY, nextPeerInfo);
if (response == 0)
outMsg.setOnFailedSendJob(new TunnelBuildNextHopFailJob(_context, cfg));
_context.outNetMessagePool().add(outMsg);
}
}
}
use of net.i2p.router.tunnel.HopConfig in project i2p.i2p by i2p.
the class TunnelPool method configureNewTunnel.
/**
* @return null on failure
*/
private PooledTunnelCreatorConfig configureNewTunnel(boolean forceZeroHop) {
TunnelPoolSettings settings = getSettings();
// peers for new tunnel, including us, ENDPOINT FIRST
List<Hash> peers = null;
long now = _context.clock().now();
long expiration = now + TunnelPoolSettings.DEFAULT_DURATION;
if (!forceZeroHop) {
int len = settings.getLengthOverride();
if (len < 0)
len = settings.getLength();
if (len > 0 && (!settings.isExploratory()) && _context.random().nextBoolean()) {
// look for a tunnel to reuse, if the right length and expiring soon
// ignore variance for now.
// us
len++;
synchronized (_tunnels) {
for (TunnelInfo ti : _tunnels) {
if (ti.getLength() >= len && ti.getExpiration() < now + 3 * 60 * 1000 && !ti.wasReused()) {
ti.setReused();
len = ti.getLength();
peers = new ArrayList<Hash>(len);
// peers list is ordered endpoint first, but cfg.getPeer() is ordered gateway first
for (int i = len - 1; i >= 0; i--) {
peers.add(ti.getPeer(i));
}
break;
}
}
}
}
if (peers == null) {
setLengthOverride();
peers = _peerSelector.selectPeers(settings);
}
if ((peers == null) || (peers.isEmpty())) {
// the pool is refusing 0 hop tunnels
if (peers == null) {
if (_log.shouldLog(Log.WARN))
_log.warn("No peers to put in the new tunnel! selectPeers returned null! boo, hiss!");
} else {
if (_log.shouldLog(Log.WARN))
_log.warn("No peers to put in the new tunnel! selectPeers returned an empty list?!");
}
return null;
}
} else {
peers = Collections.singletonList(_context.routerHash());
}
PooledTunnelCreatorConfig cfg = new PooledTunnelCreatorConfig(_context, peers.size(), settings.isInbound(), settings.getDestination());
cfg.setTunnelPool(this);
// peers list is ordered endpoint first, but cfg.getPeer() is ordered gateway first
for (int i = 0; i < peers.size(); i++) {
int j = peers.size() - 1 - i;
cfg.setPeer(j, peers.get(i));
HopConfig hop = cfg.getConfig(j);
hop.setCreation(now);
hop.setExpiration(expiration);
hop.setIVKey(_context.keyGenerator().generateSessionKey());
hop.setLayerKey(_context.keyGenerator().generateSessionKey());
// tunnelIds will be updated during building, and as the creator, we
// don't need to worry about prev/next hop
}
// note that this will be adjusted by expire job
cfg.setExpiration(expiration);
if (!settings.isInbound())
cfg.setPriority(settings.getPriority());
if (_log.shouldLog(Log.DEBUG))
_log.debug("Config contains " + peers + ": " + cfg);
synchronized (_inProgress) {
_inProgress.add(cfg);
}
return cfg;
}
use of net.i2p.router.tunnel.HopConfig in project i2p.i2p by i2p.
the class TunnelRenderer method renderStatusHTML.
public void renderStatusHTML(Writer out) throws IOException {
out.write("<h3 class=\"tabletitle\" id=\"exploratorytunnels\"><a name=\"exploratory\" ></a>" + _t("Exploratory tunnels") + " <a href=\"/configtunnels#exploratory\" title=\"" + _t("Configure tunnels") + "\">[" + _t("configure") + "]</a></h3>\n");
renderPool(out, _context.tunnelManager().getInboundExploratoryPool(), _context.tunnelManager().getOutboundExploratoryPool());
List<Hash> destinations = null;
Map<Hash, TunnelPool> clientInboundPools = _context.tunnelManager().getInboundClientPools();
Map<Hash, TunnelPool> clientOutboundPools = _context.tunnelManager().getOutboundClientPools();
destinations = new ArrayList<Hash>(clientInboundPools.keySet());
boolean debug = _context.getBooleanProperty(HelperBase.PROP_ADVANCED);
for (int i = 0; i < destinations.size(); i++) {
Hash client = destinations.get(i);
boolean isLocal = _context.clientManager().isLocal(client);
if ((!isLocal) && (!debug))
continue;
TunnelPool in = clientInboundPools.get(client);
TunnelPool outPool = clientOutboundPools.get(client);
if ((in != null && in.getSettings().getAliasOf() != null) || (outPool != null && outPool.getSettings().getAliasOf() != null)) {
// skip aliases, we will print a header under the main tunnel pool below
continue;
}
// TODO the following code is duplicated in SummaryHelper
String name = (in != null) ? in.getSettings().getDestinationNickname() : null;
if ((name == null) && (outPool != null))
name = outPool.getSettings().getDestinationNickname();
if (name == null)
name = client.toBase64().substring(0, 4);
out.write("<h3 class=\"tabletitle\" id=\"" + client.toBase64().substring(0, 4) + "\" >" + _t("Client tunnels for") + ' ' + DataHelper.escapeHTML(_t(name)));
if (isLocal)
out.write(" <a href=\"/configtunnels#" + client.toBase64().substring(0, 4) + "\" title=\"" + _t("Configure tunnels for session") + "\">[" + _t("configure") + "]</a></h3>\n");
else
out.write(" (" + _t("dead") + ")</h3>\n");
if (in != null) {
// list aliases
Set<Hash> aliases = in.getSettings().getAliases();
if (aliases != null) {
for (Hash a : aliases) {
TunnelPool ain = clientInboundPools.get(a);
if (ain != null) {
String aname = ain.getSettings().getDestinationNickname();
if (aname == null)
aname = a.toBase64().substring(0, 4);
out.write("<h3 class=\"tabletitle\" id=\"" + a.toBase64().substring(0, 4) + "\" >" + _t("Client tunnels for") + ' ' + DataHelper.escapeHTML(_t(aname)));
if (isLocal)
out.write(" <a href=\"/configtunnels#" + client.toBase64().substring(0, 4) + "\" title=\"" + _t("Configure tunnels for session") + "\">[" + _t("configure") + "]</a></h3>\n");
else
out.write(" (" + _t("dead") + ")</h3>\n");
}
}
}
}
renderPool(out, in, outPool);
}
List<HopConfig> participating = _context.tunnelDispatcher().listParticipatingTunnels();
out.write("<h3 class=\"tabletitle\" id=\"participating\">" + _t("Participating tunnels") + "</h3>\n");
int bwShare = getShareBandwidth();
if (bwShare > 12) {
// Don't bother re-indenting
if (!participating.isEmpty()) {
Collections.sort(participating, new TunnelComparator());
out.write("<table class=\"tunneldisplay tunnels_participating\"><tr><th>" + _t("Receive on") + "</th><th>" + _t("From") + "</th><th>" + _t("Send on") + "</th><th>" + _t("To") + "</th><th>" + _t("Expiration") + "</th>" + "<th>" + _t("Usage") + "</th><th>" + _t("Rate") + "</th><th>" + _t("Role") + "</th></tr>\n");
}
long processed = 0;
RateStat rs = _context.statManager().getRate("tunnel.participatingMessageCount");
if (rs != null)
processed = (long) rs.getRate(10 * 60 * 1000).getLifetimeTotalValue();
int inactive = 0;
int displayed = 0;
for (int i = 0; i < participating.size(); i++) {
HopConfig cfg = participating.get(i);
int count = cfg.getProcessedMessagesCount();
if (count <= 0) {
inactive++;
continue;
}
// everything that isn't 'recent' is already in the tunnel.participatingMessageCount stat
processed += cfg.getRecentMessagesCount();
if (++displayed > DISPLAY_LIMIT)
continue;
out.write("<tr>");
if (cfg.getReceiveTunnel() != null)
out.write("<td class=\"cells\" align=\"center\" title=\"" + _t("Tunnel identity") + "\"><span class=\"tunnel_id\">" + cfg.getReceiveTunnel().getTunnelId() + "</span></td>");
else
out.write("<td class=\"cells\" align=\"center\">n/a</td>");
if (cfg.getReceiveFrom() != null)
out.write("<td class=\"cells\" align=\"center\"><span class=\"tunnel_peer\">" + netDbLink(cfg.getReceiveFrom()) + "</span></td>");
else
out.write("<td class=\"cells\"> </td>");
if (cfg.getSendTunnel() != null)
out.write("<td class=\"cells\" align=\"center\" title=\"" + _t("Tunnel identity") + "\"><span class=\"tunnel_id\">" + cfg.getSendTunnel().getTunnelId() + "</span></td>");
else
out.write("<td class=\"cells\"> </td>");
if (cfg.getSendTo() != null)
out.write("<td class=\"cells\" align=\"center\"><span class=\"tunnel_peer\">" + netDbLink(cfg.getSendTo()) + "</span></td>");
else
out.write("<td class=\"cells\"> </td>");
long timeLeft = cfg.getExpiration() - _context.clock().now();
if (timeLeft > 0)
out.write("<td class=\"cells\" align=\"center\">" + DataHelper.formatDuration2(timeLeft) + "</td>");
else
out.write("<td class=\"cells\" align=\"center\">(" + _t("grace period") + ")</td>");
out.write("<td class=\"cells\" align=\"center\">" + (count * 1024 / 1000) + " KB</td>");
int lifetime = (int) ((_context.clock().now() - cfg.getCreation()) / 1000);
if (lifetime <= 0)
lifetime = 1;
if (lifetime > 10 * 60)
lifetime = 10 * 60;
int bps = 1024 * count / lifetime;
out.write("<td class=\"cells\" align=\"center\">" + bps + " Bps</td>");
if (cfg.getSendTo() == null)
out.write("<td class=\"cells\" align=\"center\">" + _t("Outbound Endpoint") + "</td>");
else if (cfg.getReceiveFrom() == null)
out.write("<td class=\"cells\" align=\"center\">" + _t("Inbound Gateway") + "</td>");
else
out.write("<td class=\"cells\" align=\"center\">" + _t("Participant") + "</td>");
out.write("</tr>\n");
}
if (!participating.isEmpty())
out.write("</table>\n");
if (displayed > DISPLAY_LIMIT)
out.write("<div class=\"statusnotes\"><b>" + _t("Limited display to the {0} tunnels with the highest usage", DISPLAY_LIMIT) + "</b></div>\n");
if (inactive > 0)
out.write("<div class=\"statusnotes\"><b>" + _t("Inactive participating tunnels") + ": " + inactive + "</b></div>\n");
else if (displayed <= 0)
out.write("<div class=\"statusnotes\"><b>" + _t("none") + "</b></div>\n");
out.write("<div class=\"statusnotes\"><b>" + _t("Lifetime bandwidth usage") + ": " + DataHelper.formatSize2Decimal(processed * 1024) + "B</b></div>\n");
} else {
out.write("<div class=\"statusnotes noparticipate\"><b>" + _t("Not enough shared bandwidth to build participating tunnels.") + "</b> <a href=\"config\">[" + _t("Configure") + "]</a></div>\n");
}
// renderPeers(out);
out.write("<h3 class=\"tabletitle\">" + _t("Bandwidth Tiers") + "</h3>\n");
out.write("<table id=\"tunnel_defs\"><tbody>");
out.write("<tr><td> </td>" + "<td><span class=\"tunnel_cap\"><b>L</b></span></td><td>" + _t("{0} shared bandwidth", range(Router.MIN_BW_L, Router.MIN_BW_M)) + "</td>" + "<td><span class=\"tunnel_cap\"><b>M</b></span></td><td>" + _t("{0} shared bandwidth", range(Router.MIN_BW_M, Router.MIN_BW_N)) + "</td>" + "<td> </td></tr>");
out.write("<tr><td> </td>" + "<td><span class=\"tunnel_cap\"><b>N</b></span></td><td>" + _t("{0} shared bandwidth", range(Router.MIN_BW_N, Router.MIN_BW_O)) + "</td>" + "<td><span class=\"tunnel_cap\"><b>O</b></span></td><td>" + _t("{0} shared bandwidth", range(Router.MIN_BW_O, Router.MIN_BW_P)) + "</td>" + "<td> </td></tr>");
out.write("<tr><td> </td>" + "<td><span class=\"tunnel_cap\"><b>P</b></span></td><td>" + _t("{0} shared bandwidth", range(Router.MIN_BW_P, Router.MIN_BW_X)) + "</td>" + "<td><span class=\"tunnel_cap\"><b>X</b></span></td><td>" + _t("Over {0} shared bandwidth", Math.round(Router.MIN_BW_X * 1.024f) + " KBps") + "</td>" + "<td> </td></tr>");
out.write("</tbody></table>");
}
Aggregations