package net.i2p.client.streaming.impl;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import net.i2p.I2PAppContext;
import net.i2p.I2PException;
import net.i2p.client.I2PSession;
import net.i2p.data.ByteArray;
import net.i2p.data.Destination;
import net.i2p.data.Hash;
import net.i2p.data.SessionKey;
import net.i2p.util.ConcurrentHashSet;
import net.i2p.util.ConvertToHash;
import net.i2p.util.LHMCache;
import net.i2p.util.Log;
import net.i2p.util.SimpleTimer2;
/**
* Coordinate all of the connections for a single local destination.
*
*
*/
class ConnectionManager {
private final I2PAppContext _context;
private final Log _log;
private final I2PSession _session;
private final MessageHandler _messageHandler;
private final PacketHandler _packetHandler;
private final ConnectionHandler _connectionHandler;
private final PacketQueue _outboundQueue;
private final SchedulerChooser _schedulerChooser;
private final ConnectionPacketHandler _conPacketHandler;
private final TCBShare _tcbShare;
/** Inbound stream ID (Long) to Connection map */
private final ConcurrentHashMap<Long, Connection> _connectionByInboundId;
/** Ping ID (Long) to PingRequest */
private final ConcurrentHashMap<Long, PingRequest> _pendingPings;
private volatile boolean _throttlersInitialized;
private final ConnectionOptions _defaultOptions;
private final AtomicInteger _numWaiting = new AtomicInteger();
private long _soTimeout;
private volatile ConnThrottler _minuteThrottler;
private volatile ConnThrottler _hourThrottler;
private volatile ConnThrottler _dayThrottler;
/** since 0.9, each manager instantiates its own timer */
private final SimpleTimer2 _timer;
private final Map<Long, Object> _recentlyClosed;
private static final Object DUMMY = new Object();
/** cache of the property to detect changes */
private static volatile String _currentBlacklist = "";
private static final Set<Hash> _globalBlacklist = new ConcurrentHashSet<Hash>();
/** @since 0.9.3 */
public static final String PROP_BLACKLIST = "i2p.streaming.blacklist";
private static final long MAX_PING_TIMEOUT = 5*60*1000;
private static final int MAX_PONG_PAYLOAD = 32;
/**
* Manage all conns for this session
*/
public ConnectionManager(I2PAppContext context, I2PSession session, ConnectionOptions defaultOptions) {
_context = context;
_session = session;
_defaultOptions = defaultOptions;
_log = _context.logManager().getLog(ConnectionManager.class);
_connectionByInboundId = new ConcurrentHashMap<Long,Connection>(32);
_pendingPings = new ConcurrentHashMap<Long,PingRequest>(4);
_messageHandler = new MessageHandler(_context, this);
_packetHandler = new PacketHandler(_context, this);
_schedulerChooser = new SchedulerChooser(_context);
_conPacketHandler = new ConnectionPacketHandler(_context);
_timer = new RetransmissionTimer(_context, "Streaming Timer " +
session.getMyDestination().calculateHash().toBase64().substring(0, 4));
_connectionHandler = new ConnectionHandler(_context, this, _timer);
_tcbShare = new TCBShare(_context, _timer);
// PROTO_ANY is for backward compatibility (pre-0.7.1)
// TODO change proto to PROTO_STREAMING someday.
// Right now we get everything, and rely on Datagram to specify PROTO_UDP.
// PacketQueue has sent PROTO_STREAMING since the beginning of mux support (0.7.1)
// As of 0.9.1, new option to enforce streaming protocol, off by default
// As of 0.9.1, listen on configured port (default 0 = all)
int protocol = defaultOptions.getEnforceProtocol() ? I2PSession.PROTO_STREAMING : I2PSession.PROTO_ANY;
_session.addMuxedSessionListener(_messageHandler, protocol, defaultOptions.getLocalPort());
_outboundQueue = new PacketQueue(_context, _timer);
_recentlyClosed = new LHMCache<Long, Object>(32);
/** Socket timeout for accept() */
_soTimeout = -1;
// Stats for this class
_context.statManager().createRateStat("stream.con.lifetimeMessagesSent", "How many messages do we send on a stream?", "Stream", new long[] { 60*60*1000, 24*60*60*1000 });
_context.statManager().createRateStat("stream.con.lifetimeMessagesReceived", "How many messages do we receive on a stream?", "Stream", new long[] { 60*60*1000, 24*60*60*1000 });
_context.statManager().createRateStat("stream.con.lifetimeBytesSent", "How many bytes do we send on a stream?", "Stream", new long[] { 60*60*1000, 24*60*60*1000 });
_context.statManager().createRateStat("stream.con.lifetimeBytesReceived", "How many bytes do we receive on a stream?", "Stream", new long[] { 60*60*1000, 24*60*60*1000 });
_context.statManager().createRateStat("stream.con.lifetimeDupMessagesSent", "How many duplicate messages do we send on a stream?", "Stream", new long[] { 60*60*1000, 24*60*60*1000 });
_context.statManager().createRateStat("stream.con.lifetimeDupMessagesReceived", "How many duplicate messages do we receive on a stream?", "Stream", new long[] { 60*60*1000, 24*60*60*1000 });
_context.statManager().createRateStat("stream.con.lifetimeRTT", "What is the final RTT when a stream closes?", "Stream", new long[] { 60*60*1000, 24*60*60*1000 });
_context.statManager().createRateStat("stream.con.lifetimeCongestionSeenAt", "When was the last congestion seen at when a stream closes?", "Stream", new long[] { 60*60*1000, 24*60*60*1000 });
_context.statManager().createRateStat("stream.con.lifetimeSendWindowSize", "What is the final send window size when a stream closes?", "Stream", new long[] { 60*60*1000, 24*60*60*1000 });
_context.statManager().createRateStat("stream.receiveActive", "How many streams are active when a new one is received (period being not yet dropped)", "Stream", new long[] { 60*60*1000, 24*60*60*1000 });
// Stats for Connection
_context.statManager().createRateStat("stream.con.windowSizeAtCongestion", "How large was our send window when we send a dup?", "Stream", new long[] { 60*60*1000 });
_context.statManager().createRateStat("stream.chokeSizeBegin", "How many messages were outstanding when we started to choke?", "Stream", new long[] { 60*60*1000 });
_context.statManager().createRateStat("stream.chokeSizeEnd", "How many messages were outstanding when we stopped being choked?", "Stream", new long[] { 60*60*1000 });
_context.statManager().createRateStat("stream.fastRetransmit", "How long a packet has been around for if it has been resent per the fast retransmit timer?", "Stream", new long[] { 10*60*1000 });
// Stats for PacketQueue
_context.statManager().createRateStat("stream.con.sendMessageSize", "Size of a message sent on a connection", "Stream", new long[] { 10*60*1000, 60*60*1000 });
_context.statManager().createRateStat("stream.con.sendDuplicateSize", "Size of a message resent on a connection", "Stream", new long[] { 10*60*1000, 60*60*1000 });
}
Connection getConnectionByInboundId(long id) {
return _connectionByInboundId.get(Long.valueOf(id));
}
/**
* not guaranteed to be unique, but in case we receive more than one packet
* on an inbound connection that we havent ack'ed yet...
*/
Connection getConnectionByOutboundId(long id) {
for (Connection con : _connectionByInboundId.values()) {
if (con.getSendStreamId() == id)
return con;
}
return null;
}
/**
* Was this conn recently closed?
* @since 0.9.12
*/
public boolean wasRecentlyClosed(long inboundID) {
synchronized(_recentlyClosed) {
// use get() instead of containsKey() to update LRU access order,
// as we may get additional packets with the same ID
return _recentlyClosed.get(Long.valueOf(inboundID)) != null;
}
}
/**
* Set the socket accept() timeout.
* @param x
*/
public void setSoTimeout(long x) {
_soTimeout = x;
}
/**
* Get the socket accept() timeout.
* @return accept timeout in ms.
*/
public long getSoTimeout() {
return _soTimeout;
}
public void setAllowIncomingConnections(boolean allow) {
_connectionHandler.setActive(allow);
if (allow) {
synchronized(this) {
if (!_throttlersInitialized) {
updateOptions();
_throttlersInitialized = true;
}
}
}
}
/*
* Update the throttler options
* @since 0.9.3
*/
public synchronized void updateOptions() {
if ((_defaultOptions.getMaxConnsPerMinute() > 0 || _defaultOptions.getMaxTotalConnsPerMinute() > 0) &&
_minuteThrottler == null) {
_context.statManager().createRateStat("stream.con.throttledMinute", "Dropped for conn limit", "Stream", new long[] { 5*60*1000 });
_minuteThrottler = new ConnThrottler(_defaultOptions.getMaxConnsPerMinute(), _defaultOptions.getMaxTotalConnsPerMinute(),
60*1000, _timer);
} else if (_minuteThrottler != null) {
_minuteThrottler.updateLimits(_defaultOptions.getMaxConnsPerMinute(), _defaultOptions.getMaxTotalConnsPerMinute());
}
if ((_defaultOptions.getMaxConnsPerHour() > 0 || _defaultOptions.getMaxTotalConnsPerHour() > 0) &&
_hourThrottler == null) {
_context.statManager().createRateStat("stream.con.throttledHour", "Dropped for conn limit", "Stream", new long[] { 5*60*1000 });
_hourThrottler = new ConnThrottler(_defaultOptions.getMaxConnsPerHour(), _defaultOptions.getMaxTotalConnsPerHour(),
60*60*1000, _timer);
} else if (_hourThrottler != null) {
_hourThrottler.updateLimits(_defaultOptions.getMaxConnsPerHour(), _defaultOptions.getMaxTotalConnsPerHour());
}
if ((_defaultOptions.getMaxConnsPerDay() > 0 || _defaultOptions.getMaxTotalConnsPerDay() > 0) &&
_dayThrottler == null) {
_context.statManager().createRateStat("stream.con.throttledDay", "Dropped for conn limit", "Stream", new long[] { 5*60*1000 });
_dayThrottler = new ConnThrottler(_defaultOptions.getMaxConnsPerDay(), _defaultOptions.getMaxTotalConnsPerDay(),
24*60*60*1000, _timer);
} else if (_dayThrottler != null) {
_dayThrottler.updateLimits(_defaultOptions.getMaxConnsPerDay(), _defaultOptions.getMaxTotalConnsPerDay());
}
}
/** @return if we should accept connections */
public boolean getAllowIncomingConnections() {
return _connectionHandler.getActive();
}
/**
* Create a new connection based on the SYN packet we received.
*
* @param synPacket SYN packet to process
* @return created Connection with the packet's data already delivered to
* it, or null if the syn's streamId was already taken
*/
public Connection receiveConnection(Packet synPacket) {
ConnectionOptions opts = new ConnectionOptions(_defaultOptions);
opts.setPort(synPacket.getRemotePort());
opts.setLocalPort(synPacket.getLocalPort());
Connection con = new Connection(_context, this, synPacket.getSession(), _schedulerChooser,
_timer, _outboundQueue, _conPacketHandler, opts, true);
_tcbShare.updateOptsFromShare(con);
boolean reject = false;
int active = 0;
int total = 0;
// just for the stat
//total = _connectionByInboundId.size();
//for (Iterator iter = _connectionByInboundId.values().iterator(); iter.hasNext(); ) {
// if ( ((Connection)iter.next()).getIsConnected() )
// active++;
//}
if (locked_tooManyStreams()) {
if ((!_defaultOptions.getDisableRejectLogging()) || _log.shouldLog(Log.WARN))
_log.logAlways(Log.WARN, "Refusing connection since we have exceeded our max of "
+ _defaultOptions.getMaxConns() + " connections");
reject = true;
} else {
// this may not be right if more than one is enabled
String why = shouldRejectConnection(synPacket);
if (why != null) {
if ((!_defaultOptions.getDisableRejectLogging()) || _log.shouldLog(Log.WARN))
_log.logAlways(Log.WARN, "Refusing connection since peer is " + why +
(synPacket.getOptionalFrom() == null ? "" : ": " + synPacket.getOptionalFrom().toBase32()));
reject = true;
} else {
assignReceiveStreamId(con);
}
}
_context.statManager().addRateData("stream.receiveActive", active, total);
if (reject) {
Destination from = synPacket.getOptionalFrom();
if (from == null)
return null;
if (_dayThrottler != null || _hourThrottler != null) {
Hash h = from.calculateHash();
if ((_hourThrottler != null && _hourThrottler.isThrottled(h)) ||
(_dayThrottler != null && _dayThrottler.isThrottled(h)) ||
_globalBlacklist.contains(h) ||
(_defaultOptions.isAccessListEnabled() && !_defaultOptions.getAccessList().contains(h)) ||
(_defaultOptions.isBlacklistEnabled() && _defaultOptions.getBlacklist().contains(h))) {
// A signed RST packet + ElGamal + session tags is fairly expensive, so
// once the hour/day limit is hit for a particular peer, don't even send it.
// Ditto for blacklist / whitelist
// This is a tradeoff, because it will keep retransmitting the SYN for a while,
// thus more inbound, but let's not spend several KB on the outbound.
if (!Boolean.valueOf(_context.getProperty("i2p.streaming.sendResetOnBlock"))) {
// this is the default. Set property to send reset for debugging.
if (_log.shouldLog(Log.INFO))
_log.info("Dropping RST to " + h);
return null;
}
}
}
PacketLocal reply = new PacketLocal(_context, from, synPacket.getSession());
reply.setFlag(Packet.FLAG_RESET);
reply.setFlag(Packet.FLAG_SIGNATURE_INCLUDED);
reply.setAckThrough(synPacket.getSequenceNum());
reply.setSendStreamId(synPacket.getReceiveStreamId());
reply.setReceiveStreamId(0);
reply.setOptionalFrom();
reply.setLocalPort(synPacket.getLocalPort());
reply.setRemotePort(synPacket.getRemotePort());
// this just sends the packet - no retries or whatnot
_outboundQueue.enqueue(reply);
return null;
}
// finally, we know enough that we can log the packet with the conn filled in
if (I2PSocketManagerFull.pcapWriter != null &&
_context.getBooleanProperty(I2PSocketManagerFull.PROP_PCAP))
synPacket.logTCPDump(con);
try {
// This validates the packet, and sets the con's SendStreamID and RemotePeer
con.getPacketHandler().receivePacket(synPacket, con);
} catch (I2PException ie) {
_connectionByInboundId.remove(Long.valueOf(con.getReceiveStreamId()));
return null;
}
_context.statManager().addRateData("stream.connectionReceived", 1);
return con;
}
/**
* Process a ping by checking for throttling, etc., then sending a pong.
*
* @param con null if unknown
* @param ping Ping packet to process, must have From and Sig fields,
* with signature already verified, only if answerPings() returned true
* @return true if we sent a pong
* @since 0.9.12 from PacketHandler.receivePing()
*/
public boolean receivePing(Connection con, Packet ping) {
Destination dest = ping.getOptionalFrom();
if (dest == null)
return false;
if (con == null) {
// Use the same throttling as for connections
String why = shouldRejectConnection(ping);
if (why != null) {
if ((!_defaultOptions.getDisableRejectLogging()) || _log.shouldLog(Log.WARN))
_log.logAlways(Log.WARN, "Dropping ping since peer is " + why + ": " + dest.calculateHash());
return false;
}
} else {
// in-connection ping to a 3rd party ???
if (!dest.equals(con.getRemotePeer())) {
_log.logAlways(Log.WARN, "Dropping ping from " + con.getRemotePeer().calculateHash() +
" to " + dest.calculateHash());
return false;
}
}
PacketLocal pong = new PacketLocal(_context, dest, ping.getSession());
pong.setFlag(Packet.FLAG_ECHO | Packet.FLAG_NO_ACK);
pong.setReceiveStreamId(ping.getSendStreamId());
pong.setLocalPort(ping.getLocalPort());
pong.setRemotePort(ping.getRemotePort());
// as of 0.9.18, return the payload
ByteArray payload = ping.getPayload();
if (payload != null) {
if (payload.getValid() > MAX_PONG_PAYLOAD)
payload.setValid(MAX_PONG_PAYLOAD);
pong.setPayload(payload);
}
_outboundQueue.enqueue(pong);
return true;
}
/**
* Pick a new random stream ID for the con and assign it,
* taking care to avoid duplicates, and put it in the connection table.
*
* @since 0.9.12 consolidated from receiveConnection() and connect()
*/
private void assignReceiveStreamId(Connection con) {
long receiveId;
synchronized(_recentlyClosed) {
Long rcvID;
do {
receiveId = _context.random().nextLong(Packet.MAX_STREAM_ID-1)+1;
rcvID = Long.valueOf(receiveId);
} while (_recentlyClosed.containsKey(rcvID) ||
_pendingPings.containsKey(rcvID) ||
_connectionByInboundId.putIfAbsent(rcvID, con) != null);
}
con.setReceiveStreamId(receiveId);
}
/**
* Pick a new random stream ID for a ping and assign it,
* taking care to avoid duplicates, and return it.
*
* @since 0.9.12
*/
private long assignPingId(PingRequest req) {
long receiveId;
synchronized(_recentlyClosed) {
Long rcvID;
do {
receiveId = _context.random().nextLong(Packet.MAX_STREAM_ID-1)+1;
rcvID = Long.valueOf(receiveId);
} while (_recentlyClosed.containsKey(rcvID) ||
_connectionByInboundId.containsKey(rcvID) ||
_pendingPings.putIfAbsent(rcvID, req) != null);
}
return receiveId;
}
private static final long DEFAULT_STREAM_DELAY_MAX = 10*1000;
/**
* Build a new connection to the given peer. This blocks if there is no
* connection delay, otherwise it returns immediately.
*
* @param peer Destination to contact, non-null
* @param opts Connection's options
* @param session generally the session from the constructor, but could be a subsession
* @return new connection, or null if we have exceeded our limit
*/
public Connection connect(Destination peer, ConnectionOptions opts, I2PSession session) {
if (peer == null)
throw new NullPointerException();
Connection con = null;
long expiration = _context.clock().now();
long tmout = opts.getConnectTimeout();
if (tmout <= 0)
expiration += DEFAULT_STREAM_DELAY_MAX;
else
expiration += tmout;
_numWaiting.incrementAndGet();
while (true) {
long remaining = expiration - _context.clock().now();
if (remaining <= 0) {
_log.logAlways(Log.WARN, "Refusing to connect since we have exceeded our max of "
+ _defaultOptions.getMaxConns() + " connections");
_numWaiting.decrementAndGet();
return null;
}
if (locked_tooManyStreams()) {
int max = _defaultOptions.getMaxConns();
// allow a full buffer of pending/waiting streams
if (_numWaiting.get() > max) {
_log.logAlways(Log.WARN, "Refusing connection since we have exceeded our max of "
+ max + " and there are " + _numWaiting
+ " waiting already");
_numWaiting.decrementAndGet();
return null;
}
// no remaining streams, lets wait a bit
// got rid of the lock, so just sleep (fixme?)
// try { _connectionLock.wait(remaining); } catch (InterruptedException ie) {}
try { Thread.sleep(remaining/4); } catch (InterruptedException ie) {}
} else {
con = new Connection(_context, this, session, _schedulerChooser, _timer,
_outboundQueue, _conPacketHandler, opts, false);
con.setRemotePeer(peer);
assignReceiveStreamId(con);
break; // stop looping as a psuedo-wait
}
}
// ok we're in...
con.eventOccurred();
if (_log.shouldLog(Log.DEBUG))
_log.debug("Connect() conDelay = " + opts.getConnectDelay());
if (opts.getConnectDelay() <= 0) {
con.waitForConnect();
}
// safe decrement
for (;;) {
int n = _numWaiting.get();
if (n <= 0)
break;
if (_numWaiting.compareAndSet(n, n - 1))
break;
}
_context.statManager().addRateData("stream.connectionCreated", 1);
return con;
}
/**
* Doesn't need to be locked any more
* @return too many
*/
private boolean locked_tooManyStreams() {
int max = _defaultOptions.getMaxConns();
if (max <= 0) return false;
int size = _connectionByInboundId.size();
if (size < max) return false;
// count both so we can break out of the for loop asap
int active = 0;
int inactive = 0;
int maxInactive = size - max;
for (Connection con : _connectionByInboundId.values()) {
// ticket #1039
if (con.getIsConnected() &&
!(con.getCloseSentOn() > 0 && con.getCloseReceivedOn() > 0)) {
if (++active >= max)
return true;
} else {
if (++inactive > maxInactive)
return false;
}
}
//if ( (_connectionByInboundId.size() > 100) && (_log.shouldLog(Log.INFO)) )
// _log.info("More than 100 connections! " + active
// + " total: " + _connectionByInboundId.size());
return false;
}
/**
* @return reason string or null if not rejected
*/
private String shouldRejectConnection(Packet syn) {
// unfortunately we don't have access to the router client manager here,
// so we can't whitelist local access
Destination from = syn.getOptionalFrom();
if (from == null)
return "null";
Hash h = from.calculateHash();
// As of 0.9.9, run the blacklist checks BEFORE the port counters,
// so blacklisted dests will not increment the counters and
// possibly trigger total-counter blocks for others.
// if the sig is absent or bad it will be caught later (in CPH)
String hashes = _context.getProperty(PROP_BLACKLIST, "");
if (!_currentBlacklist.equals(hashes)) {
// rebuild _globalBlacklist when property changes
synchronized(_globalBlacklist) {
if (hashes.length() > 0) {
Set<Hash> newSet = new HashSet<Hash>();
StringTokenizer tok = new StringTokenizer(hashes, ",; ");
while (tok.hasMoreTokens()) {
String hashstr = tok.nextToken();
Hash hh = ConvertToHash.getHash(hashstr);
if (hh != null)
newSet.add(hh);
else
_log.error("Bad blacklist entry: " + hashstr);
}
_globalBlacklist.addAll(newSet);
_globalBlacklist.retainAll(newSet);
_currentBlacklist = hashes;
} else {
_globalBlacklist.clear();
_currentBlacklist = "";
}
}
}
if (hashes.length() > 0 && _globalBlacklist.contains(h))
return "blacklisted globally";
if (_defaultOptions.isAccessListEnabled() &&
!_defaultOptions.getAccessList().contains(h))
return "not whitelisted";
if (_defaultOptions.isBlacklistEnabled() &&
_defaultOptions.getBlacklist().contains(h))
return "blacklisted";
if (_dayThrottler != null && _dayThrottler.shouldThrottle(h)) {
_context.statManager().addRateData("stream.con.throttledDay", 1);
if (_defaultOptions.getMaxConnsPerDay() <= 0)
return "throttled by" +
" total limit of " + _defaultOptions.getMaxTotalConnsPerDay() +
" per day";
else if (_defaultOptions.getMaxTotalConnsPerDay() <= 0)
return "throttled by per-peer limit of " + _defaultOptions.getMaxConnsPerDay() +
" per day";
else
return "throttled by per-peer limit of " + _defaultOptions.getMaxConnsPerDay() +
" or total limit of " + _defaultOptions.getMaxTotalConnsPerDay() +
" per day";
}
if (_hourThrottler != null && _hourThrottler.shouldThrottle(h)) {
_context.statManager().addRateData("stream.con.throttledHour", 1);
if (_defaultOptions.getMaxConnsPerHour() <= 0)
return "throttled by" +
" total limit of " + _defaultOptions.getMaxTotalConnsPerHour() +
" per hour";
else if (_defaultOptions.getMaxTotalConnsPerHour() <= 0)
return "throttled by per-peer limit of " + _defaultOptions.getMaxConnsPerHour() +
" per hour";
else
return "throttled by per-peer limit of " + _defaultOptions.getMaxConnsPerHour() +
" or total limit of " + _defaultOptions.getMaxTotalConnsPerHour() +
" per hour";
}
if (_minuteThrottler != null && _minuteThrottler.shouldThrottle(h)) {
_context.statManager().addRateData("stream.con.throttledMinute", 1);
if (_defaultOptions.getMaxConnsPerMinute() <= 0)
return "throttled by" +
" total limit of " + _defaultOptions.getMaxTotalConnsPerMinute() +
" per minute";
else if (_defaultOptions.getMaxTotalConnsPerMinute() <= 0)
return "throttled by per-peer limit of " + _defaultOptions.getMaxConnsPerMinute() +
" per minute";
else
return "throttled by per-peer limit of " + _defaultOptions.getMaxConnsPerMinute() +
" or total limit of " + _defaultOptions.getMaxTotalConnsPerMinute() +
" per minute";
}
return null;
}
public MessageHandler getMessageHandler() { return _messageHandler; }
public PacketHandler getPacketHandler() { return _packetHandler; }
/**
* This is the primary session only
*/
public I2PSession getSession() { return _session; }
public void updateOptsFromShare(Connection con) { _tcbShare.updateOptsFromShare(con); }
public void updateShareOpts(Connection con) { _tcbShare.updateShareOpts(con); }
// Both of these methods are
// exporting non-public type through public API, this is a potential bug.
public ConnectionHandler getConnectionHandler() { return _connectionHandler; }
public PacketQueue getPacketQueue() { return _outboundQueue; }
/** do we respond to pings that aren't on an existing connection? */
public boolean answerPings() { return _defaultOptions.getAnswerPings(); }
/**
* Something b0rked hard, so kill all of our connections without mercy.
* Don't bother sending close packets.
* This will not close the ServerSocket.
* This will not kill the timer threads.
*
* CAN continue to use the manager.
*/
public void disconnectAllHard() {
//if (_log.shouldLog(Log.INFO))
// _log.info("ConnMan hard disconnect", new Exception("I did it"));
for (Iterator<Connection> iter = _connectionByInboundId.values().iterator(); iter.hasNext(); ) {
Connection con = iter.next();
con.disconnect(false, false);
iter.remove();
}
synchronized(_recentlyClosed) {
_recentlyClosed.clear();
}
_pendingPings.clear();
// FIXME
// Ideally we would like to stop all TCBShare and all the timer threads here,
// but leave them ready to restart when things resume.
// However that's quite difficult.
// So the timer threads will continue to run.
}
/**
* Kill all connections and the timers.
* Don't bother sending close packets.
* As of 0.9.17, this will close the ServerSocket, killing one thread in accept().
*
* CANNOT continue to use the manager or restart.
*
* @since 0.9.7
*/
public void shutdown() {
//if (_log.shouldLog(Log.INFO))
// _log.info("ConnMan shutdown", new Exception("I did it"));
disconnectAllHard();
_tcbShare.stop();
_timer.stop();
_outboundQueue.close();
_connectionHandler.setActive(false);
}
/**
* Drop the (already closed) connection on the floor.
*
* @param con Connection to drop.
*/
public void removeConnection(Connection con) {
Long rcvID = Long.valueOf(con.getReceiveStreamId());
synchronized(_recentlyClosed) {
_recentlyClosed.put(rcvID, DUMMY);
}
Object o = _connectionByInboundId.remove(Long.valueOf(con.getReceiveStreamId()));
boolean removed = (o == con);
if (_log.shouldLog(Log.DEBUG))
_log.debug("Connection removed? " + removed + " remaining: "
+ _connectionByInboundId.size() + ": " + con);
if (!removed && _log.shouldLog(Log.DEBUG))
_log.debug("Failed to remove " + con +"\n" + _connectionByInboundId.values());
if (removed) {
_context.statManager().addRateData("stream.con.lifetimeMessagesSent", 1+con.getLastSendId(), con.getLifetime());
MessageInputStream stream = con.getInputStream();
long rcvd = 1 + stream.getHighestBlockId();
long nacks[] = stream.getNacks();
if (nacks != null)
rcvd -= nacks.length;
_context.statManager().addRateData("stream.con.lifetimeMessagesReceived", rcvd, con.getLifetime());
_context.statManager().addRateData("stream.con.lifetimeBytesSent", con.getLifetimeBytesSent(), con.getLifetime());
_context.statManager().addRateData("stream.con.lifetimeBytesReceived", con.getLifetimeBytesReceived(), con.getLifetime());
_context.statManager().addRateData("stream.con.lifetimeDupMessagesSent", con.getLifetimeDupMessagesSent(), con.getLifetime());
_context.statManager().addRateData("stream.con.lifetimeDupMessagesReceived", con.getLifetimeDupMessagesReceived(), con.getLifetime());
_context.statManager().addRateData("stream.con.lifetimeRTT", con.getOptions().getRTT(), con.getLifetime());
_context.statManager().addRateData("stream.con.lifetimeCongestionSeenAt", con.getLastCongestionSeenAt(), con.getLifetime());
_context.statManager().addRateData("stream.con.lifetimeSendWindowSize", con.getOptions().getWindowSize(), con.getLifetime());
if (I2PSocketManagerFull.pcapWriter != null)
I2PSocketManagerFull.pcapWriter.flush();
}
}
/** return a set of Connection objects
* @return set of Connection objects
*/
public Set<Connection> listConnections() {
return new HashSet<Connection>(_connectionByInboundId.values());
}
/**
* blocking
*
* @param timeoutMs greater than zero
* @return true if pong received
*/
public boolean ping(Destination peer, long timeoutMs) {
return ping(peer, 0, 0, timeoutMs, true, null);
}
/**
* blocking
*
* @param timeoutMs greater than zero
* @return true if pong received
* @since 0.9.12 added port args
*/
public boolean ping(Destination peer, int fromPort, int toPort, long timeoutMs) {
return ping(peer, fromPort, toPort, timeoutMs, true, null);
}
/**
* @param timeoutMs greater than zero
* @return true if blocking and pong received
* @since 0.9.12 added port args
*/
public boolean ping(Destination peer, int fromPort, int toPort, long timeoutMs, boolean blocking) {
return ping(peer, fromPort, toPort, timeoutMs, blocking, null);
}
/**
* @param timeoutMs greater than zero
* @param notifier may be null
* @return true if blocking and pong received
* @since 0.9.12 added port args
*/
public boolean ping(Destination peer, int fromPort, int toPort, long timeoutMs,
boolean blocking, PingNotifier notifier) {
PingRequest req = new PingRequest(notifier);
long id = assignPingId(req);
PacketLocal packet = new PacketLocal(_context, peer, _session);
packet.setSendStreamId(id);
packet.setFlag(Packet.FLAG_ECHO |
Packet.FLAG_NO_ACK |
Packet.FLAG_SIGNATURE_INCLUDED);
packet.setOptionalFrom();
packet.setLocalPort(fromPort);
packet.setRemotePort(toPort);
if (timeoutMs > MAX_PING_TIMEOUT)
timeoutMs = MAX_PING_TIMEOUT;
if (_log.shouldLog(Log.INFO)) {
_log.info(String.format("about to ping %s port %d from port %d timeout=%d blocking=%b",
peer.calculateHash().toString(), toPort, fromPort, timeoutMs, blocking));
}
_outboundQueue.enqueue(packet);
packet.releasePayload();
if (blocking) {
synchronized (req) {
if (!req.pongReceived())
try { req.wait(timeoutMs); } catch (InterruptedException ie) {}
}
_pendingPings.remove(id);
} else {
PingFailed pf = new PingFailed(id, notifier);
pf.schedule(timeoutMs);
}
boolean ok = req.pongReceived();
return ok;
}
/**
* blocking
*
* @param timeoutMs greater than zero
* @param payload non-null, include in packet, up to 32 bytes may be returned in pong
* not copied, do not modify
* @return the payload received in the pong, zero-length if none, null on failure or timeout
* @since 0.9.18
*/
public byte[] ping(Destination peer, int fromPort, int toPort, long timeoutMs,
byte[] payload) {
PingRequest req = new PingRequest(null);
long id = assignPingId(req);
PacketLocal packet = new PacketLocal(_context, peer, _session);
packet.setSendStreamId(id);
packet.setFlag(Packet.FLAG_ECHO |
Packet.FLAG_NO_ACK |
Packet.FLAG_SIGNATURE_INCLUDED);
packet.setOptionalFrom();
packet.setLocalPort(fromPort);
packet.setRemotePort(toPort);
packet.setPayload(new ByteArray(payload));
if (timeoutMs > MAX_PING_TIMEOUT)
timeoutMs = MAX_PING_TIMEOUT;
if (_log.shouldLog(Log.INFO)) {
_log.info(String.format("about to ping %s port %d from port %d timeout=%d payload=%d",
peer.calculateHash().toString(), toPort, fromPort, timeoutMs, payload.length));
}
_outboundQueue.enqueue(packet);
packet.releasePayload();
synchronized (req) {
if (!req.pongReceived())
try { req.wait(timeoutMs); } catch (InterruptedException ie) {}
}
_pendingPings.remove(id);
boolean ok = req.pongReceived();
if (!ok)
return null;
ByteArray ba = req.getPayload();
if (ba == null)
return new byte[0];
byte[] rv = new byte[ba.getValid()];
System.arraycopy(ba, ba.getOffset(), rv, 0, ba.getValid());
return rv;
}
/**
* The callback interface for a pong.
* Unused? Not part of the public streaming API.
*/
public interface PingNotifier {
/**
* @param ok true if pong received; false if timed out
*/
public void pingComplete(boolean ok);
}
private class PingFailed extends SimpleTimer2.TimedEvent {
private final Long _id;
private final PingNotifier _notifier;
public PingFailed(Long id, PingNotifier notifier) {
super(_timer);
_id = id;
_notifier = notifier;
}
public void timeReached() {
PingRequest pr = _pendingPings.remove(_id);
if (pr != null) {
if (_notifier != null)
_notifier.pingComplete(false);
if (_log.shouldLog(Log.INFO))
_log.info("Ping failed");
}
}
}
private static class PingRequest {
private boolean _ponged;
private ByteArray _payload;
private final PingNotifier _notifier;
/** @param notifier may be null */
public PingRequest(PingNotifier notifier) {
_notifier = notifier;
}
/**
* @param payload may be null
*/
public void pong(ByteArray payload) {
// static, no log
//_log.debug("Ping successful");
//_context.sessionKeyManager().tagsDelivered(_peer.getPublicKey(), _packet.getKeyUsed(), _packet.getTagsSent());
synchronized (this) {
_ponged = true;
_payload = payload;
notifyAll();
}
if (_notifier != null)
_notifier.pingComplete(true);
}
public synchronized boolean pongReceived() { return _ponged; }
/**
* @return null if no payload or no pong received
* @since 0.9.18
*/
public synchronized ByteArray getPayload() { return _payload; }
}
/**
* @param payload may be null
*/
void receivePong(long pingId, ByteArray payload) {
PingRequest req = _pendingPings.remove(Long.valueOf(pingId));
if (req != null)
req.pong(payload);
}
/**
* @since 0.9.21
*/
@Override
public String toString() {
return "ConnectionManager for " + _session;
}
}