package net.i2p.router.transport.udp;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
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.SessionKey;
import net.i2p.data.i2np.DatabaseStoreMessage;
import net.i2p.data.i2np.DeliveryStatusMessage;
import net.i2p.data.i2np.I2NPMessage;
import net.i2p.router.OutNetMessage;
import net.i2p.router.Router;
import net.i2p.router.RouterContext;
import net.i2p.router.transport.TransportUtil;
import net.i2p.router.transport.crypto.DHSessionKeyBuilder;
import static net.i2p.router.transport.udp.InboundEstablishState.InboundState.*;
import static net.i2p.router.transport.udp.OutboundEstablishState.OutboundState.*;
import net.i2p.router.util.DecayingHashSet;
import net.i2p.router.util.DecayingBloomFilter;
import net.i2p.util.Addresses;
import net.i2p.util.I2PThread;
import net.i2p.util.Log;
import net.i2p.util.VersionComparator;
/**
* Coordinate the establishment of new sessions - both inbound and outbound.
* This has its own thread to add packets to the packet queue when necessary,
* as well as to drop any failed establishment attempts.
*
*/
class EstablishmentManager {
private final RouterContext _context;
private final Log _log;
private final UDPTransport _transport;
private final PacketBuilder _builder;
private final int _networkID;
/** map of RemoteHostId to InboundEstablishState */
private final ConcurrentHashMap<RemoteHostId, InboundEstablishState> _inboundStates;
/**
* Map of RemoteHostId to OutboundEstablishState.
* The key could be either an IP/Port (for direct) or
* a Hash (for indirect, before the RelayResponse is received).
* Once the RelayResponse is received we change the key.
*/
private final ConcurrentHashMap<RemoteHostId, OutboundEstablishState> _outboundStates;
/** map of RemoteHostId to List of OutNetMessage for messages exceeding capacity */
private final ConcurrentHashMap<RemoteHostId, List<OutNetMessage>> _queuedOutbound;
/**
* Map of nonce (Long) to OutboundEstablishState.
* Only for indirect, before we receive the RelayResponse.
* This is so we can lookup state for the RelayResponse.
* After we receive the relay response, _outboundStates is keyed by actual IP.
*/
private final ConcurrentHashMap<Long, OutboundEstablishState> _liveIntroductions;
/**
* Map of claimed IP/port to OutboundEstablishState.
* Only for indirect, before we receive the RelayResponse.
* This is so we can lookup a pending introduction by IP
* even before we know the "real" IP, so we can match an inbound packet.
* After we receive the relay response, _outboundStates is keyed by actual IP.
*/
private final ConcurrentHashMap<RemoteHostId, OutboundEstablishState> _outboundByClaimedAddress;
/**
* Map of router hash to OutboundEstablishState.
* Only for indirect, after we receive the RelayResponse.
* This is so we can lookup a pending connection by Hash
* even after we've got the IP/port, so we can match a subsequent outbound packet.
* Before we receive the relay response, _outboundStates is keyed by hash.
*/
private final ConcurrentHashMap<Hash, OutboundEstablishState> _outboundByHash;
private volatile boolean _alive;
private final Object _activityLock;
private int _activity;
/** "bloom filter" */
private final DecayingBloomFilter _replayFilter;
/** max outbound in progress - max inbound is half of this */
private final int DEFAULT_MAX_CONCURRENT_ESTABLISH;
private static final int DEFAULT_LOW_MAX_CONCURRENT_ESTABLISH = 20;
private static final int DEFAULT_HIGH_MAX_CONCURRENT_ESTABLISH = 150;
private static final String PROP_MAX_CONCURRENT_ESTABLISH = "i2np.udp.maxConcurrentEstablish";
/** max pending outbound connections (waiting because we are at MAX_CONCURRENT_ESTABLISH) */
private static final int MAX_QUEUED_OUTBOUND = 50;
/** max queued msgs per peer while the peer connection is queued */
private static final int MAX_QUEUED_PER_PEER = 16;
private static final long MAX_NONCE = 0xFFFFFFFFl;
/**
* Kill any outbound that takes more than this.
* Two round trips (Req-Created-Confirmed-Data) for direct;
* 3 1/2 round trips (RReq-RResp+Intro-HolePunch-Req-Created-Confirmed-Data) for indirect.
* Note that this is way too long for us to be able to fall back to NTCP
* for individual messages unless the message timer fires first.
* But SSU probably isn't higher priority than NTCP.
* And it's important to not fail an establishment too soon and waste it.
*/
private static final int MAX_OB_ESTABLISH_TIME = 35*1000;
/**
* Kill any inbound that takes more than this
* One round trip (Created-Confirmed)
*/
private static final int MAX_IB_ESTABLISH_TIME = 20*1000;
/** max before receiving a response to a single message during outbound establishment */
public static final int OB_MESSAGE_TIMEOUT = 15*1000;
/** for the DSM and or netdb store */
private static final int DATA_MESSAGE_TIMEOUT = 10*1000;
/**
* Java I2P has always parsed the length of the extended options field,
* but i2pd hasn't recognized it until this release.
* No matter, the options weren't defined until this release anyway.
*
*/
private static final String VERSION_ALLOW_EXTENDED_OPTIONS = "0.9.24";
private static final String PROP_DISABLE_EXT_OPTS = "i2np.udp.disableExtendedOptions";
public EstablishmentManager(RouterContext ctx, UDPTransport transport) {
_context = ctx;
_log = ctx.logManager().getLog(EstablishmentManager.class);
_networkID = ctx.router().getNetworkID();
_transport = transport;
_builder = new PacketBuilder(ctx, transport);
_inboundStates = new ConcurrentHashMap<RemoteHostId, InboundEstablishState>();
_outboundStates = new ConcurrentHashMap<RemoteHostId, OutboundEstablishState>();
_queuedOutbound = new ConcurrentHashMap<RemoteHostId, List<OutNetMessage>>();
_liveIntroductions = new ConcurrentHashMap<Long, OutboundEstablishState>();
_outboundByClaimedAddress = new ConcurrentHashMap<RemoteHostId, OutboundEstablishState>();
_outboundByHash = new ConcurrentHashMap<Hash, OutboundEstablishState>();
_activityLock = new Object();
_replayFilter = new DecayingHashSet(ctx, 10*60*1000, 8, "SSU-DH-X");
DEFAULT_MAX_CONCURRENT_ESTABLISH = Math.max(DEFAULT_LOW_MAX_CONCURRENT_ESTABLISH,
Math.min(DEFAULT_HIGH_MAX_CONCURRENT_ESTABLISH,
ctx.bandwidthLimiter().getOutboundKBytesPerSecond() / 2));
_context.statManager().createRateStat("udp.inboundEstablishTime", "How long it takes for a new inbound session to be established", "udp", UDPTransport.RATES);
_context.statManager().createRateStat("udp.outboundEstablishTime", "How long it takes for a new outbound session to be established", "udp", UDPTransport.RATES);
//_context.statManager().createRateStat("udp.inboundEstablishFailedState", "What state a failed inbound establishment request fails in", "udp", UDPTransport.RATES);
//_context.statManager().createRateStat("udp.outboundEstablishFailedState", "What state a failed outbound establishment request fails in", "udp", UDPTransport.RATES);
_context.statManager().createRateStat("udp.sendIntroRelayRequest", "How often we send a relay request to reach a peer", "udp", UDPTransport.RATES);
_context.statManager().createRateStat("udp.sendIntroRelayTimeout", "How often a relay request times out before getting a response (due to the target or intro peer being offline)", "udp", UDPTransport.RATES);
_context.statManager().createRateStat("udp.receiveIntroRelayResponse", "How long it took to receive a relay response", "udp", UDPTransport.RATES);
_context.statManager().createRateStat("udp.establishDropped", "Dropped an inbound establish message", "udp", UDPTransport.RATES);
_context.statManager().createRateStat("udp.establishRejected", "How many pending outbound connections are there when we refuse to add any more?", "udp", UDPTransport.RATES);
_context.statManager().createRateStat("udp.establishOverflow", "How many messages were queued up on a pending connection when it was too much?", "udp", UDPTransport.RATES);
_context.statManager().createRateStat("udp.establishBadIP", "Received IP or port was bad", "udp", UDPTransport.RATES);
// following are for PeerState
_context.statManager().createRateStat("udp.congestionOccurred", "How large the cwin was when congestion occurred (duration == sendBps)", "udp", UDPTransport.RATES);
_context.statManager().createRateStat("udp.congestedRTO", "retransmission timeout after congestion (duration == rtt dev)", "udp", UDPTransport.RATES);
_context.statManager().createRateStat("udp.sendACKPartial", "Number of partial ACKs sent (duration == number of full ACKs in that ack packet)", "udp", UDPTransport.RATES);
_context.statManager().createRateStat("udp.sendBps", "How fast we are transmitting when a packet is acked", "udp", UDPTransport.RATES);
_context.statManager().createRateStat("udp.receiveBps", "How fast we are receiving when a packet is fully received (at most one per second)", "udp", UDPTransport.RATES);
_context.statManager().createRateStat("udp.mtuIncrease", "How many retransmissions have there been to the peer when the MTU was increased", "udp", UDPTransport.RATES);
_context.statManager().createRateStat("udp.mtuDecrease", "How many retransmissions have there been to the peer when the MTU was decreased", "udp", UDPTransport.RATES);
_context.statManager().createRateStat("udp.rejectConcurrentActive", "How many messages are currently being sent to the peer when we reject it (period is how many concurrent packets we allow)", "udp", UDPTransport.RATES);
_context.statManager().createRateStat("udp.allowConcurrentActive", "How many messages are currently being sent to the peer when we accept it (period is how many concurrent packets we allow)", "udp", UDPTransport.RATES);
_context.statManager().createRateStat("udp.rejectConcurrentSequence", "How many consecutive concurrency rejections have we had when we stop rejecting (period is how many concurrent packets we are on)", "udp", UDPTransport.RATES);
//_context.statManager().createRateStat("udp.queueDropSize", "How many messages were queued up when it was considered full, causing a tail drop?", "udp", UDPTransport.RATES);
//_context.statManager().createRateStat("udp.queueAllowTotalLifetime", "When a peer is retransmitting and we probabalistically allow a new message, what is the sum of the pending message lifetimes? (period is the new message's lifetime)?", "udp", UDPTransport.RATES);
_context.statManager().createRateStat("udp.dupDHX", "Session request replay", "udp", new long[] { 24*60*60*1000L } );
}
public synchronized void startup() {
_alive = true;
I2PThread t = new I2PThread(new Establisher(), "UDP Establisher", true);
t.start();
}
public synchronized void shutdown() {
_alive = false;
notifyActivity();
}
/**
* Grab the active establishing state
* @return null if none
*/
InboundEstablishState getInboundState(RemoteHostId from) {
InboundEstablishState state = _inboundStates.get(from);
// if ( (state == null) && (_log.shouldLog(Log.DEBUG)) )
// _log.debug("No inbound states for " + from + ", with remaining: " + _inboundStates);
return state;
}
/**
* Grab the active establishing state
* @return null if none
*/
OutboundEstablishState getOutboundState(RemoteHostId from) {
OutboundEstablishState state = _outboundStates.get(from);
if (state == null) {
state = _outboundByClaimedAddress.get(from);
if (state != null && _log.shouldLog(Log.INFO))
_log.info("Found by claimed address: " + state);
}
// if ( (state == null) && (_log.shouldLog(Log.DEBUG)) )
// _log.debug("No outbound states for " + from + ", with remaining: " + _outboundStates);
return state;
}
/**
* How many concurrent outbound sessions to deal with
*/
private int getMaxConcurrentEstablish() {
return _context.getProperty(PROP_MAX_CONCURRENT_ESTABLISH, DEFAULT_MAX_CONCURRENT_ESTABLISH);
}
/**
* Send the message to its specified recipient by establishing a connection
* with them and sending it off. This call does not block, and on failure,
* the message is failed.
*
* Note - if we go back to multiple PacketHandler threads, this may need more locking.
*/
public void establish(OutNetMessage msg) {
establish(msg, true);
}
/**
* @param queueIfMaxExceeded true normally, false if called from locked_admit so we don't loop
* @since 0.9.2
*/
private void establish(OutNetMessage msg, boolean queueIfMaxExceeded) {
RouterInfo toRouterInfo = msg.getTarget();
RouterAddress ra = _transport.getTargetAddress(toRouterInfo);
if (ra == null) {
_transport.failed(msg, "Remote peer has no address, cannot establish");
return;
}
RouterIdentity toIdentity = toRouterInfo.getIdentity();
Hash toHash = toIdentity.calculateHash();
if (toRouterInfo.getNetworkId() != _networkID) {
_context.banlist().banlistRouter(toHash);
_transport.markUnreachable(toHash);
_transport.failed(msg, "Remote peer is on the wrong network, cannot establish");
return;
}
UDPAddress addr = new UDPAddress(ra);
RemoteHostId maybeTo = null;
InetAddress remAddr = addr.getHostAddress();
int port = addr.getPort();
// check for validity and existing inbound state, using the
// claimed address (which we won't be using if indirect)
if (remAddr != null && port > 0 && port <= 65535) {
maybeTo = new RemoteHostId(remAddr.getAddress(), port);
if ((!_transport.isValid(maybeTo.getIP())) ||
(Arrays.equals(maybeTo.getIP(), _transport.getExternalIP()) && !_transport.allowLocal())) {
_transport.failed(msg, "Remote peer's IP isn't valid");
_transport.markUnreachable(toHash);
//_context.banlist().banlistRouter(msg.getTarget().getIdentity().calculateHash(), "Invalid SSU address", UDPTransport.STYLE);
_context.statManager().addRateData("udp.establishBadIP", 1);
return;
}
InboundEstablishState inState = _inboundStates.get(maybeTo);
if (inState != null) {
// we have an inbound establishment in progress, queue it there instead
synchronized (inState) {
switch (inState.getState()) {
case IB_STATE_UNKNOWN:
case IB_STATE_REQUEST_RECEIVED:
case IB_STATE_CREATED_SENT:
case IB_STATE_CONFIRMED_PARTIALLY:
case IB_STATE_CONFIRMED_COMPLETELY:
// queue it
inState.addMessage(msg);
if (_log.shouldLog(Log.WARN))
_log.debug("OB msg queued to IES");
break;
case IB_STATE_COMPLETE:
// race, send it out (but don't call _transport.send() again and risk a loop)
_transport.sendIfEstablished(msg);
break;
case IB_STATE_FAILED:
// race, failed
_transport.failed(msg, "OB msg failed during IB establish");
break;
}
}
return;
}
}
RemoteHostId to;
boolean isIndirect = addr.getIntroducerCount() > 0 || maybeTo == null;
if (isIndirect) {
to = new RemoteHostId(toHash);
} else {
to = maybeTo;
}
OutboundEstablishState state = null;
int deferred = 0;
boolean rejected = false;
int queueCount = 0;
state = _outboundStates.get(to);
if (state == null) {
state = _outboundByHash.get(toHash);
if (state != null && _log.shouldLog(Log.INFO))
_log.info("Found by hash: " + state);
}
if (state == null) {
if (queueIfMaxExceeded && _outboundStates.size() >= getMaxConcurrentEstablish()) {
if (_queuedOutbound.size() >= MAX_QUEUED_OUTBOUND && !_queuedOutbound.containsKey(to)) {
rejected = true;
} else {
List<OutNetMessage> newQueued = new ArrayList<OutNetMessage>(MAX_QUEUED_PER_PEER);
List<OutNetMessage> queued = _queuedOutbound.putIfAbsent(to, newQueued);
if (queued == null) {
queued = newQueued;
if (_log.shouldLog(Log.WARN))
_log.warn("Queueing outbound establish to " + to + ", increase " + PROP_MAX_CONCURRENT_ESTABLISH);
}
// this used to be inside a synchronized (_outboundStates) block,
// but that's now a CHM, so protect the ArrayList
// There are still races possible but this should prevent AIOOBE and NPE
synchronized (queued) {
queueCount = queued.size();
if (queueCount < MAX_QUEUED_PER_PEER) {
queued.add(msg);
// increment for the stat below
queueCount++;
} else {
rejected = true;
}
deferred = _queuedOutbound.size();
}
}
} else {
// must have a valid session key
byte[] keyBytes = addr.getIntroKey();
if (keyBytes == null) {
_transport.markUnreachable(toHash);
_transport.failed(msg, "Peer has no key, cannot establish");
return;
}
SessionKey sessionKey;
try {
sessionKey = new SessionKey(keyBytes);
} catch (IllegalArgumentException iae) {
_transport.markUnreachable(toHash);
_transport.failed(msg, "Peer has bad key, cannot establish");
return;
}
boolean allowExtendedOptions = VersionComparator.comp(toRouterInfo.getVersion(),
VERSION_ALLOW_EXTENDED_OPTIONS) >= 0
&& !_context.getBooleanProperty(PROP_DISABLE_EXT_OPTS);
// w/o ext options, it's always 'requested', no need to set
// don't ask if they are indirect
boolean requestIntroduction = allowExtendedOptions && !isIndirect &&
_transport.introducersMaybeRequired();
state = new OutboundEstablishState(_context, maybeTo, to,
toIdentity, allowExtendedOptions,
requestIntroduction,
sessionKey, addr, _transport.getDHFactory());
OutboundEstablishState oldState = _outboundStates.putIfAbsent(to, state);
boolean isNew = oldState == null;
if (isNew) {
if (isIndirect && maybeTo != null)
_outboundByClaimedAddress.put(maybeTo, state);
if (_log.shouldLog(Log.DEBUG))
_log.debug("Adding new " + state);
} else {
// whoops, somebody beat us to it, throw out the state we just created
state = oldState;
}
}
}
if (state != null) {
state.addMessage(msg);
List<OutNetMessage> queued = _queuedOutbound.remove(to);
if (queued != null) {
// see comments above
synchronized (queued) {
for (OutNetMessage m : queued) {
state.addMessage(m);
}
}
}
}
if (rejected) {
if (_log.shouldLog(Log.WARN))
_log.warn("Too many pending, rejecting outbound establish to " + to);
_transport.failed(msg, "Too many pending outbound connections");
_context.statManager().addRateData("udp.establishRejected", deferred);
return;
}
if (queueCount >= MAX_QUEUED_PER_PEER) {
_transport.failed(msg, "Too many pending messages for the given peer");
_context.statManager().addRateData("udp.establishOverflow", queueCount, deferred);
return;
}
if (deferred > 0)
msg.timestamp("too many deferred establishers");
else if (state != null)
msg.timestamp("establish state already waiting");
notifyActivity();
}
/**
* How many concurrent inbound sessions to deal with
*/
private int getMaxInboundEstablishers() {
return getMaxConcurrentEstablish()/2;
}
/**
* Should we allow another inbound establishment?
* Used to throttle outbound hole punches.
* @since 0.9.2
*/
public boolean shouldAllowInboundEstablishment() {
return _inboundStates.size() < getMaxInboundEstablishers();
}
/**
* Got a SessionRequest (initiates an inbound establishment)
*
*/
void receiveSessionRequest(RemoteHostId from, UDPPacketReader reader) {
if (!TransportUtil.isValidPort(from.getPort()) || !_transport.isValid(from.getIP())) {
if (_log.shouldLog(Log.WARN))
_log.warn("Receive session request from invalid: " + from);
return;
}
boolean isNew = false;
InboundEstablishState state = _inboundStates.get(from);
if (state == null) {
// TODO this is insufficient to prevent DoSing, especially if
// IP spoofing is used. For further study.
if (!shouldAllowInboundEstablishment()) {
if (_log.shouldLog(Log.WARN))
_log.warn("Dropping inbound establish, increase " + PROP_MAX_CONCURRENT_ESTABLISH);
_context.statManager().addRateData("udp.establishDropped", 1);
return; // drop the packet
}
if (_context.blocklist().isBlocklisted(from.getIP())) {
if (_log.shouldLog(Log.WARN))
_log.warn("Receive session request from blocklisted IP: " + from);
_context.statManager().addRateData("udp.establishBadIP", 1);
return; // drop the packet
}
if (!_transport.allowConnection())
return; // drop the packet
byte[] fromIP = from.getIP();
state = new InboundEstablishState(_context, fromIP, from.getPort(),
_transport.getExternalPort(fromIP.length == 16),
_transport.getDHBuilder());
state.receiveSessionRequest(reader.getSessionRequestReader());
if (_replayFilter.add(state.getReceivedX(), 0, 8)) {
if (_log.shouldLog(Log.WARN))
_log.warn("Duplicate X in session request from: " + from);
_context.statManager().addRateData("udp.dupDHX", 1);
return; // drop the packet
}
InboundEstablishState oldState = _inboundStates.putIfAbsent(from, state);
isNew = oldState == null;
if (!isNew)
// whoops, somebody beat us to it, throw out the state we just created
state = oldState;
}
if (isNew) {
// Don't offer to relay to privileged ports.
// Only offer for an IPv4 session.
// TODO if already we have their RI, only offer if they need it (no 'C' cap)
// if extended options, only if they asked for it
if (state.isIntroductionRequested() &&
_transport.canIntroduce() && state.getSentPort() >= 1024 &&
state.getSentIP().length == 4) {
// ensure > 0
long tag = 1 + _context.random().nextLong(MAX_TAG_VALUE);
state.setSentRelayTag(tag);
} else {
// we got an IB even though we were firewalled, hidden, not high cap, etc.
}
if (_log.shouldLog(Log.INFO))
_log.info("Received NEW session request " + state);
} else {
if (_log.shouldLog(Log.DEBUG))
_log.debug("Receive DUP session request from: " + state);
}
notifyActivity();
}
/**
* got a SessionConfirmed (should only happen as part of an inbound
* establishment)
*/
void receiveSessionConfirmed(RemoteHostId from, UDPPacketReader reader) {
InboundEstablishState state = _inboundStates.get(from);
if (state != null) {
state.receiveSessionConfirmed(reader.getSessionConfirmedReader());
notifyActivity();
if (_log.shouldLog(Log.DEBUG))
_log.debug("Receive session confirmed from: " + state);
} else {
if (_log.shouldLog(Log.WARN))
_log.warn("Receive (DUP?) session confirmed from: " + from);
}
}
/**
* Got a SessionCreated (in response to our outbound SessionRequest)
*
*/
void receiveSessionCreated(RemoteHostId from, UDPPacketReader reader) {
OutboundEstablishState state = _outboundStates.get(from);
if (state != null) {
state.receiveSessionCreated(reader.getSessionCreatedReader());
notifyActivity();
if (_log.shouldLog(Log.DEBUG))
_log.debug("Receive session created from: " + state);
} else {
if (_log.shouldLog(Log.WARN))
_log.warn("Receive (DUP?) session created from: " + from);
}
}
/**
* Got a SessionDestroy on an established conn
* @since 0.8.1
*/
void receiveSessionDestroy(RemoteHostId from, PeerState state) {
if (_log.shouldLog(Log.DEBUG))
_log.debug("Receive session destroy (EST) from: " + from);
_transport.dropPeer(state, false, "received destroy message");
}
/**
* Got a SessionDestroy during outbound establish
* @since 0.8.1
*/
void receiveSessionDestroy(RemoteHostId from, OutboundEstablishState state) {
if (_log.shouldLog(Log.DEBUG))
_log.debug("Receive session destroy (OB) from: " + from);
_outboundStates.remove(from);
Hash peer = state.getRemoteIdentity().calculateHash();
_transport.dropPeer(peer, false, "received destroy message during OB establish");
}
/**
* Got a SessionDestroy - maybe during an inbound establish?
* TODO - PacketHandler won't look up inbound establishes
* As this packet was essentially unauthenticated (i.e. intro key, not session key)
* we just log it as it could be spoofed.
* @since 0.8.1
*/
void receiveSessionDestroy(RemoteHostId from) {
if (_log.shouldLog(Log.WARN))
_log.warn("Receive session destroy (none) from: " + from);
//InboundEstablishState state = _inboundStates.remove(from);
//if (state != null) {
// Hash peer = state.getConfirmedIdentity().calculateHash();
// if (peer != null)
// _transport.dropPeer(peer, false, "received destroy message");
//}
}
/**
* A data packet arrived on an outbound connection being established, which
* means its complete (yay!). This is a blocking call, more than I'd like...
*
* @return the new PeerState
*/
PeerState receiveData(OutboundEstablishState state) {
state.dataReceived();
//int active = 0;
//int admitted = 0;
//int remaining = 0;
//active = _outboundStates.size();
_outboundStates.remove(state.getRemoteHostId());
// there shouldn't have been queued messages for this active state, but just in case...
List<OutNetMessage> queued = _queuedOutbound.remove(state.getRemoteHostId());
if (queued != null) {
// see comments above
synchronized (queued) {
for (OutNetMessage m : queued) {
state.addMessage(m);
}
}
}
if (_outboundStates.size() < getMaxConcurrentEstablish() && !_queuedOutbound.isEmpty()) {
locked_admitQueued();
}
//remaining = _queuedOutbound.size();
//if (admitted > 0)
// _log.log(Log.CRIT, "Admitted " + admitted + " with " + remaining + " remaining queued and " + active + " active");
if (_log.shouldLog(Log.INFO))
_log.info("Outbound established completely! yay: " + state);
PeerState peer = handleCompletelyEstablished(state);
notifyActivity();
return peer;
}
/**
* Move pending OB messages from _queuedOutbound to _outboundStates.
* This isn't so great because _queuedOutbound is not a FIFO.
*/
private int locked_admitQueued() {
if (_queuedOutbound.isEmpty())
return 0;
int admitted = 0;
int max = getMaxConcurrentEstablish();
for (Iterator<Map.Entry<RemoteHostId, List<OutNetMessage>>> iter = _queuedOutbound.entrySet().iterator();
iter.hasNext() && _outboundStates.size() < max; ) {
// ok, active shrunk, lets let some queued in.
Map.Entry<RemoteHostId, List<OutNetMessage>> entry = iter.next();
// java 5 IllegalStateException here
try {
iter.remove();
} catch (IllegalStateException ise) {
continue;
}
RemoteHostId to = entry.getKey();
List<OutNetMessage> allQueued = entry.getValue();
List<OutNetMessage> queued = new ArrayList<OutNetMessage>();
long now = _context.clock().now();
synchronized (allQueued) {
for (OutNetMessage msg : allQueued) {
if (now - Router.CLOCK_FUDGE_FACTOR > msg.getExpiration()) {
_transport.failed(msg, "Took too long in est. mgr OB queue");
} else {
queued.add(msg);
}
}
}
if (queued.isEmpty())
continue;
for (OutNetMessage m : queued) {
m.timestamp("no longer deferred... establishing");
establish(m, false);
}
admitted++;
}
return admitted;
}
private void notifyActivity() {
synchronized (_activityLock) {
_activity++;
_activityLock.notifyAll();
}
}
/**
* ok, fully received, add it to the established cons and queue up a
* netDb store to them
*
*/
private void handleCompletelyEstablished(InboundEstablishState state) {
if (state.isComplete()) return;
RouterIdentity remote = state.getConfirmedIdentity();
PeerState peer = new PeerState(_context, _transport,
state.getSentIP(), state.getSentPort(), remote.calculateHash(), true);
peer.setCurrentCipherKey(state.getCipherKey());
peer.setCurrentMACKey(state.getMACKey());
peer.setWeRelayToThemAs(state.getSentRelayTag());
// Lookup the peer's MTU from the netdb, since it isn't included in the protocol setup (yet)
// TODO if we don't have RI then we will get it shortly, but too late.
// Perhaps netdb should notify transport when it gets a new RI...
RouterInfo info = _context.netDb().lookupRouterInfoLocally(remote.calculateHash());
if (info != null) {
RouterAddress addr = _transport.getTargetAddress(info);
if (addr != null) {
String smtu = addr.getOption(UDPAddress.PROP_MTU);
if (smtu != null) {
try {
boolean isIPv6 = state.getSentIP().length == 16;
int mtu = MTU.rectify(isIPv6, Integer.parseInt(smtu));
peer.setHisMTU(mtu);
} catch (NumberFormatException nfe) {}
}
}
}
// 0 is the default
//peer.setTheyRelayToUsAs(0);
if (_log.shouldLog(Log.DEBUG))
_log.debug("Handle completely established (inbound): " + state
+ " - " + peer.getRemotePeer());
//if (true) // for now, only support direct
// peer.setRemoteRequiresIntroduction(false);
_transport.addRemotePeerState(peer);
boolean isIPv6 = state.getSentIP().length == 16;
_transport.inboundConnectionReceived(isIPv6);
_transport.setIP(remote.calculateHash(), state.getSentIP());
_context.statManager().addRateData("udp.inboundEstablishTime", state.getLifetime());
sendInboundComplete(peer);
OutNetMessage msg;
while ((msg = state.getNextQueuedMessage()) != null) {
if (_context.clock().now() - Router.CLOCK_FUDGE_FACTOR > msg.getExpiration()) {
msg.timestamp("took too long but established...");
_transport.failed(msg, "Took too long to establish, but it was established");
} else {
msg.timestamp("session fully established and sent");
_transport.send(msg);
}
}
state.complete();
}
/**
* dont send our info immediately, just send a small data packet, and 5-10s later,
* if the peer isnt banlisted, *then* send them our info. this will help kick off
* the oldnet
* The "oldnet" was < 0.6.1.10, it is long gone.
* The delay really slows down the network.
* The peer is unbanlisted and marked reachable by addRemotePeerState() which calls markReachable()
* so the check below is fairly pointless.
* If for some strange reason an oldnet router (NETWORK_ID == 1) does show up,
* it's handled in UDPTransport.messageReceived()
* (where it will get dropped, marked unreachable and banlisted at that time).
*/
private void sendInboundComplete(PeerState peer) {
// SimpleTimer.getInstance().addEvent(new PublishToNewInbound(peer), 10*1000);
if (_log.shouldLog(Log.INFO))
_log.info("Completing to the peer after IB confirm: " + peer);
DeliveryStatusMessage dsm = new DeliveryStatusMessage(_context);
dsm.setArrival(_networkID); // overloaded, sure, but future versions can check this
// This causes huge values in the inNetPool.droppedDeliveryStatusDelay stat
// so it needs to be caught in InNetMessagePool.
dsm.setMessageExpiration(_context.clock().now() + DATA_MESSAGE_TIMEOUT);
dsm.setMessageId(_context.random().nextLong(I2NPMessage.MAX_ID_VALUE));
// sent below
// just do this inline
//_context.simpleTimer2().addEvent(new PublishToNewInbound(peer), 0);
Hash hash = peer.getRemotePeer();
if ((hash != null) && (!_context.banlist().isBanlisted(hash)) && (!_transport.isUnreachable(hash))) {
// ok, we are fine with them, send them our latest info
//if (_log.shouldLog(Log.INFO))
// _log.info("Publishing to the peer after confirm plus delay (without banlist): " + peer);
// bundle the two messages together for efficiency
DatabaseStoreMessage dbsm = getOurInfo();
List<I2NPMessage> msgs = new ArrayList<I2NPMessage>(2);
msgs.add(dsm);
msgs.add(dbsm);
_transport.send(msgs, peer);
} else {
_transport.send(dsm, peer);
// nuh uh.
if (_log.shouldLog(Log.WARN))
_log.warn("NOT publishing to the peer after confirm plus delay (WITH banlist): " + (hash != null ? hash.toString() : "unknown"));
}
}
/**
* ok, fully received, add it to the established cons and send any
* queued messages
*
* @return the new PeerState
*/
private PeerState handleCompletelyEstablished(OutboundEstablishState state) {
if (state.complete()) {
RouterIdentity rem = state.getRemoteIdentity();
if (rem != null)
return _transport.getPeerState(rem.getHash());
}
long now = _context.clock().now();
RouterIdentity remote = state.getRemoteIdentity();
// only if == state
RemoteHostId claimed = state.getClaimedAddress();
if (claimed != null)
_outboundByClaimedAddress.remove(claimed, state);
_outboundByHash.remove(remote.calculateHash(), state);
PeerState peer = new PeerState(_context, _transport,
state.getSentIP(), state.getSentPort(), remote.calculateHash(), false);
peer.setCurrentCipherKey(state.getCipherKey());
peer.setCurrentMACKey(state.getMACKey());
peer.setTheyRelayToUsAs(state.getReceivedRelayTag());
int mtu = state.getRemoteAddress().getMTU();
if (mtu > 0)
peer.setHisMTU(mtu);
// 0 is the default
//peer.setWeRelayToThemAs(0);
if (_log.shouldLog(Log.DEBUG))
_log.debug("Handle completely established (outbound): " + state
+ " - " + peer.getRemotePeer());
_transport.addRemotePeerState(peer);
_transport.setIP(remote.calculateHash(), state.getSentIP());
_context.statManager().addRateData("udp.outboundEstablishTime", state.getLifetime());
DatabaseStoreMessage dbsm = null;
if (!state.isFirstMessageOurDSM()) {
dbsm = getOurInfo();
} else if (_log.shouldLog(Log.INFO)) {
_log.info("Skipping publish: " + state);
}
List<OutNetMessage> msgs = new ArrayList<OutNetMessage>(8);
OutNetMessage msg;
while ((msg = state.getNextQueuedMessage()) != null) {
if (now - Router.CLOCK_FUDGE_FACTOR > msg.getExpiration()) {
msg.timestamp("took too long but established...");
_transport.failed(msg, "Took too long to establish, but it was established");
} else {
msg.timestamp("session fully established and sent");
msgs.add(msg);
}
}
_transport.send(dbsm, msgs, peer);
return peer;
}
/****
private void sendOurInfo(PeerState peer, boolean isInbound) {
if (_log.shouldLog(Log.INFO))
_log.info("Publishing to the peer after confirm: " +
(isInbound ? " inbound con from " + peer : "outbound con to " + peer));
DatabaseStoreMessage m = getOurInfo();
_transport.send(m, peer);
}
****/
/**
* A database store message with our router info
* @return non-null
* @since 0.9.24 split from sendOurInfo()
*/
private DatabaseStoreMessage getOurInfo() {
DatabaseStoreMessage m = new DatabaseStoreMessage(_context);
m.setEntry(_context.router().getRouterInfo());
m.setMessageExpiration(_context.clock().now() + DATA_MESSAGE_TIMEOUT);
return m;
}
/** the relay tag is a 4-byte field in the protocol */
public static final long MAX_TAG_VALUE = 0xFFFFFFFFl;
/**
* This may be called more than once
*/
private void sendCreated(InboundEstablishState state) {
if (_log.shouldLog(Log.DEBUG))
_log.debug("Send created to: " + state);
try {
state.generateSessionKey();
} catch (DHSessionKeyBuilder.InvalidPublicParameterException ippe) {
if (_log.shouldLog(Log.WARN))
_log.warn("Peer " + state + " sent us an invalid DH parameter", ippe);
_inboundStates.remove(state.getRemoteHostId());
state.fail();
return;
}
UDPPacket pkt = _builder.buildSessionCreatedPacket(state,
_transport.getExternalPort(state.getSentIP().length == 16),
_transport.getIntroKey());
if (pkt == null) {
if (_log.shouldLog(Log.WARN))
_log.warn("Peer " + state + " sent us an invalid IP?");
_inboundStates.remove(state.getRemoteHostId());
state.fail();
return;
}
_transport.send(pkt);
state.createdPacketSent();
}
/**
* Caller should probably synch on outboundState
*/
private void sendRequest(OutboundEstablishState state) {
if (_log.shouldLog(Log.DEBUG))
_log.debug("Send SessionRequest to: " + state);
UDPPacket packet = _builder.buildSessionRequestPacket(state);
if (packet != null) {
_transport.send(packet);
} else {
if (_log.shouldLog(Log.WARN))
_log.warn("Unable to build a session request packet for " + state);
}
state.requestSent();
}
/**
* Send RelayRequests to multiple introducers.
* This may be called multiple times, it sets the nonce the first time only
* Caller should probably synch on state.
*/
private void handlePendingIntro(OutboundEstablishState state) {
long nonce = state.getIntroNonce();
if (nonce < 0) {
OutboundEstablishState old;
do {
nonce = _context.random().nextLong(MAX_NONCE);
old = _liveIntroductions.putIfAbsent(Long.valueOf(nonce), state);
} while (old != null);
state.setIntroNonce(nonce);
}
_context.statManager().addRateData("udp.sendIntroRelayRequest", 1);
List<UDPPacket> requests = _builder.buildRelayRequest(_transport, state, _transport.getIntroKey());
if (requests.isEmpty()) {
// FIXME need a failed OB state
if (_log.shouldLog(Log.WARN))
_log.warn("No valid introducers! " + state);
// set failed state, remove nonce, and return
}
for (UDPPacket req : requests) {
_transport.send(req);
}
if (_log.shouldLog(Log.DEBUG))
_log.debug("Send intro for " + state + " with our intro key as " + _transport.getIntroKey());
state.introSent();
}
/**
* We are Alice, we sent a RelayRequest to Bob and got a response back.
*/
void receiveRelayResponse(RemoteHostId bob, UDPPacketReader reader) {
long nonce = reader.getRelayResponseReader().readNonce();
OutboundEstablishState state = _liveIntroductions.remove(Long.valueOf(nonce));
if (state == null) {
if (_log.shouldLog(Log.INFO))
_log.info("Dup or unknown RelayResponse: " + nonce);
return; // already established
}
// Note that we ignore the Alice (us) IP/Port in the RelayResponse
int sz = reader.getRelayResponseReader().readCharlieIPSize();
byte ip[] = new byte[sz];
reader.getRelayResponseReader().readCharlieIP(ip, 0);
int port = reader.getRelayResponseReader().readCharliePort();
if ((!isValid(ip, port)) || (!isValid(bob.getIP(), bob.getPort()))) {
if (_log.shouldLog(Log.WARN))
_log.warn("Bad relay resp from " + bob + " for " + Addresses.toString(ip, port));
_context.statManager().addRateData("udp.relayBadIP", 1);
return;
}
InetAddress addr = null;
try {
addr = InetAddress.getByAddress(ip);
} catch (UnknownHostException uhe) {
if (_log.shouldLog(Log.WARN))
_log.warn("Introducer for " + state + " (" + bob + ") sent us an invalid address for our target: " + Addresses.toString(ip, port), uhe);
// TODO either put the nonce back in liveintroductions, or fail
return;
}
_context.statManager().addRateData("udp.receiveIntroRelayResponse", state.getLifetime());
if (_log.shouldLog(Log.INFO))
_log.info("Received RelayResponse for " + state.getRemoteIdentity().calculateHash() + " - they are on "
+ addr.toString() + ":" + port + " (according to " + bob + ") nonce=" + nonce);
synchronized (state) {
RemoteHostId oldId = state.getRemoteHostId();
state.introduced(ip, port);
RemoteHostId newId = state.getRemoteHostId();
// Swap out the RemoteHostId the state is indexed under.
// It was a Hash, change it to a IP/port.
// Remove the entry in the byClaimedAddress map as it's now in main map.
// Add an entry in the byHash map so additional OB pkts can find it.
_outboundByHash.put(state.getRemoteIdentity().calculateHash(), state);
RemoteHostId claimed = state.getClaimedAddress();
if (!oldId.equals(newId)) {
_outboundStates.remove(oldId);
_outboundStates.put(newId, state);
if (_log.shouldLog(Log.INFO))
_log.info("RR replaced " + oldId + " with " + newId + ", claimed address was " + claimed);
}
//
if (claimed != null)
_outboundByClaimedAddress.remove(oldId, state); // only if == state
}
notifyActivity();
}
/**
* Called from UDPReceiver.
* Accelerate response to RelayResponse if we haven't sent it yet.
*
* @since 0.9.15
*/
void receiveHolePunch(InetAddress from, int fromPort) {
RemoteHostId id = new RemoteHostId(from.getAddress(), fromPort);
OutboundEstablishState state = _outboundStates.get(id);
if (state != null) {
boolean sendNow = state.receiveHolePunch();
if (sendNow) {
if (_log.shouldLog(Log.INFO))
_log.info("Hole punch from " + state + ", sending SessionRequest now");
notifyActivity();
} else {
if (_log.shouldLog(Log.INFO))
_log.info("Hole punch from " + state + ", already sent SessionRequest");
}
} else {
// HolePunch received before RelayResponse, and we didn't know the IP/port, or it changed
if (_log.shouldLog(Log.INFO))
_log.info("No state found for hole punch from " + from + " port " + fromPort);
}
}
/**
* Are IP and port valid? This is only for checking the relay response.
* Reject all IPv6, for now, even if we are configured for it.
* Refuse anybody in the same /16
* @since 0.9.3
*/
private boolean isValid(byte[] ip, int port) {
return TransportUtil.isValidPort(port) &&
ip != null && ip.length == 4 &&
_transport.isValid(ip) &&
(!_transport.isTooClose(ip)) &&
(!_context.blocklist().isBlocklisted(ip));
}
/**
* Note that while a SessionConfirmed could in theory be fragmented,
* in practice a RouterIdentity is 387 bytes and a single fragment is 512 bytes max,
* so it will never be fragmented.
* Caller should probably synch on state.
*/
private void sendConfirmation(OutboundEstablishState state) {
boolean valid = state.validateSessionCreated();
if (!valid) {
// validate clears fields on failure
// sendDestroy(state) won't work as we haven't sent the confirmed...
if (_log.shouldLog(Log.WARN))
_log.warn("SessionCreated validate failed: " + state);
return;
}
if (!_transport.isValid(state.getReceivedIP()) || !_transport.isValid(state.getRemoteHostId().getIP())) {
state.fail();
return;
}
// gives us the opportunity to "detect" our external addr
_transport.externalAddressReceived(state.getRemoteIdentity().calculateHash(), state.getReceivedIP(), state.getReceivedPort());
// signs if we havent signed yet
state.prepareSessionConfirmed();
// BUG - handle null return
UDPPacket packets[] = _builder.buildSessionConfirmedPackets(state, _context.router().getRouterInfo().getIdentity());
if (_log.shouldLog(Log.DEBUG))
_log.debug("Send confirm to: " + state);
for (int i = 0; i < packets.length; i++)
_transport.send(packets[i]);
state.confirmedPacketsSent();
}
/**
* Tell the other side never mind.
* This is only useful after we have received SessionCreated,
* and sent SessionConfirmed, but not yet gotten a data packet as an
* ack to the SessionConfirmed - otherwise we haven't generated the keys.
* Caller should probably synch on state.
*
* @since 0.9.2
*/
private void sendDestroy(OutboundEstablishState state) {
UDPPacket packet = _builder.buildSessionDestroyPacket(state);
if (packet != null) {
if (_log.shouldLog(Log.DEBUG))
_log.debug("Send destroy to: " + state);
_transport.send(packet);
}
}
/**
* Tell the other side never mind.
* This is only useful after we have sent SessionCreated,
* but not received SessionConfirmed
* Otherwise we haven't generated the keys.
* Caller should probably synch on state.
*
* @since 0.9.2
*/
private void sendDestroy(InboundEstablishState state) {
UDPPacket packet = _builder.buildSessionDestroyPacket(state);
if (packet != null) {
if (_log.shouldLog(Log.DEBUG))
_log.debug("Send destroy to: " + state);
_transport.send(packet);
}
}
/**
* Drive through the inbound establishment states, adjusting one of them
* as necessary. Called from Establisher thread only.
* @return next requested time or -1
*/
private long handleInbound() {
long now = _context.clock().now();
long nextSendTime = -1;
InboundEstablishState inboundState = null;
boolean expired = false;
for (Iterator<InboundEstablishState> iter = _inboundStates.values().iterator(); iter.hasNext(); ) {
InboundEstablishState cur = iter.next();
if (cur.getState() == IB_STATE_CONFIRMED_COMPLETELY) {
// completely received (though the signature may be invalid)
iter.remove();
inboundState = cur;
//if (_log.shouldLog(Log.DEBUG))
// _log.debug("Removing completely confirmed inbound state");
break;
} else if (cur.getLifetime() > MAX_IB_ESTABLISH_TIME) {
// took too long
iter.remove();
inboundState = cur;
//_context.statManager().addRateData("udp.inboundEstablishFailedState", cur.getState(), cur.getLifetime());
//if (_log.shouldLog(Log.DEBUG))
// _log.debug("Removing expired inbound state");
expired = true;
break;
} else if (cur.getState() == IB_STATE_FAILED) {
iter.remove();
//_context.statManager().addRateData("udp.inboundEstablishFailedState", cur.getState(), cur.getLifetime());
} else {
if (cur.getNextSendTime() <= now) {
// our turn...
inboundState = cur;
// if (_log.shouldLog(Log.DEBUG))
// _log.debug("Processing inbound that wanted activity");
break;
} else {
// nothin to do but wait for them to send us
// stuff, so lets move on to the next one being
// established
long when = -1;
if (cur.getNextSendTime() <= 0) {
when = cur.getEstablishBeginTime() + MAX_IB_ESTABLISH_TIME;
} else {
when = cur.getNextSendTime();
}
if (when < nextSendTime)
nextSendTime = when;
}
}
}
if (inboundState != null) {
//if (_log.shouldLog(Log.DEBUG))
// _log.debug("Processing for inbound: " + inboundState);
synchronized (inboundState) {
switch (inboundState.getState()) {
case IB_STATE_REQUEST_RECEIVED:
if (expired)
processExpired(inboundState);
else
sendCreated(inboundState);
break;
case IB_STATE_CREATED_SENT: // fallthrough
case IB_STATE_CONFIRMED_PARTIALLY:
if (expired) {
sendDestroy(inboundState);
processExpired(inboundState);
} else if (inboundState.getNextSendTime() <= now) {
sendCreated(inboundState);
}
break;
case IB_STATE_CONFIRMED_COMPLETELY:
RouterIdentity remote = inboundState.getConfirmedIdentity();
if (remote != null) {
if (_context.banlist().isBanlistedForever(remote.calculateHash())) {
if (_log.shouldLog(Log.WARN))
_log.warn("Dropping inbound connection from permanently banlisted peer: " + remote.calculateHash());
// So next time we will not accept the con, rather than doing the whole handshake
_context.blocklist().add(inboundState.getSentIP());
inboundState.fail();
processExpired(inboundState);
} else {
handleCompletelyEstablished(inboundState);
}
} else {
if (_log.shouldLog(Log.WARN))
_log.warn("confirmed with invalid? " + inboundState);
inboundState.fail();
processExpired(inboundState);
}
break;
case IB_STATE_COMPLETE: // fall through
case IB_STATE_FAILED: // leak here if fail() was called in IES???
break; // already removed;
case IB_STATE_UNKNOWN:
// Can't happen, always call receiveSessionRequest() before putting in map
if (_log.shouldLog(Log.ERROR))
_log.error("hrm, state is unknown for " + inboundState);
}
}
// ok, since there was something to do, we want to loop again
nextSendTime = now;
}
return nextSendTime;
}
/**
* Drive through the outbound establishment states, adjusting one of them
* as necessary. Called from Establisher thread only.
* @return next requested time or -1
*/
private long handleOutbound() {
long now = _context.clock().now();
long nextSendTime = -1;
OutboundEstablishState outboundState = null;
//int admitted = 0;
//int remaining = 0;
//int active = 0;
for (Iterator<OutboundEstablishState> iter = _outboundStates.values().iterator(); iter.hasNext(); ) {
OutboundEstablishState cur = iter.next();
OutboundEstablishState.OutboundState state = cur.getState();
if (state == OB_STATE_CONFIRMED_COMPLETELY ||
state == OB_STATE_VALIDATION_FAILED) {
iter.remove();
outboundState = cur;
break;
} else if (cur.getLifetime() >= MAX_OB_ESTABLISH_TIME) {
// took too long
iter.remove();
outboundState = cur;
//_context.statManager().addRateData("udp.outboundEstablishFailedState", cur.getState(), cur.getLifetime());
//if (_log.shouldLog(Log.DEBUG))
// _log.debug("Removing expired outbound: " + cur);
break;
} else {
if (cur.getNextSendTime() <= now) {
// our turn...
outboundState = cur;
// if (_log.shouldLog(Log.DEBUG))
// _log.debug("Outbound wants activity: " + cur);
break;
} else {
// nothin to do but wait for them to send us
// stuff, so lets move on to the next one being
// established
long when = -1;
if (cur.getNextSendTime() <= 0) {
when = cur.getEstablishBeginTime() + MAX_OB_ESTABLISH_TIME;
} else {
when = cur.getNextSendTime();
}
if ( (nextSendTime <= 0) || (when < nextSendTime) )
nextSendTime = when;
// if (_log.shouldLog(Log.DEBUG))
// _log.debug("Outbound doesn't want activity: " + cur + " (next=" + (when-now) + ")");
}
}
}
//admitted = locked_admitQueued();
//remaining = _queuedOutbound.size();
//if (admitted > 0)
// _log.log(Log.CRIT, "Admitted " + admitted + " in push with " + remaining + " remaining queued and " + active + " active");
if (outboundState != null) {
//if (_log.shouldLog(Log.DEBUG))
// _log.debug("Processing for outbound: " + outboundState);
synchronized (outboundState) {
boolean expired = outboundState.getLifetime() >= MAX_OB_ESTABLISH_TIME;
switch (outboundState.getState()) {
case OB_STATE_UNKNOWN: // fall thru
case OB_STATE_INTRODUCED:
if (expired)
processExpired(outboundState);
else
sendRequest(outboundState);
break;
case OB_STATE_REQUEST_SENT:
// no response yet (or it was invalid), lets retry
long rtime = outboundState.getRequestSentTime();
if (expired || (rtime > 0 && rtime + OB_MESSAGE_TIMEOUT <= now))
processExpired(outboundState);
else if (outboundState.getNextSendTime() <= now)
sendRequest(outboundState);
break;
case OB_STATE_CREATED_RECEIVED:
if (expired)
processExpired(outboundState);
else if (outboundState.getNextSendTime() <= now)
sendConfirmation(outboundState);
break;
case OB_STATE_CONFIRMED_PARTIALLY:
long ctime = outboundState.getConfirmedSentTime();
if (expired || (ctime > 0 && ctime + OB_MESSAGE_TIMEOUT <= now)) {
sendDestroy(outboundState);
processExpired(outboundState);
} else if (outboundState.getNextSendTime() <= now) {
sendConfirmation(outboundState);
}
break;
case OB_STATE_CONFIRMED_COMPLETELY:
if (expired)
processExpired(outboundState);
else
handleCompletelyEstablished(outboundState);
break;
case OB_STATE_PENDING_INTRO:
long itime = outboundState.getIntroSentTime();
if (expired || (itime > 0 && itime + OB_MESSAGE_TIMEOUT <= now))
processExpired(outboundState);
else if (outboundState.getNextSendTime() <= now)
handlePendingIntro(outboundState);
break;
case OB_STATE_VALIDATION_FAILED:
processExpired(outboundState);
break;
}
}
// ok, since there was something to do, we want to loop again
nextSendTime = now;
}
return nextSendTime;
}
/**
* Caller should probably synch on outboundState
*/
private void processExpired(OutboundEstablishState outboundState) {
long nonce = outboundState.getIntroNonce();
if (nonce >= 0) {
// remove only if value == state
boolean removed = _liveIntroductions.remove(Long.valueOf(nonce), outboundState);
if (removed) {
if (_log.shouldLog(Log.DEBUG))
_log.debug("Send intro for " + outboundState + " timed out");
_context.statManager().addRateData("udp.sendIntroRelayTimeout", 1);
}
}
// only if == state
RemoteHostId claimed = outboundState.getClaimedAddress();
if (claimed != null)
_outboundByClaimedAddress.remove(claimed, outboundState);
_outboundByHash.remove(outboundState.getRemoteIdentity().calculateHash(), outboundState);
// should have already been removed in handleOutbound() above
// remove only if value == state
_outboundStates.remove(outboundState.getRemoteHostId(), outboundState);
if (outboundState.getState() != OB_STATE_CONFIRMED_COMPLETELY) {
if (_log.shouldLog(Log.INFO))
_log.info("Expired: " + outboundState + " Lifetime: " + outboundState.getLifetime());
OutNetMessage msg;
while ((msg = outboundState.getNextQueuedMessage()) != null) {
_transport.failed(msg, "Expired during failed establish");
}
String err = "Took too long to establish OB connection, state = " + outboundState.getState();
Hash peer = outboundState.getRemoteIdentity().calculateHash();
//_context.banlist().banlistRouter(peer, err, UDPTransport.STYLE);
_transport.markUnreachable(peer);
_transport.dropPeer(peer, false, err);
//_context.profileManager().commErrorOccurred(peer);
outboundState.fail();
} else {
OutNetMessage msg;
while ((msg = outboundState.getNextQueuedMessage()) != null) {
_transport.send(msg);
}
}
}
/**
* Caller should probably synch on inboundState
* @since 0.9.2
*/
private void processExpired(InboundEstablishState inboundState) {
OutNetMessage msg;
while ((msg = inboundState.getNextQueuedMessage()) != null) {
_transport.failed(msg, "Expired during failed establish");
}
}
/**
* Driving thread, processing up to one step for an inbound peer and up to
* one step for an outbound peer. This is prodded whenever any peer's state
* changes as well.
*
*/
private class Establisher implements Runnable {
public void run() {
while (_alive) {
try {
doPass();
} catch (RuntimeException re) {
_log.log(Log.CRIT, "Error in the establisher", re);
}
}
_inboundStates.clear();
_outboundStates.clear();
_queuedOutbound.clear();
_outboundByClaimedAddress.clear();
_outboundByHash.clear();
}
private long _lastFailsafe;
private static final long FAILSAFE_INTERVAL = 3*60*1000;
// Debugging
private long _lastPrinted;
private static final long PRINT_INTERVAL = 5*1000;
private void doPass() {
if (_log.shouldLog(Log.DEBUG) && _lastPrinted + PRINT_INTERVAL < _context.clock().now()) {
_lastPrinted = _context.clock().now();
int iactive = _inboundStates.size();
int oactive = _outboundStates.size();
if (iactive > 0 || oactive > 0) {
int queued = _queuedOutbound.size();
int live = _liveIntroductions.size();
int claimed = _outboundByClaimedAddress.size();
int hash = _outboundByHash.size();
_log.debug("OB states: " + oactive + " IB states: " + iactive +
" OB queued: " + queued + " intros: " + live +
" OB claimed: " + claimed + " hash: " + hash);
}
}
_activity = 0;
long now = _context.clock().now();
if (_lastFailsafe + FAILSAFE_INTERVAL < _context.clock().now()) {
_lastFailsafe = _context.clock().now();
doFailsafe();
}
long nextSendTime = -1;
long nextSendInbound = handleInbound();
long nextSendOutbound = handleOutbound();
if (nextSendInbound > 0)
nextSendTime = nextSendInbound;
if ( (nextSendTime < 0) || (nextSendOutbound < nextSendTime) )
nextSendTime = nextSendOutbound;
long delay = nextSendTime - now;
if ( (nextSendTime == -1) || (delay > 0) ) {
if (delay > 1000)
delay = 1000;
try {
synchronized (_activityLock) {
if (_activity > 0)
return;
if (nextSendTime == -1)
_activityLock.wait(1000);
else
_activityLock.wait(delay);
}
} catch (InterruptedException ie) {
}
// if (_log.shouldLog(Log.DEBUG))
// _log.debug("After waiting w/ nextSend=" + nextSendTime
// + " and delay=" + delay + " and interrupted=" + interrupted);
}
}
/** @since 0.9.2 */
private void doFailsafe() {
for (Iterator<OutboundEstablishState> iter = _liveIntroductions.values().iterator(); iter.hasNext(); ) {
OutboundEstablishState state = iter.next();
if (state.getLifetime() > 3*MAX_OB_ESTABLISH_TIME) {
iter.remove();
if (_log.shouldLog(Log.WARN))
_log.warn("Failsafe remove LI " + state);
}
}
for (Iterator<OutboundEstablishState> iter = _outboundByClaimedAddress.values().iterator(); iter.hasNext(); ) {
OutboundEstablishState state = iter.next();
if (state.getLifetime() > 3*MAX_OB_ESTABLISH_TIME) {
iter.remove();
if (_log.shouldLog(Log.WARN))
_log.warn("Failsafe remove OBBCA " + state);
}
}
for (Iterator<OutboundEstablishState> iter = _outboundByHash.values().iterator(); iter.hasNext(); ) {
OutboundEstablishState state = iter.next();
if (state.getLifetime() > 3*MAX_OB_ESTABLISH_TIME) {
iter.remove();
if (_log.shouldLog(Log.WARN))
_log.warn("Failsafe remove OBBH " + state);
}
}
}
}
}