package net.i2p.router.transport;
/*
* free (adj.): unencumbered; not under the control of others
* Written by jrandom in 2003 and released into the public domain
* with no warranty of any kind, either expressed or implied.
* It probably won't make your computer catch on fire, or eat
* your children, but it might. Use at your own risk.
*
*/
import java.io.IOException;
import java.io.Serializable;
import java.io.Writer;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.Vector;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CopyOnWriteArrayList;
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.I2NPMessage;
import net.i2p.router.CommSystemFacade.Status;
import net.i2p.router.Job;
import net.i2p.router.MessageSelector;
import net.i2p.router.OutNetMessage;
import net.i2p.router.Router;
import net.i2p.router.RouterContext;
import net.i2p.util.ConcurrentHashSet;
import net.i2p.util.LHMCache;
import net.i2p.util.Log;
import net.i2p.util.SimpleTimer;
import net.i2p.util.SystemVersion;
import net.i2p.util.Translate;
/**
* Defines a way to send a message to another peer and start listening for messages
*
*/
public abstract class TransportImpl implements Transport {
private final Log _log;
private TransportEventListener _listener;
protected final List<RouterAddress> _currentAddresses;
// Only used by NTCP. SSU does not use. See send() below.
private final BlockingQueue<OutNetMessage> _sendPool;
protected final RouterContext _context;
/** map from routerIdentHash to timestamp (Long) that the peer was last unreachable */
private final Map<Hash, Long> _unreachableEntries;
private final Map<Hash, Long> _wasUnreachableEntries;
private final Set<InetAddress> _localAddresses;
/** global router ident -> IP */
private static final Map<Hash, byte[]> _IPMap;
private static final long UNREACHABLE_PERIOD = 5*60*1000;
private static final long WAS_UNREACHABLE_PERIOD = 30*60*1000;
static {
long maxMemory = SystemVersion.getMaxMemory();
long min = 512;
long max = 4096;
// 1024 nominal for 128 MB
int size = (int) Math.max(min, Math.min(max, 1 + (maxMemory / (128*1024))));
_IPMap = new LHMCache<Hash, byte[]>(size);
}
/**
* Initialize the new transport
*
*/
public TransportImpl(RouterContext context) {
_context = context;
_log = _context.logManager().getLog(TransportImpl.class);
_context.statManager().createRateStat("transport.sendMessageFailureLifetime", "How long the lifetime of messages that fail are?", "Transport", new long[] { 60*1000l, 10*60*1000l, 60*60*1000l, 24*60*60*1000l });
_context.statManager().createRequiredRateStat("transport.sendMessageSize", "Size of sent messages (bytes)", "Transport", new long[] { 60*1000l, 5*60*1000l, 60*60*1000l, 24*60*60*1000l });
_context.statManager().createRequiredRateStat("transport.receiveMessageSize", "Size of received messages (bytes)", "Transport", new long[] { 60*1000l, 5*60*1000l, 60*60*1000l, 24*60*60*1000l });
_context.statManager().createRateStat("transport.receiveMessageTime", "How long it takes to read a message?", "Transport", new long[] { 60*1000l, 5*60*1000l, 10*60*1000l, 60*60*1000l, 24*60*60*1000l });
_context.statManager().createRateStat("transport.receiveMessageTimeSlow", "How long it takes to read a message (when it takes more than a second)?", "Transport", new long[] { 60*1000l, 5*60*1000l, 10*60*1000l, 60*60*1000l, 24*60*60*1000l });
_context.statManager().createRequiredRateStat("transport.sendProcessingTime", "Time to process and send a message (ms)", "Transport", new long[] { 60*1000l, 10*60*1000l, 60*60*1000l, 24*60*60*1000l });
//_context.statManager().createRateStat("transport.sendProcessingTime." + getStyle(), "Time to process and send a message (ms)", "Transport", new long[] { 60*1000l });
_context.statManager().createRateStat("transport.expiredOnQueueLifetime", "How long a message that expires on our outbound queue is processed", "Transport", new long[] { 60*1000l, 10*60*1000l, 60*60*1000l, 24*60*60*1000l } );
_currentAddresses = new CopyOnWriteArrayList<RouterAddress>();
if (getStyle().equals("NTCP"))
_sendPool = new ArrayBlockingQueue<OutNetMessage>(8);
else
_sendPool = null;
_unreachableEntries = new HashMap<Hash, Long>(32);
_wasUnreachableEntries = new HashMap<Hash, Long>(32);
_localAddresses = new ConcurrentHashSet<InetAddress>(4);
_context.simpleTimer2().addPeriodicEvent(new CleanupUnreachable(), 2 * UNREACHABLE_PERIOD, UNREACHABLE_PERIOD / 2);
}
/**
* How many peers are we connected to?
*/
public abstract int countPeers();
/**
* How many peers are we currently connected to, that we have
* sent a message to or received a message from in the last five minutes.
*/
public abstract int countActivePeers();
/**
* How many peers are we currently connected to, that we have
* sent a message to in the last minute.
* Unused for anything, to be removed.
*/
public abstract int countActiveSendPeers();
/** ...and 50/100/150/200/250 for BW Tiers K/L/M/N/O */
private static final int MAX_CONNECTION_FACTOR = 50;
/** Per-transport connection limit */
public int getMaxConnections() {
if (_context.commSystem().isDummy())
// testing
return 0;
String style = getStyle();
// object churn
String maxProp;
if (style.equals("SSU"))
maxProp = "i2np.udp.maxConnections";
else if (style.equals("NTCP"))
maxProp = "i2np.ntcp.maxConnections";
else // shouldn't happen
maxProp = "i2np." + style.toLowerCase(Locale.US) + ".maxConnections";
int def = MAX_CONNECTION_FACTOR;
// get it from here, not the RI, to avoid deadlock
char bw = _context.router().getBandwidthClass();
switch (bw) {
case Router.CAPABILITY_BW12:
case 'u': // unknown
default:
break;
case Router.CAPABILITY_BW32:
def *= 2;
break;
case Router.CAPABILITY_BW64:
def *= 3;
break;
case Router.CAPABILITY_BW128:
def *= 5;
break;
case Router.CAPABILITY_BW256:
def *= 9;
break;
case Router.CAPABILITY_BW512:
def *= 11;
break;
case Router.CAPABILITY_BW_UNLIMITED:
def *= 14;
break;
}
if (_context.netDb().floodfillEnabled()) {
// && !SystemVersion.isWindows()) {
def *= 17; def /= 10;
}
// increase limit for SSU, for now
if (style.equals("SSU"))
//def = def * 3 / 2;
def *= 3;
return _context.getProperty(maxProp, def);
}
private static final int DEFAULT_CAPACITY_PCT = 75;
/**
* Can we initiate or accept a connection to another peer, saving some margin
*/
public boolean haveCapacity() {
return haveCapacity(DEFAULT_CAPACITY_PCT);
}
/** @param pct are we under x% 0-100 */
public boolean haveCapacity(int pct) {
return countPeers() < getMaxConnections() * pct / 100;
}
/**
* Return our peer clock skews on a transport.
* Vector composed of Long, each element representing a peer skew in seconds.
* Dummy version. Transports override it.
*/
public Vector<Long> getClockSkews() { return new Vector<Long>(); }
public List<String> getMostRecentErrorMessages() { return Collections.emptyList(); }
/**
* Nonblocking call to pull the next outbound message
* off the queue.
*
* Only used by NTCP. SSU does not call.
*
* @return the next message or null if none are available
*/
protected OutNetMessage getNextMessage() {
OutNetMessage msg = _sendPool.poll();
if (msg != null)
msg.beginSend();
return msg;
}
/**
* The transport is done sending this message
*
* @param msg message in question
* @param sendSuccessful true if the peer received it
*/
protected void afterSend(OutNetMessage msg, boolean sendSuccessful) {
afterSend(msg, sendSuccessful, true, 0);
}
/**
* The transport is done sending this message
*
* @param msg message in question
* @param sendSuccessful true if the peer received it
* @param allowRequeue true if we should try other transports if available
*/
protected void afterSend(OutNetMessage msg, boolean sendSuccessful, boolean allowRequeue) {
afterSend(msg, sendSuccessful, allowRequeue, 0);
}
/**
* The transport is done sending this message
*
* @param msg message in question
* @param sendSuccessful true if the peer received it
* @param msToSend how long it took to transfer the data to the peer
*/
protected void afterSend(OutNetMessage msg, boolean sendSuccessful, long msToSend) {
afterSend(msg, sendSuccessful, true, msToSend);
}
/**
* The transport is done sending this message. This is the method that actually
* does all of the cleanup - firing off jobs, requeueing, updating stats, etc.
*
* @param msg message in question
* @param sendSuccessful true if the peer received it
* @param msToSend how long it took to transfer the data to the peer
* @param allowRequeue true if we should try other transports if available
*/
protected void afterSend(OutNetMessage msg, boolean sendSuccessful, boolean allowRequeue, long msToSend) {
if (msg.getTarget() == null) {
// Probably injected by the transport.
// Bail out now as it will NPE in a dozen places below.
return;
}
boolean log = false;
if (sendSuccessful)
msg.timestamp("afterSend(successful)");
else
msg.timestamp("afterSend(failed)");
if (!sendSuccessful)
msg.transportFailed(getStyle());
if (msToSend > 1500) {
if (_log.shouldLog(Log.INFO))
_log.info(getStyle() + " afterSend slow: " + (sendSuccessful ? "success " : "FAIL ")
+ msg.getMessageSize() + " byte "
+ msg.getMessageType() + ' ' + msg.getMessageId() + " to "
+ msg.getTarget().getIdentity().calculateHash().toBase64().substring(0,6) + " took " + msToSend + " ms");
}
//if (true)
// _log.error("(not error) I2NP message sent? " + sendSuccessful + " " + msg.getMessageId() + " after " + msToSend + "/" + msg.getTransmissionTime());
long lifetime = msg.getLifetime();
if (lifetime > 3000) {
int level = Log.INFO;
if (!sendSuccessful)
level = Log.DEBUG;
if (_log.shouldLog(level))
_log.log(level, getStyle() + " afterSend slow (" + (sendSuccessful ? "success " : "FAIL ")
+ lifetime + "/" + msToSend + "): " + msg.getMessageSize() + " byte "
+ msg.getMessageType() + " " + msg.getMessageId() + " from " + _context.routerHash().toBase64().substring(0,6)
+ " to " + msg.getTarget().getIdentity().calculateHash().toBase64().substring(0,6) + ": " + msg.toString());
} else {
if (_log.shouldLog(Log.INFO))
_log.info(getStyle() + " afterSend: " + (sendSuccessful ? "success " : "FAIL ")
+ msg.getMessageSize() + " byte "
+ msg.getMessageType() + " " + msg.getMessageId() + " from " + _context.routerHash().toBase64().substring(0,6)
+ " to " + msg.getTarget().getIdentity().calculateHash().toBase64().substring(0,6) + "\n" + msg.toString());
}
if (sendSuccessful) {
if (_log.shouldLog(Log.DEBUG))
_log.debug(getStyle() + " Sent " + msg.getMessageType() + " successfully to "
+ msg.getTarget().getIdentity().getHash().toBase64());
Job j = msg.getOnSendJob();
if (j != null)
_context.jobQueue().addJob(j);
log = true;
msg.discardData();
} else {
if (_log.shouldLog(Log.INFO))
_log.info(getStyle() + " Failed to send " + msg.getMessageType()
+ " to " + msg.getTarget().getIdentity().getHash().toBase64()
+ " (details: " + msg + ')');
if (msg.getExpiration() < _context.clock().now())
_context.statManager().addRateData("transport.expiredOnQueueLifetime", lifetime);
if (allowRequeue) {
if ( ( (msg.getExpiration() <= 0) || (msg.getExpiration() > _context.clock().now()) )
&& (msg.getMessage() != null) ) {
// this may not be the last transport available - keep going
_context.outNetMessagePool().add(msg);
// don't discard the data yet!
} else {
if (_log.shouldLog(Log.INFO))
_log.info("No more time left (" + new Date(msg.getExpiration())
+ ", expiring without sending successfully the "
+ msg.getMessageType());
if (msg.getOnFailedSendJob() != null)
_context.jobQueue().addJob(msg.getOnFailedSendJob());
MessageSelector selector = msg.getReplySelector();
if (selector != null) {
_context.messageRegistry().unregisterPending(msg);
}
log = true;
msg.discardData();
}
} else {
MessageSelector selector = msg.getReplySelector();
if (_log.shouldLog(Log.INFO))
_log.info("Failed and no requeue allowed for a "
+ msg.getMessageSize() + " byte "
+ msg.getMessageType() + " message with selector " + selector, new Exception("fail cause"));
if (msg.getOnFailedSendJob() != null)
_context.jobQueue().addJob(msg.getOnFailedSendJob());
if (msg.getOnFailedReplyJob() != null)
_context.jobQueue().addJob(msg.getOnFailedReplyJob());
if (selector != null)
_context.messageRegistry().unregisterPending(msg);
log = true;
msg.discardData();
}
}
if (log) {
/*
String type = msg.getMessageType();
// the udp transport logs some further details
_context.messageHistory().sendMessage(type, msg.getMessageId(),
msg.getExpiration(),
msg.getTarget().getIdentity().getHash(),
sendSuccessful);
*/
}
long now = _context.clock().now();
long sendTime = now - msg.getSendBegin();
long allTime = now - msg.getCreated();
if (allTime > 5*1000) {
if (_log.shouldLog(Log.INFO))
_log.info("Took too long from preparation to afterSend(ok? " + sendSuccessful
+ "): " + allTime + "ms/" + sendTime + "ms after failing on: "
+ msg.getFailedTransports() + " and succeeding on " + getStyle());
if ( (allTime > 60*1000) && (sendSuccessful) ) {
// VERY slow
if (_log.shouldLog(Log.WARN))
_log.warn("Severe latency? More than a minute slow? " + msg.getMessageType()
+ " of id " + msg.getMessageId() + " (send begin on "
+ new Date(msg.getSendBegin()) + " / created on "
+ new Date(msg.getCreated()) + "): " + msg);
_context.messageHistory().messageProcessingError(msg.getMessageId(),
msg.getMessageType(),
"Took too long to send [" + allTime + "ms]");
}
}
if (sendSuccessful) {
// TODO fix this stat for SSU ticket #698
_context.statManager().addRateData("transport.sendProcessingTime", lifetime);
// object churn. 33 ms for NTCP and 788 for SSU, but meaningless due to
// differences in how it's computed (immediate vs. round trip)
//_context.statManager().addRateData("transport.sendProcessingTime." + getStyle(), lifetime, 0);
_context.profileManager().messageSent(msg.getTarget().getIdentity().getHash(), getStyle(), sendTime, msg.getMessageSize());
_context.statManager().addRateData("transport.sendMessageSize", msg.getMessageSize(), sendTime);
} else {
_context.profileManager().messageFailed(msg.getTarget().getIdentity().getHash(), getStyle());
_context.statManager().addRateData("transport.sendMessageFailureLifetime", lifetime);
}
}
/**
* Asynchronously send the message as requested in the message and, if the
* send is successful, queue up any msg.getOnSendJob job, and register it
* with the OutboundMessageRegistry (if it has a reply selector). If the
* send fails, queue up any msg.getOnFailedSendJob
*
* Only used by NTCP. SSU overrides.
*
* Note that this adds to the queue and then takes it back off in the same thread,
* so it actually blocks, and we don't need a big queue.
*
* TODO: Override in NTCP also and get rid of queue?
*/
public void send(OutNetMessage msg) {
if (msg.getTarget() == null) {
if (_log.shouldLog(Log.ERROR))
_log.error("Error - bad message enqueued [target is null]: " + msg, new Exception("Added by"));
return;
}
try {
_sendPool.put(msg);
} catch (InterruptedException ie) {
if (_log.shouldLog(Log.ERROR))
_log.error("Interrupted during send " + msg);
return;
}
//if (_log.shouldLog(Log.DEBUG))
// _log.debug("Message added to send pool");
//msg.timestamp("send on " + getStyle());
outboundMessageReady();
//if (_log.shouldLog(Log.INFO))
// _log.debug("OutboundMessageReady called");
}
/**
* This message is called whenever a new message is added to the send pool,
* and it should not block
*
* Only used by NTCP. SSU throws UOE.
*/
protected abstract void outboundMessageReady();
/**
* Message received from the I2NPMessageReader - send it to the listener
*
* @param inMsg non-null
* @param remoteIdent may be null
* @param remoteIdentHash may be null, calculated from remoteIdent if null
*/
public void messageReceived(I2NPMessage inMsg, RouterIdentity remoteIdent, Hash remoteIdentHash, long msToReceive, int bytesReceived) {
//if (true)
// _log.error("(not error) I2NP message received: " + inMsg.getUniqueId() + " after " + msToReceive);
int level = Log.INFO;
if (msToReceive > 5000)
level = Log.WARN;
if (_log.shouldLog(level)) {
StringBuilder buf = new StringBuilder(128);
buf.append("Message received: ").append(inMsg.getClass().getSimpleName());
buf.append(" / ").append(inMsg.getUniqueId());
buf.append(" in ").append(msToReceive).append("ms containing ");
buf.append(bytesReceived).append(" bytes ");
buf.append(" from ");
if (remoteIdentHash != null) {
buf.append(remoteIdentHash.toBase64());
} else if (remoteIdent != null) {
buf.append(remoteIdent.getHash().toBase64());
} else {
buf.append("[unknown]");
}
buf.append(" and forwarding to listener: ");
if (_listener != null)
buf.append(_listener);
_log.log(level, buf.toString());
}
if (remoteIdent != null)
remoteIdentHash = remoteIdent.getHash();
if (remoteIdentHash != null) {
_context.profileManager().messageReceived(remoteIdentHash, getStyle(), msToReceive, bytesReceived);
_context.statManager().addRateData("transport.receiveMessageSize", bytesReceived, msToReceive);
}
_context.statManager().addRateData("transport.receiveMessageTime", msToReceive);
if (msToReceive > 1000) {
_context.statManager().addRateData("transport.receiveMessageTimeSlow", msToReceive);
}
//// this functionality is built into the InNetMessagePool
//String type = inMsg.getClass().getName();
//MessageHistory.getInstance().receiveMessage(type, inMsg.getUniqueId(), inMsg.getMessageExpiration(), remoteIdentHash, true);
if (_listener != null) {
_listener.messageReceived(inMsg, remoteIdent, remoteIdentHash);
} else {
if (_log.shouldLog(Log.ERROR))
_log.error("Null listener! this = " + toString(), new Exception("Null listener"));
}
}
/** Do we increase the advertised cost when approaching conn limits? */
protected static final boolean ADJUST_COST = true;
/** TODO change to 2 */
protected static final int CONGESTION_COST_ADJUSTMENT = 1;
/**
* What addresses are we currently listening to?
* Replaces getCurrentAddress()
* @return all addresses, non-null
* @since IPv6
*/
public List<RouterAddress> getCurrentAddresses() {
return _currentAddresses;
}
/**
* What address are we currently listening to?
* Replaces getCurrentAddress()
* @param ipv6 true for IPv6 only; false for IPv4 only
* @return first matching address or null
* @since IPv6
*/
public RouterAddress getCurrentAddress(boolean ipv6) {
for (RouterAddress ra : _currentAddresses) {
if (ipv6 == TransportUtil.isIPv6(ra))
return ra;
}
return null;
}
/**
* Do we have any current address?
* @since IPv6
*/
public boolean hasCurrentAddress() {
return !_currentAddresses.isEmpty();
}
/**
* Ask the transport to update its address based on current information and return it
* Transports should override.
* @return all addresses, non-null
* @since 0.7.12
*/
public List<RouterAddress> updateAddress() {
return _currentAddresses;
}
/**
* Replace any existing addresses for the current transport
* with the same IP length (4 or 16) with the given one.
* TODO: Allow multiple addresses of the same length.
* Calls listener.transportAddressChanged()
* To remove all IPv4 or IPv6 addresses, use removeAddress(boolean).
*
* @param address null to remove all
*/
protected void replaceAddress(RouterAddress address) {
if (_log.shouldLog(Log.WARN))
_log.warn("Replacing address with " + address, new Exception());
if (address == null) {
_currentAddresses.clear();
} else {
boolean isIPv6 = TransportUtil.isIPv6(address);
for (RouterAddress ra : _currentAddresses) {
if (isIPv6 == TransportUtil.isIPv6(ra))
// COWAL
_currentAddresses.remove(ra);
}
_currentAddresses.add(address);
}
if (_log.shouldLog(Log.WARN))
_log.warn(getStyle() + " now has " + _currentAddresses.size() + " addresses");
if (_listener != null)
_listener.transportAddressChanged();
}
/**
* Remove only this address.
* Calls listener.transportAddressChanged().
* To remove all IPv4 or IPv6 addresses, use removeAddress(boolean).
* To remove all IPv4 and IPv6 addresses, use replaceAddress(null).
*
* @since 0.9.20
*/
protected void removeAddress(RouterAddress address) {
if (_log.shouldWarn())
_log.warn("Removing address " + address, new Exception());
boolean changed = _currentAddresses.remove(address);
changed = true;
if (changed) {
if (_log.shouldWarn())
_log.warn(getStyle() + " now has " + _currentAddresses.size() + " addresses");
if (_listener != null)
_listener.transportAddressChanged();
} else {
if (_log.shouldWarn())
_log.warn(getStyle() + " no addresses removed");
}
}
/**
* Remove all existing addresses with the specified IP length (4 or 16).
* Calls listener.transportAddressChanged().
* To remove all IPv4 and IPv6 addresses, use replaceAddress(null).
*
* @param ipv6 true to remove all IPv6 addresses, false to remove all IPv4 addresses
* @since 0.9.20
*/
protected void removeAddress(boolean ipv6) {
if (_log.shouldWarn())
_log.warn("Removing addresses, ipv6? " + ipv6, new Exception());
boolean changed = false;
for (RouterAddress ra : _currentAddresses) {
if (ipv6 == TransportUtil.isIPv6(ra)) {
// COWAL
if (_currentAddresses.remove(ra))
changed = true;
}
}
if (changed) {
if (_log.shouldWarn())
_log.warn(getStyle() + " now has " + _currentAddresses.size() + " addresses");
if (_listener != null)
_listener.transportAddressChanged();
} else {
if (_log.shouldWarn())
_log.warn(getStyle() + " no addresses removed");
}
}
/**
* Save a local address we were notified about before we started.
*
* @since IPv6
*/
protected void saveLocalAddress(InetAddress address) {
_localAddresses.add(address);
}
/**
* Return and then clear all saved local addresses.
*
* @since IPv6
*/
protected Collection<InetAddress> getSavedLocalAddresses() {
List<InetAddress> rv = new ArrayList<InetAddress>(_localAddresses);
_localAddresses.clear();
return rv;
}
/**
* Get all available address we can use,
* shuffled and then sorted by cost/preference.
* Lowest cost (most preferred) first.
* @return non-null, possibly empty
* @since IPv6
*/
protected List<RouterAddress> getTargetAddresses(RouterInfo target) {
List<RouterAddress> rv = target.getTargetAddresses(getStyle());
if (rv.isEmpty())
return rv;
// Shuffle so everybody doesn't use the first one
if (rv.size() > 1)
Collections.shuffle(rv, _context.random());
TransportUtil.IPv6Config config = getIPv6Config();
int adj;
switch (config) {
case IPV6_DISABLED:
adj = 10;
/**** IPv6 addresses will be rejected in isPubliclyRoutable()
for (Iterator<RouterAddress> iter = rv.iterator(); iter.hasNext(); ) {
byte[] ip = iter.next().getIP();
if (ip != null && ip.length == 16)
iter.remove();
}
****/
break;
case IPV6_NOT_PREFERRED:
adj = 1; break;
default:
case IPV6_ENABLED:
adj = 0; break;
case IPV6_PREFERRED:
adj = -1; break;
case IPV6_ONLY:
adj = -10;
/**** IPv6 addresses will be rejected in isPubliclyRoutable()
for (Iterator<RouterAddress> iter = rv.iterator(); iter.hasNext(); ) {
byte[] ip = iter.next().getIP();
if (ip != null && ip.length == 4)
iter.remove();
}
****/
break;
}
if (rv.size() > 1)
Collections.sort(rv, new AddrComparator(adj));
return rv;
}
/**
* Compare based on published cost, adjusting for our IPv6 preference.
* Lowest cost (most preferred) first.
* @since IPv6
*/
private static class AddrComparator implements Comparator<RouterAddress>, Serializable {
private final int adj;
public AddrComparator(int ipv6Adjustment) {
adj = ipv6Adjustment;
}
public int compare(RouterAddress l, RouterAddress r) {
int lc = l.getCost();
int rc = r.getCost();
byte[] lip = l.getIP();
byte[] rip = r.getIP();
if (lip == null)
lc += 20;
else if (lip.length == 16)
lc += adj;
if (rip == null)
rc += 20;
else if (rip.length == 16)
rc += adj;
if (lc > rc)
return 1;
if (lc < rc)
return -1;
return 0;
}
}
/**
* 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 before startListening() to set an initial address,
* or after the transport is running.
*
* @param source defined in Transport.java
* @param ip typ. IPv4 or IPv6 non-local; may be null to indicate IPv4 failure or port info only
* @param port 0 for unknown or unchanged
*/
public abstract void externalAddressReceived(AddressSource source, byte[] ip, int 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)
*
* This implementation does nothing. Transports should override if they want notification.
*
* @param source defined in Transport.java
* @since 0.9.20
*/
public void externalAddressRemoved(AddressSource source, boolean ipv6) {}
/**
* Notify a transport of the results of trying to forward a port.
*
* This implementation does nothing. Transports should override if they want notification.
*
* @param ip may be null
* @param port the internal port
* @param externalPort the external port, which for now should always be the same as
* the internal port if the forwarding was successful.
*/
public void forwardPortStatus(byte[] ip, int port, int externalPort, boolean success, String reason) {}
/**
* What INTERNAL port would the transport like to have forwarded by UPnP.
* This can't be passed via getCurrentAddress(), as we have to open the port
* before we can publish the address, and that's the external port anyway.
*
* @return port or -1 for none or 0 for any
*/
public int getRequestedPort() { return -1; }
/** Who to notify on message availability */
public void setListener(TransportEventListener listener) { _listener = listener; }
/** Make this stuff pretty (only used in the old console) */
public void renderStatusHTML(Writer out) throws IOException {}
public void renderStatusHTML(Writer out, String urlBase, int sortFlags) throws IOException { renderStatusHTML(out); }
/**
* Previously returned short, now enum as of 0.9.20
*/
public abstract Status getReachabilityStatus();
/**
* @deprecated unused
*/
@Deprecated
public void recheckReachability() {}
/**
* This returns true if the force-firewalled setting is configured, false otherwise.
*
* @since 0.9.20, public since 0.9.30
*/
public boolean isIPv4Firewalled() {
return TransportUtil.isIPv4Firewalled(_context, getStyle());
}
/**
* This returns true if the force-firewalled setting is configured, false otherwise.
*
* @since 0.9.27, public since 0.9.30
*/
public boolean isIPv6Firewalled() {
return TransportUtil.isIPv6Firewalled(_context, getStyle());
}
public boolean isBacklogged(Hash peer) { return false; }
public boolean isEstablished(Hash peer) { return false; }
/**
* Tell the transport that we may disconnect from this peer.
* This is advisory only.
*
* @since 0.9.24
*/
public void mayDisconnect(Hash peer) {}
public boolean isUnreachable(Hash peer) {
long now = _context.clock().now();
synchronized (_unreachableEntries) {
Long when = _unreachableEntries.get(peer);
if (when == null) return false;
if (when.longValue() + UNREACHABLE_PERIOD < now) {
_unreachableEntries.remove(peer);
return false;
} else {
return true;
}
}
}
/** called when we can't reach a peer */
public void markUnreachable(Hash peer) {
Status status = _context.commSystem().getStatus();
if (status == Status.DISCONNECTED ||
status == Status.HOSED)
return;
Long now = Long.valueOf(_context.clock().now());
synchronized (_unreachableEntries) {
// This isn't very useful since it is cleared when they contact us
_unreachableEntries.put(peer, now);
}
// This is not cleared when they contact us
markWasUnreachable(peer, true);
}
/** called when we establish a peer connection (outbound or inbound) */
public void markReachable(Hash peer, boolean isInbound) {
// if *some* transport can reach them, then we shouldn't banlist 'em
_context.banlist().unbanlistRouter(peer);
synchronized (_unreachableEntries) {
_unreachableEntries.remove(peer);
}
if (!isInbound)
markWasUnreachable(peer, false);
}
private class CleanupUnreachable implements SimpleTimer.TimedEvent {
public void timeReached() {
long now = _context.clock().now();
synchronized (_unreachableEntries) {
for (Iterator<Long> iter = _unreachableEntries.values().iterator(); iter.hasNext(); ) {
Long when = iter.next();
if (when.longValue() + UNREACHABLE_PERIOD < now)
iter.remove();
}
}
synchronized (_wasUnreachableEntries) {
for (Iterator<Long> iter = _wasUnreachableEntries.values().iterator(); iter.hasNext(); ) {
Long when = iter.next();
if (when.longValue() + WAS_UNREACHABLE_PERIOD < now)
iter.remove();
}
}
}
}
/**
* Was the peer UNreachable (outbound only) the last time we tried it?
* This is NOT reset if the peer contacts us.
*/
public boolean wasUnreachable(Hash peer) {
long now = _context.clock().now();
synchronized (_wasUnreachableEntries) {
Long when = _wasUnreachableEntries.get(peer);
if (when != null) {
if (when.longValue() + WAS_UNREACHABLE_PERIOD < now) {
_unreachableEntries.remove(peer);
return false;
} else {
return true;
}
}
}
RouterInfo ri = _context.netDb().lookupRouterInfoLocally(peer);
if (ri == null)
return false;
return null == ri.getTargetAddress(this.getStyle());
}
/**
* Maintain the WasUnreachable list
*/
private void markWasUnreachable(Hash peer, boolean yes) {
if (yes) {
Long now = Long.valueOf(_context.clock().now());
synchronized (_wasUnreachableEntries) {
_wasUnreachableEntries.put(peer, now);
}
} else {
synchronized (_wasUnreachableEntries) {
_wasUnreachableEntries.remove(peer);
}
}
if (_log.shouldLog(Log.INFO))
_log.info(this.getStyle() + " setting wasUnreachable to " + yes + " for " + peer,
yes ? new Exception() : null);
}
/**
* Are we allowed to connect to local addresses?
*
* @since 0.9.28 moved from UDPTransport
*/
public boolean allowLocal() {
return _context.getBooleanProperty("i2np.allowLocal");
}
/**
* IP of the peer from the last connection (in or out, any transport).
*
* @param ip IPv4 or IPv6, non-null
*/
public void setIP(Hash peer, byte[] ip) {
byte[] old;
synchronized (_IPMap) {
old = _IPMap.put(peer, ip);
}
if (!DataHelper.eq(old, ip))
_context.commSystem().queueLookup(ip);
}
/**
* IP of the peer from the last connection (in or out, any transport).
*
* @return IPv4 or IPv6 or null
*/
public static byte[] getIP(Hash peer) {
synchronized (_IPMap) {
return _IPMap.get(peer);
}
}
/**
* @since 0.9.3
*/
static void clearCaches() {
synchronized(_IPMap) {
_IPMap.clear();
}
}
/**
* @since IPv6, public since 0.9.30
*/
public TransportUtil.IPv6Config getIPv6Config() {
return TransportUtil.getIPv6Config(_context, getStyle());
}
/**
* Allows IPv6 only if the transport is configured for it.
* Caller must check if we actually have a public IPv6 address.
* @param addr non-null
*/
protected boolean isPubliclyRoutable(byte addr[]) {
TransportUtil.IPv6Config cfg = getIPv6Config();
return TransportUtil.isPubliclyRoutable(addr,
cfg != TransportUtil.IPv6Config.IPV6_ONLY,
cfg != TransportUtil.IPv6Config.IPV6_DISABLED);
}
private static final String BUNDLE_NAME = "net.i2p.router.web.messages";
/**
* Translate
* @since 0.9.8 moved from transports
*/
protected String _t(String s) {
return Translate.getString(s, _context, BUNDLE_NAME);
}
/**
* Translate
* @since 0.9.8 moved from transports
*/
protected String _t(String s, Object o) {
return Translate.getString(s, o, _context, BUNDLE_NAME);
}
/**
* Translate
* @since 0.9.8
*/
protected String ngettext(String s, String p, int n) {
return Translate.getString(n, s, p, _context, BUNDLE_NAME);
}
}