package net.i2p.router.transport.ntcp;
import java.io.IOException;
import java.io.Serializable;
import java.net.InetSocketAddress;
import java.net.InetAddress;
import java.net.Inet6Address;
import java.net.UnknownHostException;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.Vector;
import java.util.concurrent.ConcurrentHashMap;
import net.i2p.crypto.SigType;
import net.i2p.data.DataHelper;
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.DatabaseStoreMessage;
import net.i2p.data.i2np.I2NPMessage;
import net.i2p.router.CommSystemFacade.Status;
import net.i2p.router.OutNetMessage;
import net.i2p.router.RouterContext;
import net.i2p.router.transport.Transport;
import static net.i2p.router.transport.Transport.AddressSource.*;
import net.i2p.router.transport.TransportBid;
import net.i2p.router.transport.TransportImpl;
import net.i2p.router.transport.TransportUtil;
import static net.i2p.router.transport.TransportUtil.IPv6Config.*;
import net.i2p.router.transport.crypto.DHSessionKeyBuilder;
import net.i2p.router.util.DecayingHashSet;
import net.i2p.router.util.DecayingBloomFilter;
import net.i2p.util.Addresses;
import net.i2p.util.ConcurrentHashSet;
import net.i2p.util.Log;
import net.i2p.util.OrderedProperties;
import net.i2p.util.SystemVersion;
import net.i2p.util.VersionComparator;
/**
* The NIO TCP transport
*/
public class NTCPTransport extends TransportImpl {
private final Log _log;
private final SharedBid _fastBid;
private final SharedBid _slowBid;
private final SharedBid _slowCostBid;
/** save some conns for inbound */
private final SharedBid _nearCapacityBid;
private final SharedBid _nearCapacityCostBid;
private final SharedBid _transientFail;
private final Object _conLock;
private final Map<Hash, NTCPConnection> _conByIdent;
private final EventPumper _pumper;
private final Reader _reader;
private net.i2p.router.transport.ntcp.Writer _writer;
private int _ssuPort;
/** synch on this */
private final Set<InetSocketAddress> _endpoints;
/**
* list of NTCPConnection of connections not yet established that we
* want to remove on establishment or close on timeout
*/
private final Set<NTCPConnection> _establishing;
/** "bloom filter" */
private final DecayingBloomFilter _replayFilter;
/**
* Do we have a public IPv6 address?
* TODO periodically update via CSFI.NetMonitor?
*/
private boolean _haveIPv6Address;
private long _lastInboundIPv4;
private long _lastInboundIPv6;
public final static String PROP_I2NP_NTCP_HOSTNAME = "i2np.ntcp.hostname";
public final static String PROP_I2NP_NTCP_PORT = "i2np.ntcp.port";
public final static String PROP_I2NP_NTCP_AUTO_PORT = "i2np.ntcp.autoport";
public final static String PROP_I2NP_NTCP_AUTO_IP = "i2np.ntcp.autoip";
private static final String PROP_ADVANCED = "routerconsole.advanced";
public static final int DEFAULT_COST = 10;
/** this is rarely if ever used, default is to bind to wildcard address */
public static final String PROP_BIND_INTERFACE = "i2np.ntcp.bindInterface";
private final NTCPSendFinisher _finisher;
private final DHSessionKeyBuilder.Factory _dhFactory;
private long _lastBadSkew;
private static final long[] RATES = { 10*60*1000 };
// Opera doesn't have the char, TODO check UA
//private static final String THINSP = " / ";
private static final String THINSP = " / ";
/**
* RI sigtypes supported in 0.9.16
*/
public static final String MIN_SIGTYPE_VERSION = "0.9.16";
public NTCPTransport(RouterContext ctx, DHSessionKeyBuilder.Factory dh) {
super(ctx);
_dhFactory = dh;
_log = ctx.logManager().getLog(getClass());
_context.statManager().createRateStat("ntcp.sendTime", "Total message lifetime when sent completely", "ntcp", RATES);
_context.statManager().createRateStat("ntcp.sendQueueSize", "How many messages were ahead of the current one on the connection's queue when it was first added", "ntcp", RATES);
_context.statManager().createRateStat("ntcp.receiveTime", "How long it takes to receive an inbound message", "ntcp", RATES);
_context.statManager().createRateStat("ntcp.receiveSize", "How large the received message was", "ntcp", RATES);
_context.statManager().createRateStat("ntcp.sendBacklogTime", "How long the head of the send queue has been waiting when we fail to add a new one to the queue (period is the number of messages queued)", "ntcp", RATES);
_context.statManager().createRateStat("ntcp.failsafeWrites", "How many times do we need to proactively add in an extra nio write to a peer at any given failsafe pass?", "ntcp", RATES);
_context.statManager().createRateStat("ntcp.failsafeCloses", "How many times do we need to proactively close an idle connection to a peer at any given failsafe pass?", "ntcp", RATES);
_context.statManager().createRateStat("ntcp.failsafeInvalid", "How many times do we close a connection to a peer to work around a JVM bug?", "ntcp", RATES);
_context.statManager().createRateStat("ntcp.failsafeThrottle", "Delay event pumper", "ntcp", RATES);
_context.statManager().createRateStat("ntcp.accept", "", "ntcp", RATES);
_context.statManager().createRateStat("ntcp.attemptBanlistedPeer", "", "ntcp", RATES);
_context.statManager().createRateStat("ntcp.attemptUnreachablePeer", "", "ntcp", RATES);
_context.statManager().createRateStat("ntcp.closeOnBacklog", "", "ntcp", RATES);
_context.statManager().createRateStat("ntcp.connectFailedIOE", "", "ntcp", RATES);
//_context.statManager().createRateStat("ntcp.connectFailedInvalidPort", "", "ntcp", RATES);
//_context.statManager().createRateStat("ntcp.bidRejectedLocalAddress", "", "ntcp", RATES);
//_context.statManager().createRateStat("ntcp.bidRejectedNoNTCPAddress", "", "ntcp", RATES);
_context.statManager().createRateStat("ntcp.connectFailedTimeout", "", "ntcp", RATES);
_context.statManager().createRateStat("ntcp.connectFailedTimeoutIOE", "", "ntcp", RATES);
_context.statManager().createRateStat("ntcp.connectFailedUnresolved", "", "ntcp", RATES);
//_context.statManager().createRateStat("ntcp.connectImmediate", "", "ntcp", RATES);
_context.statManager().createRateStat("ntcp.connectSuccessful", "", "ntcp", RATES);
_context.statManager().createRateStat("ntcp.corruptDecryptedI2NP", "", "ntcp", RATES);
_context.statManager().createRateStat("ntcp.corruptI2NPCRC", "", "ntcp", RATES);
_context.statManager().createRateStat("ntcp.corruptI2NPIME", "", "ntcp", RATES);
_context.statManager().createRateStat("ntcp.corruptI2NPIOE", "", "ntcp", RATES);
_context.statManager().createRateStat("ntcp.corruptMetaCRC", "", "ntcp", RATES);
_context.statManager().createRateStat("ntcp.corruptSkew", "", "ntcp", RATES);
_context.statManager().createRateStat("ntcp.corruptTooLargeI2NP", "", "ntcp", RATES);
_context.statManager().createRateStat("ntcp.dontSendOnBacklog", "", "ntcp", RATES);
//_context.statManager().createRateStat("ntcp.inboundCheckConnection", "", "ntcp", RATES);
_context.statManager().createRateStat("ntcp.inboundEstablished", "", "ntcp", RATES);
_context.statManager().createRateStat("ntcp.inboundEstablishedDuplicate", "", "ntcp", RATES);
_context.statManager().createRateStat("ntcp.inboundIPv4Conn", "Inbound IPv4 NTCP Connection", "ntcp", RATES);
_context.statManager().createRateStat("ntcp.inboundIPv6Conn", "Inbound IPv6 NTCP Connection", "ntcp", RATES);
//_context.statManager().createRateStat("ntcp.infoMessageEnqueued", "", "ntcp", RATES);
//_context.statManager().createRateStat("ntcp.floodInfoMessageEnqueued", "", "ntcp", RATES);
_context.statManager().createRateStat("ntcp.invalidDH", "", "ntcp", RATES);
_context.statManager().createRateStat("ntcp.invalidHXY", "", "ntcp", RATES);
_context.statManager().createRateStat("ntcp.invalidHXxorBIH", "", "ntcp", RATES);
_context.statManager().createRateStat("ntcp.invalidInboundDFE", "", "ntcp", RATES);
_context.statManager().createRateStat("ntcp.invalidInboundIOE", "", "ntcp", RATES);
_context.statManager().createRateStat("ntcp.invalidInboundSignature", "", "ntcp", RATES);
_context.statManager().createRateStat("ntcp.invalidInboundSize", "", "ntcp", RATES);
_context.statManager().createRateStat("ntcp.invalidInboundSkew", "", "ntcp", RATES);
_context.statManager().createRateStat("ntcp.invalidSignature", "", "ntcp", RATES);
//_context.statManager().createRateStat("ntcp.liveReadBufs", "", "ntcp", RATES);
_context.statManager().createRateStat("ntcp.multipleCloseOnRemove", "", "ntcp", RATES);
_context.statManager().createRateStat("ntcp.outboundEstablishFailed", "", "ntcp", RATES);
_context.statManager().createRateStat("ntcp.outboundFailedIOEImmediate", "", "ntcp", RATES);
_context.statManager().createRateStat("ntcp.invalidOutboundSkew", "", "ntcp", RATES);
_context.statManager().createRateStat("ntcp.noBidTooLargeI2NP", "send size", "ntcp", RATES);
_context.statManager().createRateStat("ntcp.queuedRecv", "", "ntcp", RATES);
_context.statManager().createRateStat("ntcp.read", "", "ntcp", RATES);
//_context.statManager().createRateStat("ntcp.readEOF", "", "ntcp", RATES);
_context.statManager().createRateStat("ntcp.readError", "", "ntcp", RATES);
_context.statManager().createRateStat("ntcp.receiveCorruptEstablishment", "", "ntcp", RATES);
_context.statManager().createRateStat("ntcp.receiveMeta", "", "ntcp", RATES);
_context.statManager().createRateStat("ntcp.registerConnect", "", "ntcp", RATES);
_context.statManager().createRateStat("ntcp.replayHXxorBIH", "", "ntcp", RATES);
_context.statManager().createRateStat("ntcp.throttledReadComplete", "", "ntcp", RATES);
_context.statManager().createRateStat("ntcp.throttledWriteComplete", "", "ntcp", RATES);
_context.statManager().createRateStat("ntcp.wantsQueuedWrite", "", "ntcp", RATES);
//_context.statManager().createRateStat("ntcp.write", "", "ntcp", RATES);
_context.statManager().createRateStat("ntcp.writeError", "", "ntcp", RATES);
_endpoints = new HashSet<InetSocketAddress>(4);
_establishing = new ConcurrentHashSet<NTCPConnection>(16);
_conLock = new Object();
_conByIdent = new ConcurrentHashMap<Hash, NTCPConnection>(64);
_replayFilter = new DecayingHashSet(ctx, 10*60*1000, 8, "NTCP-Hx^HI");
_finisher = new NTCPSendFinisher(ctx, this);
_pumper = new EventPumper(ctx, this);
_reader = new Reader(ctx);
_writer = new net.i2p.router.transport.ntcp.Writer(ctx);
_fastBid = new SharedBid(25); // best
_slowBid = new SharedBid(70); // better than ssu unestablished, but not better than ssu established
_slowCostBid = new SharedBid(85);
_nearCapacityBid = new SharedBid(90); // not better than ssu - save our conns for inbound
_nearCapacityCostBid = new SharedBid(105);
_transientFail = new SharedBid(TransportBid.TRANSIENT_FAIL);
}
/**
* @param con that is established
* @return the previous connection to the same peer, must be closed by caller, null if no such.
*/
NTCPConnection inboundEstablished(NTCPConnection con) {
_context.statManager().addRateData("ntcp.inboundEstablished", 1);
Hash peer = con.getRemotePeer().calculateHash();
markReachable(peer, true);
//_context.banlist().unbanlistRouter(con.getRemotePeer().calculateHash());
NTCPConnection old;
synchronized (_conLock) {
old = _conByIdent.put(peer, con);
}
if (con.isIPv6()) {
_lastInboundIPv6 = con.getCreated();
_context.statManager().addRateData("ntcp.inboundIPv6Conn", 1);
} else {
_lastInboundIPv4 = con.getCreated();
_context.statManager().addRateData("ntcp.inboundIPv4Conn", 1);
}
return old;
}
protected void outboundMessageReady() {
OutNetMessage msg = getNextMessage();
if (msg != null) {
RouterInfo target = msg.getTarget();
RouterIdentity ident = target.getIdentity();
Hash ih = ident.calculateHash();
NTCPConnection con = null;
boolean isNew = false;
boolean fail = false;
synchronized (_conLock) {
con = _conByIdent.get(ih);
if (con == null) {
isNew = true;
RouterAddress addr = getTargetAddress(target);
if (addr != null) {
con = new NTCPConnection(_context, this, ident, addr);
if (_log.shouldLog(Log.DEBUG))
_log.debug("Send on a new con: " + con + " at " + addr + " for " + ih);
// Note that outbound conns go in the map BEFORE establishment
_conByIdent.put(ih, con);
} else {
// race, RI changed out from under us
// call afterSend below outside of conLock
fail = true;
}
}
}
if (fail) {
// race, RI changed out from under us, maybe SSU can handle it
if (_log.shouldLog(Log.WARN))
_log.warn("we bid on a peer who doesn't have an ntcp address? " + target);
afterSend(msg, false);
return;
}
if (isNew) {
// doesn't do anything yet, just enqueues it
con.send(msg);
// As of 0.9.12, don't send our info if the first message is
// doing the same (common when connecting to a floodfill).
// Also, put the info message after whatever we are trying to send
// (it's a priority queue anyway and the info is low priority)
// Prior to 0.9.12, Bob would not send his RI unless he had ours,
// but that's fixed in 0.9.12.
boolean shouldSkipInfo = false;
I2NPMessage m = msg.getMessage();
if (m.getType() == DatabaseStoreMessage.MESSAGE_TYPE) {
DatabaseStoreMessage dsm = (DatabaseStoreMessage) m;
if (dsm.getKey().equals(_context.routerHash())) {
shouldSkipInfo = true;
}
}
if (!shouldSkipInfo) {
con.enqueueInfoMessage();
} else if (_log.shouldLog(Log.INFO)) {
_log.info("SKIPPING INFO message: " + con);
}
try {
SocketChannel channel = SocketChannel.open();
con.setChannel(channel);
channel.configureBlocking(false);
_pumper.registerConnect(con);
con.getEstablishState().prepareOutbound();
} catch (IOException ioe) {
if (_log.shouldLog(Log.ERROR))
_log.error("Error opening a channel", ioe);
_context.statManager().addRateData("ntcp.outboundFailedIOEImmediate", 1);
con.close();
}
} else {
con.send(msg);
}
/*
NTCPConnection con = getCon(ident);
remove the race here
if (con != null) {
//if (_log.shouldLog(Log.DEBUG))
// _log.debug("Send on an existing con: " + con);
con.send(msg);
} else {
RouterAddress addr = msg.getTarget().getTargetAddress(STYLE);
if (addr != null) {
NTCPAddress naddr = new NTCPAddress(addr);
con = new NTCPConnection(_context, this, ident, naddr);
Hash ih = ident.calculateHash();
if (_log.shouldLog(Log.DEBUG))
_log.debug("Send on a new con: " + con + " at " + addr + " for " + ih.toBase64());
NTCPConnection old = null;
synchronized (_conLock) {
old = (NTCPConnection)_conByIdent.put(ih, con);
}
if (old != null) {
if (_log.shouldLog(Log.WARN))
_log.warn("Multiple connections on out ready, closing " + old + " and keeping " + con);
old.close();
}
con.enqueueInfoMessage(); // enqueues a netDb store of our own info
con.send(msg); // doesn't do anything yet, just enqueues it
try {
SocketChannel channel = SocketChannel.open();
con.setChannel(channel);
channel.configureBlocking(false);
_pumper.registerConnect(con);
} catch (IOException ioe) {
if (_log.shouldLog(Log.ERROR))
_log.error("Error opening a channel", ioe);
con.close();
}
} else {
con.close();
}
}
*/
}
}
@Override
public void afterSend(OutNetMessage msg, boolean sendSuccessful, boolean allowRequeue, long msToSend) {
super.afterSend(msg, sendSuccessful, allowRequeue, msToSend);
}
public TransportBid bid(RouterInfo toAddress, long dataSize) {
if (!isAlive())
return null;
if (dataSize > NTCPConnection.MAX_MSG_SIZE) {
// let SSU deal with it
_context.statManager().addRateData("ntcp.noBidTooLargeI2NP", dataSize);
return null;
}
Hash peer = toAddress.getIdentity().calculateHash();
if (_context.banlist().isBanlisted(peer, STYLE)) {
// we aren't banlisted in general (since we are trying to get a bid), but we have
// recently banlisted the peer on the NTCP transport, so don't try it
_context.statManager().addRateData("ntcp.attemptBanlistedPeer", 1);
return null;
} else if (isUnreachable(peer)) {
_context.statManager().addRateData("ntcp.attemptUnreachablePeer", 1);
return null;
}
boolean established = isEstablished(toAddress.getIdentity());
if (established) { // should we check the queue size? nah, if its valid, use it
if (_log.shouldLog(Log.DEBUG))
_log.debug("fast bid when trying to send to " + peer + " as its already established");
return _fastBid;
}
RouterAddress addr = getTargetAddress(toAddress);
if (addr == null) {
markUnreachable(peer);
return null;
}
// Check for supported sig type
SigType type = toAddress.getIdentity().getSigType();
if (type == null || !type.isAvailable()) {
markUnreachable(peer);
return null;
}
// Can we connect to them if we are not DSA?
RouterInfo us = _context.router().getRouterInfo();
if (us != null) {
RouterIdentity id = us.getIdentity();
if (id.getSigType() != SigType.DSA_SHA1) {
String v = toAddress.getVersion();
if (VersionComparator.comp(v, MIN_SIGTYPE_VERSION) < 0) {
markUnreachable(peer);
return null;
}
}
}
if (!allowConnection()) {
if (_log.shouldLog(Log.WARN))
_log.warn("no bid when trying to send to " + peer + ", max connection limit reached");
return _transientFail;
}
//if ( (_myAddress != null) && (_myAddress.equals(addr)) )
// return null; // dont talk to yourself
if (_log.shouldLog(Log.DEBUG))
_log.debug("slow bid when trying to send to " + peer);
if (haveCapacity()) {
if (addr.getCost() > DEFAULT_COST)
return _slowCostBid;
else
return _slowBid;
} else {
if (addr.getCost() > DEFAULT_COST)
return _nearCapacityCostBid;
else
return _nearCapacityBid;
}
}
/**
* Get first available address we can use.
* @return address or null
* @since 0.9.6
*/
private RouterAddress getTargetAddress(RouterInfo target) {
List<RouterAddress> addrs = getTargetAddresses(target);
for (int i = 0; i < addrs.size(); i++) {
RouterAddress addr = addrs.get(i);
byte[] ip = addr.getIP();
if (!TransportUtil.isValidPort(addr.getPort()) || ip == null) {
//_context.statManager().addRateData("ntcp.connectFailedInvalidPort", 1);
//_context.banlist().banlistRouter(toAddress.getIdentity().calculateHash(), "Invalid NTCP address", STYLE);
//if (_log.shouldLog(Log.DEBUG))
// _log.debug("no bid when trying to send to " + peer + " as they don't have a valid ntcp address");
continue;
}
if (!isValid(ip)) {
if (! allowLocal()) {
//_context.statManager().addRateData("ntcp.bidRejectedLocalAddress", 1);
//if (_log.shouldLog(Log.DEBUG))
// _log.debug("no bid when trying to send to " + peer + " as they have a private ntcp address");
continue;
}
}
return addr;
}
return null;
}
/**
* An IPv6 address is only valid if we are configured to support IPv6
* AND we have a public IPv6 address.
*
* @param addr may be null, returns false
* @since 0.9.8
*/
private boolean isValid(byte addr[]) {
if (addr == null) return false;
if (isPubliclyRoutable(addr) &&
(addr.length != 16 || _haveIPv6Address))
return true;
return false;
}
public boolean allowConnection() {
return countActivePeers() < getMaxConnections();
}
/** queue up afterSend call, which can take some time w/ jobs, etc */
void sendComplete(OutNetMessage msg) { _finisher.add(msg); }
private boolean isEstablished(RouterIdentity peer) {
return isEstablished(peer.calculateHash());
}
@Override
public boolean isEstablished(Hash dest) {
NTCPConnection con = _conByIdent.get(dest);
return (con != null) && con.isEstablished() && !con.isClosed();
}
@Override
public boolean isBacklogged(Hash dest) {
NTCPConnection con = _conByIdent.get(dest);
return (con != null) && con.isEstablished() && con.tooBacklogged();
}
/**
* Tell the transport that we may disconnect from this peer.
* This is advisory only.
*
* @since 0.9.24
*/
@Override
public void mayDisconnect(final Hash peer) {
final NTCPConnection con = _conByIdent.get(peer);
if (con != null && con.isEstablished() &&
con.getMessagesReceived() <= 2 && con.getMessagesSent() <= 1) {
con.setMayDisconnect();
}
}
/**
* @return usually the con passed in, but possibly a second connection with the same peer...
*/
NTCPConnection removeCon(NTCPConnection con) {
NTCPConnection removed = null;
RouterIdentity ident = con.getRemotePeer();
if (ident != null) {
synchronized (_conLock) {
removed = _conByIdent.remove(ident.calculateHash());
}
}
return removed;
}
public int countPeers() {
return _conByIdent.size();
}
/**
* How many peers have we talked to in the last 5 minutes?
* As of 0.9.20, actually returns active peer count, not total.
*/
public int countActivePeers() {
int active = 0;
for (NTCPConnection con : _conByIdent.values()) {
// con initializes times at construction,
// so check message count also
if ((con.getMessagesSent() > 0 && con.getTimeSinceSend() <= 5*60*1000) ||
(con.getMessagesReceived() > 0 && con.getTimeSinceReceive() <= 5*60*1000)) {
active++;
}
}
return active;
}
/**
* How many peers are we actively sending messages to (this minute)
*/
public int countActiveSendPeers() {
int active = 0;
for (NTCPConnection con : _conByIdent.values()) {
// con initializes times at construction,
// so check message count also
if (con.getMessagesSent() > 0 && con.getTimeSinceSend() <= 60*1000) {
active++;
}
}
return active;
}
/**
* A positive number means our clock is ahead of theirs.
*
* @param skew in seconds
*/
void setLastBadSkew(long skew) {
_lastBadSkew = skew;
}
/**
* Return our peer clock skews on this transport.
* Vector composed of Long, each element representing a peer skew in seconds.
* A positive number means our clock is ahead of theirs.
*/
@Override
public Vector<Long> getClockSkews() {
Vector<Long> skews = new Vector<Long>();
// Omit ones established too long ago,
// since the skew is only set at startup (or after a meta message)
// and won't include effects of later offset adjustments
long tooOld = _context.clock().now() - 10*60*1000;
for (NTCPConnection con : _conByIdent.values()) {
if (con.isEstablished() && con.getCreated() > tooOld)
skews.addElement(Long.valueOf(con.getClockSkew()));
}
// If we don't have many peers, maybe it is because of a bad clock, so
// return the last bad skew we got
if (skews.size() < 5 && _lastBadSkew != 0)
skews.addElement(Long.valueOf(_lastBadSkew));
if (_log.shouldLog(Log.DEBUG))
_log.debug("NTCP transport returning " + skews.size() + " peer clock skews.");
return skews;
}
/**
* Incoming connection replay detection.
* As there is no timestamp in the first message, we can't detect
* something long-delayed. To be fixed in next version of NTCP.
*
* @param hxhi 32 bytes
* @return valid
* @since 0.9.12
*/
boolean isHXHIValid(byte[] hxhi) {
return !_replayFilter.add(hxhi, 0, 8);
}
private static final int MIN_CONCURRENT_READERS = 2; // unless < 32MB
private static final int MIN_CONCURRENT_WRITERS = 2; // unless < 32MB
private static final int MAX_CONCURRENT_READERS = 4;
private static final int MAX_CONCURRENT_WRITERS = 4;
/**
* Called by TransportManager.
* Caller should stop the transport first, then
* verify stopped with isAlive()
* Unfortunately TransportManager doesn't do that, so we
* check here to prevent two pumpers.
*/
public synchronized void startListening() {
// try once again to prevent two pumpers which is fatal
if (_pumper.isAlive())
return;
if (_log.shouldLog(Log.WARN)) _log.warn("Starting NTCP transport listening");
startIt();
RouterAddress addr = configureLocalAddress();
int port;
if (addr != null)
// probably not set
port = addr.getPort();
else
// received by externalAddressReceived() from TransportManager
port = _ssuPort;
RouterAddress myAddress = bindAddress(port);
if (myAddress != null) {
// fixed interface, or bound to the specified host
replaceAddress(myAddress);
} else if (addr != null) {
// specified host, bound to wildcard
replaceAddress(addr);
} else if (port > 0) {
// all detected interfaces
for (InetAddress ia : getSavedLocalAddresses()) {
OrderedProperties props = new OrderedProperties();
props.setProperty(RouterAddress.PROP_HOST, ia.getHostAddress());
props.setProperty(RouterAddress.PROP_PORT, Integer.toString(port));
int cost = getDefaultCost(ia instanceof Inet6Address);
myAddress = new RouterAddress(STYLE, props, cost);
replaceAddress(myAddress);
}
}
// TransportManager.startListening() calls router.rebuildRouterInfo()
}
/**
* Only called by externalAddressReceived().
* Calls replaceAddress() or removeAddress().
* To remove all addresses, call replaceAddress(null) directly.
*
* Doesn't actually restart unless addr is non-null and
* the port is different from the current listen port.
* If addr is null, removes the addresses specified (v4 or v6)
*
* If we had interface addresses before, we lost them.
*
* @param addr may be null to indicate remove the address
* @param ipv6 ignored if addr is non-null
*/
private synchronized void restartListening(RouterAddress addr, boolean ipv6) {
if (addr != null) {
RouterAddress myAddress = bindAddress(addr.getPort());
if (myAddress != null)
replaceAddress(myAddress);
else
replaceAddress(addr);
// UDPTransport.rebuildExternalAddress() calls router.rebuildRouterInfo()
} else {
removeAddress(ipv6);
if (ipv6)
_lastInboundIPv6 = 0;
else
_lastInboundIPv4 = 0;
}
}
/**
* Start up. Caller must synchronize.
* @since 0.8.3
*/
private void startIt() {
_finisher.start();
_pumper.startPumping();
long maxMemory = SystemVersion.getMaxMemory();
int nr, nw;
if (maxMemory < 32*1024*1024) {
nr = nw = 1;
} else if (maxMemory < 64*1024*1024) {
nr = nw = 2;
} else {
nr = Math.max(MIN_CONCURRENT_READERS, Math.min(MAX_CONCURRENT_READERS, _context.bandwidthLimiter().getInboundKBytesPerSecond() / 20));
nw = Math.max(MIN_CONCURRENT_WRITERS, Math.min(MAX_CONCURRENT_WRITERS, _context.bandwidthLimiter().getOutboundKBytesPerSecond() / 20));
}
_reader.startReading(nr);
_writer.startWriting(nw);
}
public boolean isAlive() {
return _pumper.isAlive();
}
/**
* Only does something if myPort > 0 and myPort != current bound port
* (or there's no current port, or the configured interface or hostname changed).
* If we are changing the bound port, this restarts everything, which takes a long time.
*
* call from synchronized method
*
* @param myPort does nothing if <= 0
* @return new address ONLY if bound to specific address, otherwise null
*/
private RouterAddress bindAddress(int port) {
RouterAddress myAddress = null;
if (port > 0) {
InetAddress bindToAddr = null;
String bindTo = _context.getProperty(PROP_BIND_INTERFACE);
if (bindTo == null) {
// If we are configured with a fixed IP address,
// AND it's one of our local interfaces,
// bind only to that.
bindTo = getFixedHost();
}
if (bindTo != null) {
try {
bindToAddr = InetAddress.getByName(bindTo);
} catch (UnknownHostException uhe) {
_log.error("Invalid NTCP bind interface specified [" + bindTo + "]", uhe);
// this can be implemented later, just updates some stats
// see udp/UDPTransport.java
//setReachabilityStatus(CommSystemFacade.STATUS_HOSED);
//return null;
// fall thru
}
}
try {
InetSocketAddress addr;
if (bindToAddr == null) {
addr = new InetSocketAddress(port);
} else {
addr = new InetSocketAddress(bindToAddr, port);
if (_log.shouldLog(Log.WARN))
_log.warn("Binding only to " + bindToAddr);
OrderedProperties props = new OrderedProperties();
props.setProperty(RouterAddress.PROP_HOST, bindTo);
props.setProperty(RouterAddress.PROP_PORT, Integer.toString(port));
int cost = getDefaultCost(false);
myAddress = new RouterAddress(STYLE, props, cost);
}
if (!_endpoints.isEmpty()) {
// If we are already bound to the new address, OR
// if the host is specified and we are bound to the wildcard on the same port,
// do nothing. Changing config from wildcard to a specified host will
// require a restart.
if (_endpoints.contains(addr) ||
(bindToAddr != null && _endpoints.contains(new InetSocketAddress(port)))) {
if (_log.shouldLog(Log.WARN))
_log.warn("Already listening on " + addr);
return null;
}
// FIXME support multiple binds
// FIXME just close and unregister
stopWaitAndRestart();
}
if (!TransportUtil.isValidPort(port))
_log.error("Specified NTCP port is " + port + ", ports lower than 1024 not recommended");
ServerSocketChannel chan = ServerSocketChannel.open();
chan.configureBlocking(false);
chan.socket().bind(addr);
_endpoints.add(addr);
if (_log.shouldLog(Log.INFO))
_log.info("Listening on " + addr);
_pumper.register(chan);
} catch (IOException ioe) {
_log.error("Error listening", ioe);
myAddress = null;
}
} else {
if (_log.shouldLog(Log.INFO))
_log.info("Outbound NTCP connections only - no listener configured");
}
return myAddress;
}
/**
* @return configured host or null. Must be one of our local interfaces.
* @since IPv6 moved from bindAddress()
*/
private String getFixedHost() {
boolean isFixed = _context.getProperty(PROP_I2NP_NTCP_AUTO_IP, "true")
.toLowerCase(Locale.US).equals("false");
String fixedHost = _context.getProperty(PROP_I2NP_NTCP_HOSTNAME);
if (isFixed && fixedHost != null) {
try {
String testAddr = InetAddress.getByName(fixedHost).getHostAddress();
// FIXME range of IPv6 addresses
if (Addresses.getAddresses().contains(testAddr))
return testAddr;
} catch (UnknownHostException uhe) {}
}
return null;
}
/**
* Caller must sync
* @since IPv6 moved from externalAddressReceived()
*/
private void stopWaitAndRestart() {
if (_log.shouldLog(Log.WARN))
_log.warn("Halting NTCP to change address");
stopListening();
// Wait for NTCP Pumper to stop so we don't end up with two...
while (isAlive()) {
try { Thread.sleep(5*1000); } catch (InterruptedException ie) {}
}
if (_log.shouldLog(Log.WARN))
_log.warn("Restarting NTCP transport listening");
startIt();
}
/**
* Hook for NTCPConnection
*/
Reader getReader() { return _reader; }
/**
* Hook for NTCPConnection
*/
net.i2p.router.transport.ntcp.Writer getWriter() { return _writer; }
public String getStyle() { return STYLE; }
/**
* Hook for NTCPConnection
*/
EventPumper getPumper() { return _pumper; }
/**
* @since 0.9
*/
DHSessionKeyBuilder getDHBuilder() {
return _dhFactory.getBuilder();
}
/**
* Return an unused DH key builder
* to be put back onto the queue for reuse.
*
* @param builder must not have a peerPublicValue set
* @since 0.9.16
*/
void returnUnused(DHSessionKeyBuilder builder) {
_dhFactory.returnUnused(builder);
}
/**
* how long from initial connection attempt (accept() or connect()) until
* the con must be established to avoid premature close()ing
*/
public static final int ESTABLISH_TIMEOUT = 10*1000;
/** add us to the establishment timeout process */
void establishing(NTCPConnection con) {
_establishing.add(con);
}
/**
* called in the EventPumper no more than once a second or so, closing
* any unconnected/unestablished connections
*/
void expireTimedOut() {
int expired = 0;
for (Iterator<NTCPConnection> iter = _establishing.iterator(); iter.hasNext(); ) {
NTCPConnection con = iter.next();
if (con.isClosed() || con.isEstablished()) {
iter.remove();
} else if (con.getTimeSinceCreated() > ESTABLISH_TIMEOUT) {
iter.remove();
con.close();
expired++;
}
}
if (expired > 0)
_context.statManager().addRateData("ntcp.outboundEstablishFailed", expired);
}
//private boolean bindAllInterfaces() { return true; }
/**
* Generally returns null
* caller must synch on this
*/
private RouterAddress configureLocalAddress() {
// this generally returns null -- see javadoc
RouterAddress addr = createNTCPAddress();
if (addr != null) {
if (addr.getPort() <= 0) {
addr = null;
if (_log.shouldLog(Log.ERROR))
_log.error("NTCP address is outbound only, since the NTCP configuration is invalid");
} else {
if (_log.shouldLog(Log.INFO))
_log.info("NTCP address configured: " + addr);
}
} else {
if (_log.shouldLog(Log.INFO))
_log.info("NTCP address is outbound only");
}
return addr;
}
/**
* This only creates an address if the hostname AND port are set in router.config,
* which should be rare.
* Otherwise, notifyReplaceAddress() below takes care of it.
* Note this is called both from above and from NTCPTransport.startListening()
*
* @since IPv6 moved from CSFI
*/
private RouterAddress createNTCPAddress() {
// Fixme doesn't check PROP_BIND_INTERFACE
String name = _context.getProperty(PROP_I2NP_NTCP_HOSTNAME);
if ( (name == null) || (name.trim().length() <= 0) || ("null".equals(name)) )
return null;
int p = _context.getProperty(PROP_I2NP_NTCP_PORT, -1);
if (p <= 0 || p >= 64*1024)
return null;
OrderedProperties props = new OrderedProperties();
props.setProperty(RouterAddress.PROP_HOST, name);
props.setProperty(RouterAddress.PROP_PORT, Integer.toString(p));
int cost = getDefaultCost(false);
RouterAddress addr = new RouterAddress(STYLE, props, cost);
return addr;
}
private int getDefaultCost(boolean isIPv6) {
int rv = DEFAULT_COST;
if (isIPv6) {
TransportUtil.IPv6Config config = getIPv6Config();
if (config == IPV6_PREFERRED)
rv--;
else if (config == IPV6_NOT_PREFERRED)
rv++;
}
return rv;
}
/**
* UDP changed addresses, tell NTCP and (possibly) restart
*
* @param ip typ. IPv4 or IPv6 non-local; may be null to indicate IPv4 failure or port info only
* @since IPv6 moved from CSFI.notifyReplaceAddress()
*/
@Override
public void externalAddressReceived(AddressSource source, byte[] ip, int port) {
if (_log.shouldLog(Log.WARN))
_log.warn("Received address: " + Addresses.toString(ip, port) + " from: " + source, new Exception());
if ((source == SOURCE_INTERFACE || source == SOURCE_SSU)
&& ip != null && ip.length == 16) {
// must be set before isValid() call
_haveIPv6Address = true;
}
if (ip != null && !isValid(ip)) {
if (_log.shouldLog(Log.WARN))
_log.warn("Invalid address: " + Addresses.toString(ip, port) + " from: " + source);
return;
}
if (!isAlive()) {
if (source == SOURCE_INTERFACE || source == SOURCE_UPNP) {
try {
InetAddress ia = InetAddress.getByAddress(ip);
saveLocalAddress(ia);
} catch (UnknownHostException uhe) {}
} else if (source == SOURCE_CONFIG) {
// save for startListening()
_ssuPort = port;
}
return;
}
// ignore UPnP for now, get everything from SSU
if (source != SOURCE_SSU)
return;
boolean isIPv6 = ip != null && ip.length == 16;
externalAddressReceived(ip, isIPv6, port);
}
/**
* Notify a transport of an external address change.
* This may be from a local interface, UPnP, a config change, etc.
* This should not be called if the ip didn't change
* (from that source's point of view), or is a local address.
* May be called multiple times for IPv4 or IPv6.
* The transport should also do its own checking on whether to accept
* notifications from this source.
*
* This can be called after the transport is running.
*
* TODO externalAddressRemoved(source, ip, port)
*
* @param source defined in Transport.java
* @since 0.9.20
*/
@Override
public void externalAddressRemoved(AddressSource source, boolean ipv6) {
if (_log.shouldWarn())
_log.warn("Removing address, ipv6? " + ipv6 + " from: " + source, new Exception());
// ignore UPnP for now, get everything from SSU
if (source != SOURCE_SSU)
return;
externalAddressReceived(null, ipv6, 0);
}
/**
* UDP changed addresses, tell NTCP and restart.
* Port may be set to indicate requested port even if ip is null.
*
* @param ip previously validated; may be null to indicate IPv4 failure or port info only
* @since IPv6 moved from CSFI.notifyReplaceAddress()
*/
private synchronized void externalAddressReceived(byte[] ip, boolean isIPv6, int port) {
// FIXME just take first address for now
// FIXME if SSU set to hostname, NTCP will be set to IP
RouterAddress oldAddr = getCurrentAddress(isIPv6);
if (_log.shouldLog(Log.INFO))
_log.info("Changing NTCP Address? was " + oldAddr);
OrderedProperties newProps = new OrderedProperties();
int cost;
if (oldAddr == null) {
cost = getDefaultCost(isIPv6);
} else {
cost = oldAddr.getCost();
newProps.putAll(oldAddr.getOptionsMap());
}
RouterAddress newAddr = new RouterAddress(STYLE, newProps, cost);
boolean changed = false;
// Auto Port Setting
// old behavior (<= 0.7.3): auto-port defaults to false, and true trumps explicit setting
// new behavior (>= 0.7.4): auto-port defaults to true, but explicit setting trumps auto
// TODO rewrite this to operate on ints instead of strings
String oport = newProps.getProperty(RouterAddress.PROP_PORT);
String nport = null;
String cport = _context.getProperty(PROP_I2NP_NTCP_PORT);
if (cport != null && cport.length() > 0) {
nport = cport;
} else if (_context.getBooleanPropertyDefaultTrue(PROP_I2NP_NTCP_AUTO_PORT)) {
// 0.9.6 change
// This wasn't quite right, as udpAddr is the EXTERNAL port and we really
// want NTCP to bind to the INTERNAL port the first time,
// because if they are different, the NAT is changing them, and
// it probably isn't mapping UDP and TCP the same.
if (port > 0)
// should always be true
nport = Integer.toString(port);
}
if (_log.shouldLog(Log.INFO))
_log.info("old port: " + oport + " config: " + cport + " new: " + nport);
//if (nport == null || nport.length() <= 0)
// return;
// 0.9.6 change
// Don't have NTCP "chase" SSU's external port,
// as it may change, possibly frequently.
//if (oport == null || ! oport.equals(nport)) {
if (oport == null && nport != null && nport.length() > 0) {
newProps.setProperty(RouterAddress.PROP_PORT, nport);
changed = true;
}
// Auto IP Setting
// old behavior (<= 0.7.3): auto-ip defaults to false, and trumps configured hostname,
// and ignores reachability status - leading to
// "firewalled with inbound TCP enabled" warnings.
// new behavior (>= 0.7.4): auto-ip defaults to true, and explicit setting trumps auto,
// and only takes effect if reachability is OK.
// And new "always" setting ignores reachability status, like
// "true" was in 0.7.3
String ohost = newProps.getProperty(RouterAddress.PROP_HOST);
String enabled = _context.getProperty(PROP_I2NP_NTCP_AUTO_IP, "true").toLowerCase(Locale.US);
String name = _context.getProperty(PROP_I2NP_NTCP_HOSTNAME);
// hostname config trumps auto config
if (name != null && name.length() > 0)
enabled = "false";
// assume SSU is happy if the address is non-null
// TODO is this sufficient?
boolean ssuOK = ip != null;
if (_log.shouldLog(Log.INFO))
_log.info("old: " + ohost + " config: " + name + " auto: " + enabled + " ssuOK? " + ssuOK);
if (enabled.equals("always") ||
(Boolean.parseBoolean(enabled) && ssuOK)) {
if (!ssuOK) {
if (_log.shouldLog(Log.WARN))
_log.warn("null address with always config", new Exception());
return;
}
// ip non-null
String nhost = Addresses.toString(ip);
if (_log.shouldLog(Log.INFO))
_log.info("old: " + ohost + " config: " + name + " new: " + nhost);
if (nhost == null || nhost.length() <= 0)
return;
if (ohost == null || ! ohost.equalsIgnoreCase(nhost)) {
newProps.setProperty(RouterAddress.PROP_HOST, nhost);
changed = true;
}
} else if (enabled.equals("false") &&
name != null && name.length() > 0 &&
!name.equals(ohost)) {
// Host name is configured, and we have a port (either auto or configured)
// but we probably only get here if the port is auto,
// otherwise createNTCPAddress() would have done it already
if (_log.shouldLog(Log.INFO))
_log.info("old host: " + ohost + " config: " + name + " new: " + name);
newProps.setProperty(RouterAddress.PROP_HOST, name);
changed = true;
} else if (ohost == null || ohost.length() <= 0) {
return;
} else if (Boolean.parseBoolean(enabled) && !ssuOK) {
// UDP transitioned to not-OK, turn off NTCP address
// This will commonly happen at startup if we were initially OK
// because UPnP was successful, but a subsequent SSU Peer Test determines
// we are still firewalled (SW firewall, bad UPnP indication, etc.)
if (_log.shouldLog(Log.INFO))
_log.info("old host: " + ohost + " config: " + name + " new: null");
newAddr = null;
changed = true;
}
if (!changed) {
if (oldAddr != null) {
// change cost only?
int oldCost = oldAddr.getCost();
int newCost = getDefaultCost(ohost != null && ohost.contains(":"));
if (ADJUST_COST && !haveCapacity())
newCost += CONGESTION_COST_ADJUSTMENT;
if (newCost != oldCost) {
newAddr.setCost(newCost);
if (_log.shouldLog(Log.WARN))
_log.warn("Changing NTCP cost from " + oldCost + " to " + newCost);
// fall thru and republish
} else {
_log.info("No change to NTCP Address");
return;
}
} else {
_log.info("No change to NTCP Address");
return;
}
}
// stopListening stops the pumper, readers, and writers, so required even if
// oldAddr == null since startListening starts them all again
//
// really need to fix this so that we can change or create an inbound address
// without tearing down everything
// Especially on disabling the address, we shouldn't tear everything down.
//
//if (_log.shouldLog(Log.WARN))
// _log.warn("Halting NTCP to change address");
//stopListening();
// Wait for NTCP Pumper to stop so we don't end up with two...
//while (isAlive()) {
// try { Thread.sleep(5*1000); } catch (InterruptedException ie) {}
//}
restartListening(newAddr, isIPv6);
if (_log.shouldLog(Log.WARN))
_log.warn("Updating NTCP Address (ipv6? " + isIPv6 + ") with " + newAddr);
return;
}
/**
* If we didn't used to be forwarded, and we have an address,
* and we are configured to use UPnP, update our RouterAddress
*
* Don't do anything now. If it fails, we don't know if it's
* because there is no firewall, or if the firewall rejected the request.
* So we just use the SSU reachability status
* to decide whether to enable inbound NTCP. SSU will have CSFI build a new
* NTCP address when it transitions to OK.
*/
@Override
public void forwardPortStatus(byte[] ip, int port, int externalPort, boolean success, String reason) {
if (_log.shouldLog(Log.WARN)) {
if (success)
_log.warn("UPnP has opened the NTCP port: " + port + " via " + Addresses.toString(ip, externalPort));
else
_log.warn("UPnP has failed to open the NTCP port: " + port + " reason: " + reason);
}
}
/**
* @return current IPv4 port, else NTCP configured port, else -1 (but not UDP port if auto)
*/
@Override
public int getRequestedPort() {
RouterAddress addr = getCurrentAddress(false);
if (addr != null) {
int port = addr.getPort();
if (port > 0)
return port;
}
// would be nice to do this here but we can't easily get to the UDP transport.getRequested_Port()
// from here, so we do it in TransportManager.
// if (Boolean.valueOf(_context.getProperty(CommSystemFacadeImpl.PROP_I2NP_NTCP_AUTO_PORT)).booleanValue())
// return foo;
return _context.getProperty(PROP_I2NP_NTCP_PORT, -1);
}
/**
* Maybe we should trust UPnP here and report OK if it opened the port, but
* for now we don't. Just go through and if we have one inbound connection,
* we must be good. As we drop idle connections pretty quickly, this will
* be fairly accurate.
*
* We have to be careful here because much of the router console code assumes
* that the reachability status is really just the UDP status.
*
* This only returns OK, DISABLED, or UNKNOWN for IPv4 and IPv6.
* We leave the FIREWALLED status for UDP.
*
* Previously returned short, now enum as of 0.9.20
*/
public Status getReachabilityStatus() {
if (!isAlive())
return Status.UNKNOWN;
TransportUtil.IPv6Config config = getIPv6Config();
boolean v4Disabled, v6Disabled;
if (config == IPV6_DISABLED) {
v4Disabled = false;
v6Disabled = true;
} else if (config == IPV6_ONLY) {
v4Disabled = true;
v6Disabled = false;
} else {
v4Disabled = false;
v6Disabled = false;
}
boolean hasV4 = getCurrentAddress(false) != null;
// or use _haveIPv6Addrnss ??
boolean hasV6 = getCurrentAddress(true) != null;
if (!hasV4 && !hasV6)
return Status.UNKNOWN;
long now = _context.clock().now();
boolean v4OK = hasV4 && !v4Disabled && now - _lastInboundIPv4 < 10*60*1000;
boolean v6OK = hasV6 && !v6Disabled && now - _lastInboundIPv6 < 30*60*1000;
if (v4OK) {
if (v6OK)
return Status.OK;
if (v6Disabled)
return Status.OK;
if (!hasV6)
return Status.IPV4_OK_IPV6_UNKNOWN;
}
if (v6OK) {
if (v4Disabled)
return Status.IPV4_DISABLED_IPV6_OK;
if (!hasV4)
return Status.IPV4_UNKNOWN_IPV6_OK;
}
for (NTCPConnection con : _conByIdent.values()) {
if (con.isInbound()) {
if (con.isIPv6()) {
if (hasV6)
v6OK = true;
} else {
if (hasV4)
v4OK = true;
}
if (v4OK) {
if (v6OK)
return Status.OK;
if (v6Disabled)
return Status.OK;
if (!hasV6)
return Status.IPV4_OK_IPV6_UNKNOWN;
}
if (v6OK) {
if (v4Disabled)
return Status.IPV4_DISABLED_IPV6_OK;
if (!hasV4)
return Status.IPV4_UNKNOWN_IPV6_OK;
}
}
}
if (v4OK)
return Status.IPV4_OK_IPV6_UNKNOWN;
if (v6OK)
return Status.IPV4_UNKNOWN_IPV6_OK;
if (v4Disabled)
return Status.IPV4_DISABLED_IPV6_UNKNOWN;
if (v6Disabled)
return Status.UNKNOWN;
return Status.UNKNOWN;
}
/**
* This doesn't (completely) block, caller should check isAlive()
* before calling startListening() or restartListening()
*/
public synchronized void stopListening() {
if (_log.shouldLog(Log.WARN)) _log.warn("Stopping ntcp transport");
_pumper.stopPumping();
_writer.stopWriting();
_reader.stopReading();
_finisher.stop();
List<NTCPConnection> cons;
synchronized (_conLock) {
cons = new ArrayList<NTCPConnection>(_conByIdent.values());
_conByIdent.clear();
}
for (NTCPConnection con : cons) {
con.close();
}
NTCPConnection.releaseResources();
replaceAddress(null);
_endpoints.clear();
_lastInboundIPv4 = 0;
_lastInboundIPv6 = 0;
}
public static final String STYLE = "NTCP";
public void renderStatusHTML(java.io.Writer out, int sortFlags) throws IOException {}
@Override
public void renderStatusHTML(java.io.Writer out, String urlBase, int sortFlags) throws IOException {
TreeSet<NTCPConnection> peers = new TreeSet<NTCPConnection>(getComparator(sortFlags));
peers.addAll(_conByIdent.values());
long offsetTotal = 0;
float bpsSend = 0;
float bpsRecv = 0;
long totalUptime = 0;
long totalSend = 0;
long totalRecv = 0;
if (!_context.getBooleanProperty(PROP_ADVANCED)) {
for (Iterator<NTCPConnection> iter = peers.iterator(); iter.hasNext(); ) {
// outbound conns get put in the map before they are established
if (!iter.next().isEstablished())
iter.remove();
}
}
StringBuilder buf = new StringBuilder(512);
buf.append("<h3 id=\"ntcpcon\">").append(_t("NTCP connections")).append(": ").append(peers.size());
buf.append(". ").append(_t("Limit")).append(": ").append(getMaxConnections());
buf.append(". ").append(_t("Timeout")).append(": ").append(DataHelper.formatDuration2(_pumper.getIdleTimeout()));
if (_context.getBooleanProperty(PROP_ADVANCED)) {
buf.append(". ").append(_t("Status")).append(": ").append(_t(getReachabilityStatus().toStatusString()));
}
buf.append(".</h3>\n" +
"<table>\n" +
"<tr><th><a href=\"#def.peer\">").append(_t("Peer")).append("</a></th>" +
"<th>").append(_t("Dir")).append("</th>" +
"<th>").append(_t("IPv6")).append("</th>" +
"<th align=\"right\"><a href=\"#def.idle\">").append(_t("Idle")).append("</a></th>" +
"<th align=\"right\"><a href=\"#def.rate\">").append(_t("In/Out")).append("</a></th>" +
"<th align=\"right\"><a href=\"#def.up\">").append(_t("Up")).append("</a></th>" +
"<th align=\"right\"><a href=\"#def.skew\">").append(_t("Skew")).append("</a></th>" +
"<th align=\"right\"><a href=\"#def.send\">").append(_t("TX")).append("</a></th>" +
"<th align=\"right\"><a href=\"#def.recv\">").append(_t("RX")).append("</a></th>" +
"<th>").append(_t("Out Queue")).append("</th>" +
"<th>").append(_t("Backlogged?")).append("</th>" +
//"<th>").append(_t("Reading?")).append("</th>" +
" </tr>\n");
out.write(buf.toString());
buf.setLength(0);
for (NTCPConnection con : peers) {
buf.append("<tr><td class=\"cells\" align=\"left\" nowrap>");
buf.append(_context.commSystem().renderPeerHTML(con.getRemotePeer().calculateHash()));
//byte[] ip = getIP(con.getRemotePeer().calculateHash());
//if (ip != null)
// buf.append(' ').append(_context.blocklist().toStr(ip));
buf.append("</td><td class=\"cells\" align=\"center\">");
if (con.isInbound())
buf.append("<img src=\"/themes/console/images/inbound.png\" alt=\"Inbound\" title=\"").append(_t("Inbound")).append("\"/>");
else
buf.append("<img src=\"/themes/console/images/outbound.png\" alt=\"Outbound\" title=\"").append(_t("Outbound")).append("\"/>");
buf.append("</td><td class=\"cells\" align=\"center\">");
if (con.isIPv6())
buf.append("✓");
else
buf.append(" ");
buf.append("</td><td class=\"cells\" align=\"right\">");
buf.append(DataHelper.formatDuration2(con.getTimeSinceReceive()));
buf.append(THINSP).append(DataHelper.formatDuration2(con.getTimeSinceSend()));
buf.append("</td><td class=\"cells\" align=\"right\">");
if (con.getTimeSinceReceive() < 2*60*1000) {
float r = con.getRecvRate();
buf.append(formatRate(r / 1024));
bpsRecv += r;
} else {
buf.append(formatRate(0));
}
buf.append(THINSP);
if (con.getTimeSinceSend() < 2*60*1000) {
float r = con.getSendRate();
buf.append(formatRate(r / 1024));
bpsSend += r;
} else {
buf.append(formatRate(0));
}
//buf.append(" K/s");
buf.append("</td><td class=\"cells\" align=\"right\">").append(DataHelper.formatDuration2(con.getUptime()));
totalUptime += con.getUptime();
offsetTotal = offsetTotal + con.getClockSkew();
buf.append("</td><td class=\"cells\" align=\"right\">").append(DataHelper.formatDuration2(1000 * con.getClockSkew()));
buf.append("</td><td class=\"cells\" align=\"right\">").append(con.getMessagesSent());
totalSend += con.getMessagesSent();
buf.append("</td><td class=\"cells\" align=\"right\">").append(con.getMessagesReceived());
totalRecv += con.getMessagesReceived();
long outQueue = con.getOutboundQueueSize();
buf.append("</td><td class=\"cells\" align=\"center\">").append(outQueue);
buf.append("</td><td class=\"cells\" align=\"center\">");
if (con.isBacklogged())
buf.append("✓");
else
buf.append(" ");
//long readTime = con.getReadTime();
//if (readTime <= 0) {
// buf.append("</td> <td class=\"cells\" align=\"center\">0");
//} else {
// buf.append("</td> <td class=\"cells\" align=\"center\">").append(DataHelper.formatDuration(readTime));
//}
buf.append("</td></tr>\n");
out.write(buf.toString());
buf.setLength(0);
}
if (!peers.isEmpty()) {
// buf.append("<tr> <td colspan=\"11\"><hr></td></tr>\n");
buf.append("<tr class=\"tablefooter\"><td colspan=\"4\" align=\"left\"><b>")
.append(ngettext("{0} peer", "{0} peers", peers.size()));
buf.append("</b></td><td align=\"center\"><b>").append(formatRate(bpsRecv/1024)).append(THINSP).append(formatRate(bpsSend/1024)).append("</b>");
buf.append("</td><td align=\"center\"><b>").append(DataHelper.formatDuration2(totalUptime/peers.size()));
buf.append("</b></td><td align=\"center\"><b>").append(DataHelper.formatDuration2(offsetTotal*1000/peers.size()));
buf.append("</b></td><td align=\"center\"><b>").append(totalSend).append("</b></td><td align=\"center\"><b>").append(totalRecv);
buf.append("</b></td><td> </td><td> </td></tr>\n");
}
buf.append("</table>\n");
out.write(buf.toString());
buf.setLength(0);
}
private static final NumberFormat _rateFmt = new DecimalFormat("#,##0.00");
private static String formatRate(float rate) {
synchronized (_rateFmt) { return _rateFmt.format(rate); }
}
private Comparator<NTCPConnection> getComparator(int sortFlags) {
Comparator<NTCPConnection> rv = null;
switch (Math.abs(sortFlags)) {
default:
rv = AlphaComparator.instance();
}
if (sortFlags < 0)
rv = Collections.reverseOrder(rv);
return rv;
}
private static class AlphaComparator extends PeerComparator {
private static final AlphaComparator _instance = new AlphaComparator();
public static final AlphaComparator instance() { return _instance; }
}
private static class PeerComparator implements Comparator<NTCPConnection>, Serializable {
public int compare(NTCPConnection l, NTCPConnection r) {
if (l == null || r == null)
throw new IllegalArgumentException();
// base64 retains binary ordering
// UM, no it doesn't, but close enough
return l.getRemotePeer().calculateHash().toBase64().compareTo(r.getRemotePeer().calculateHash().toBase64());
}
}
/**
* Cache the bid to reduce object churn
*/
private class SharedBid extends TransportBid {
public SharedBid(int ms) { super(); setLatencyMs(ms); }
@Override
public Transport getTransport() { return NTCPTransport.this; }
@Override
public String toString() { return "NTCP bid @ " + getLatencyMs(); }
}
}